safelink (#8917)
Co-authored-by: hailey <me@haileyok.com> Co-authored-by: Stanislas Signoud <signez@stanisoft.net> Co-authored-by: will berry <wsb@wills-MBP.attlocal.net> Co-authored-by: BlueSkiesAndGreenPastures <will@blueskyweb.xyz> Co-authored-by: Chenyu Huang <itschenyu@gmail.com>
This commit is contained in:
+239
-2
@@ -1,7 +1,9 @@
|
||||
import assert from 'node:assert'
|
||||
import {AddressInfo} from 'node:net'
|
||||
import {type AddressInfo} from 'node:net'
|
||||
import {after, before, describe, it} from 'node:test'
|
||||
|
||||
import {ToolsOzoneSafelinkDefs} from '@atproto/api'
|
||||
|
||||
import {Database, envToCfg, LinkService, readEnv} from '../src/index.js'
|
||||
|
||||
describe('link service', async () => {
|
||||
@@ -15,6 +17,10 @@ describe('link service', async () => {
|
||||
appHostname: 'test.bsky.app',
|
||||
dbPostgresSchema: 'link_test',
|
||||
dbPostgresUrl: process.env.DB_POSTGRES_URL,
|
||||
safelinkEnabled: true,
|
||||
ozoneUrl: 'http://localhost:2583',
|
||||
ozoneAgentHandle: 'mod-authority.test',
|
||||
ozoneAgentPass: 'hunter2',
|
||||
})
|
||||
const migrateDb = Database.postgres({
|
||||
url: cfg.db.url,
|
||||
@@ -26,8 +32,85 @@ describe('link service', async () => {
|
||||
await linkService.start()
|
||||
const {port} = linkService.server?.address() as AddressInfo
|
||||
baseUrl = `http://localhost:${port}`
|
||||
})
|
||||
|
||||
// Ensure blocklist, whitelist, and safelink rules are set up
|
||||
const now = new Date().toISOString()
|
||||
linkService.ctx.cfg.eventCache.smartUpdate({
|
||||
$type: 'tools.ozone.safelink.defs#event',
|
||||
id: 1,
|
||||
eventType: ToolsOzoneSafelinkDefs.ADDRULE,
|
||||
url: 'https://en.wikipedia.org/wiki/Fight_Club',
|
||||
pattern: ToolsOzoneSafelinkDefs.URL,
|
||||
action: ToolsOzoneSafelinkDefs.WARN,
|
||||
reason: ToolsOzoneSafelinkDefs.SPAM,
|
||||
createdBy: 'did:example:admin',
|
||||
createdAt: now,
|
||||
comment: 'Do not talk about Fight Club',
|
||||
})
|
||||
linkService.ctx.cfg.eventCache.smartUpdate({
|
||||
$type: 'tools.ozone.safelink.defs#event',
|
||||
id: 2,
|
||||
eventType: ToolsOzoneSafelinkDefs.ADDRULE,
|
||||
url: 'https://gist.github.com/MattIPv4/045239bc27b16b2bcf7a3a9a4648c08a',
|
||||
pattern: ToolsOzoneSafelinkDefs.URL,
|
||||
action: ToolsOzoneSafelinkDefs.BLOCK,
|
||||
reason: ToolsOzoneSafelinkDefs.SPAM,
|
||||
createdBy: 'did:example:admin',
|
||||
createdAt: now,
|
||||
comment: 'All Bs',
|
||||
})
|
||||
linkService.ctx.cfg.eventCache.smartUpdate({
|
||||
$type: 'tools.ozone.safelink.defs#event',
|
||||
id: 3,
|
||||
eventType: ToolsOzoneSafelinkDefs.ADDRULE,
|
||||
url: 'https://en.wikipedia.org',
|
||||
pattern: ToolsOzoneSafelinkDefs.DOMAIN,
|
||||
action: ToolsOzoneSafelinkDefs.WHITELIST,
|
||||
reason: ToolsOzoneSafelinkDefs.NONE,
|
||||
createdBy: 'did:example:admin',
|
||||
createdAt: now,
|
||||
comment: 'Whitelisting the knowledge base of the internet',
|
||||
})
|
||||
linkService.ctx.cfg.eventCache.smartUpdate({
|
||||
$type: 'tools.ozone.safelink.defs#event',
|
||||
id: 4,
|
||||
eventType: ToolsOzoneSafelinkDefs.ADDRULE,
|
||||
url: 'https://www.instagram.com/teamseshbones/?hl=en',
|
||||
pattern: ToolsOzoneSafelinkDefs.URL,
|
||||
action: ToolsOzoneSafelinkDefs.BLOCK,
|
||||
reason: ToolsOzoneSafelinkDefs.SPAM,
|
||||
createdBy: 'did:example:admin',
|
||||
createdAt: now,
|
||||
comment: 'BONES has been erroneously blocked for the sake of this test',
|
||||
})
|
||||
const later = new Date(Date.now() + 1000).toISOString()
|
||||
linkService.ctx.cfg.eventCache.smartUpdate({
|
||||
$type: 'tools.ozone.safelink.defs#event',
|
||||
id: 5,
|
||||
eventType: ToolsOzoneSafelinkDefs.REMOVERULE,
|
||||
url: 'https://www.instagram.com/teamseshbones/?hl=en',
|
||||
pattern: ToolsOzoneSafelinkDefs.URL,
|
||||
action: ToolsOzoneSafelinkDefs.REMOVERULE,
|
||||
reason: ToolsOzoneSafelinkDefs.NONE,
|
||||
createdBy: 'did:example:admin',
|
||||
createdAt: later,
|
||||
comment:
|
||||
'BONES has been resurrected to bring good music to the world once again',
|
||||
})
|
||||
linkService.ctx.cfg.eventCache.smartUpdate({
|
||||
$type: 'tools.ozone.safelink.defs#event',
|
||||
id: 6,
|
||||
eventType: ToolsOzoneSafelinkDefs.ADDRULE,
|
||||
url: 'https://www.leagueoflegends.com/en-us/',
|
||||
pattern: ToolsOzoneSafelinkDefs.URL,
|
||||
action: ToolsOzoneSafelinkDefs.WARN,
|
||||
reason: ToolsOzoneSafelinkDefs.SPAM,
|
||||
createdBy: 'did:example:admin',
|
||||
createdAt: now,
|
||||
comment:
|
||||
'Could be quite the mistake to get into this addicting game, but we will warn instead of block',
|
||||
})
|
||||
})
|
||||
after(async () => {
|
||||
await linkService?.destroy()
|
||||
})
|
||||
@@ -76,6 +159,80 @@ describe('link service', async () => {
|
||||
assert.strictEqual(json.message, 'Link not found')
|
||||
})
|
||||
|
||||
it('League of Legends warned', async () => {
|
||||
const urlToRedirect = 'https://www.leagueoflegends.com/en-us/'
|
||||
const url = new URL(`${baseUrl}/redirect`)
|
||||
url.searchParams.set('u', urlToRedirect)
|
||||
const res = await fetch(url, {redirect: 'manual'})
|
||||
assert.strictEqual(res.status, 200)
|
||||
const html = await res.text()
|
||||
assert.match(
|
||||
html,
|
||||
new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
)
|
||||
// League of Legends is set to WARN, not BLOCK, so expect a warning (blocked-site div present)
|
||||
assert.match(
|
||||
html,
|
||||
/Warning: Malicious Link/,
|
||||
'Expected warning not found in HTML',
|
||||
)
|
||||
})
|
||||
|
||||
it('Wikipedia whitelisted, url restricted. Redirect safely since wikipedia is whitelisted', async () => {
|
||||
const urlToRedirect = 'https://en.wikipedia.org/wiki/Fight_Club'
|
||||
const url = new URL(`${baseUrl}/redirect`)
|
||||
url.searchParams.set('u', urlToRedirect)
|
||||
const res = await fetch(url, {redirect: 'manual'})
|
||||
assert.strictEqual(res.status, 200)
|
||||
const html = await res.text()
|
||||
assert.match(html, /meta http-equiv="refresh"/)
|
||||
assert.match(
|
||||
html,
|
||||
new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
)
|
||||
// Wikipedia domain is whitelisted, so no blocked-site div should be present
|
||||
assert.doesNotMatch(html, /"blocked-site"/)
|
||||
})
|
||||
|
||||
it('Unsafe redirect with block rule, due to the content of webpage.', async () => {
|
||||
const urlToRedirect =
|
||||
'https://gist.github.com/MattIPv4/045239bc27b16b2bcf7a3a9a4648c08a'
|
||||
const url = new URL(`${baseUrl}/redirect`)
|
||||
url.searchParams.set('u', urlToRedirect)
|
||||
const res = await fetch(url, {redirect: 'manual'})
|
||||
assert.strictEqual(res.status, 200)
|
||||
const html = await res.text()
|
||||
assert.match(
|
||||
html,
|
||||
new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
)
|
||||
assert.match(
|
||||
html,
|
||||
/"blocked-site"/,
|
||||
'Expected blocked-site div not found in HTML',
|
||||
)
|
||||
})
|
||||
|
||||
it('Rule adjustment, safe redirect, 200 response for Instagram Account of teamsesh Bones', async () => {
|
||||
// Retrieve the latest event after all updates
|
||||
const result = linkService.ctx.cfg.eventCache.smartGet(
|
||||
'https://www.instagram.com/teamseshbones/?hl=en',
|
||||
)
|
||||
assert(result, 'Expected event not found in eventCache')
|
||||
assert.strictEqual(result.eventType, ToolsOzoneSafelinkDefs.REMOVERULE)
|
||||
const urlToRedirect = 'https://www.instagram.com/teamseshbones/?hl=en'
|
||||
const url = new URL(`${baseUrl}/redirect`)
|
||||
url.searchParams.set('u', urlToRedirect)
|
||||
const res = await fetch(url, {redirect: 'manual'})
|
||||
assert.strictEqual(res.status, 200)
|
||||
const html = await res.text()
|
||||
assert.match(html, /meta http-equiv="refresh"/)
|
||||
assert.match(
|
||||
html,
|
||||
new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
)
|
||||
})
|
||||
|
||||
async function getRedirect(link: string): Promise<[number, string]> {
|
||||
const url = new URL(link)
|
||||
const base = new URL(baseUrl)
|
||||
@@ -121,3 +278,83 @@ describe('link service', async () => {
|
||||
return payload.url
|
||||
}
|
||||
})
|
||||
|
||||
describe('link service no safelink', async () => {
|
||||
let linkService: LinkService
|
||||
let baseUrl: string
|
||||
before(async () => {
|
||||
const env = readEnv()
|
||||
const cfg = envToCfg({
|
||||
...env,
|
||||
hostnames: ['test.bsky.link'],
|
||||
appHostname: 'test.bsky.app',
|
||||
dbPostgresSchema: 'link_test',
|
||||
dbPostgresUrl: process.env.DB_POSTGRES_URL,
|
||||
safelinkEnabled: false,
|
||||
ozoneUrl: 'http://localhost:2583',
|
||||
ozoneAgentHandle: 'mod-authority.test',
|
||||
ozoneAgentPass: 'hunter2',
|
||||
})
|
||||
const migrateDb = Database.postgres({
|
||||
url: cfg.db.url,
|
||||
schema: cfg.db.schema,
|
||||
})
|
||||
await migrateDb.migrateToLatestOrThrow()
|
||||
await migrateDb.close()
|
||||
linkService = await LinkService.create(cfg)
|
||||
await linkService.start()
|
||||
const {port} = linkService.server?.address() as AddressInfo
|
||||
baseUrl = `http://localhost:${port}`
|
||||
})
|
||||
after(async () => {
|
||||
await linkService?.destroy()
|
||||
})
|
||||
it('Wikipedia whitelisted, url restricted. Safelink is disabled, so redirect is always safe', async () => {
|
||||
const urlToRedirect = 'https://en.wikipedia.org/wiki/Fight_Club'
|
||||
const url = new URL(`${baseUrl}/redirect`)
|
||||
url.searchParams.set('u', urlToRedirect)
|
||||
const res = await fetch(url, {redirect: 'manual'})
|
||||
assert.strictEqual(res.status, 200)
|
||||
const html = await res.text()
|
||||
assert.match(html, /meta http-equiv="refresh"/)
|
||||
assert.match(
|
||||
html,
|
||||
new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
)
|
||||
// No blocked-site div, always safe
|
||||
assert.doesNotMatch(html, /"blocked-site"/)
|
||||
})
|
||||
|
||||
it('Unsafe redirect with block rule, but safelink is disabled so redirect is always safe', async () => {
|
||||
const urlToRedirect =
|
||||
'https://gist.github.com/MattIPv4/045239bc27b16b2bcf7a3a9a4648c08a'
|
||||
const url = new URL(`${baseUrl}/redirect`)
|
||||
url.searchParams.set('u', urlToRedirect)
|
||||
const res = await fetch(url, {redirect: 'manual'})
|
||||
assert.strictEqual(res.status, 200)
|
||||
const html = await res.text()
|
||||
assert.match(html, /meta http-equiv="refresh"/)
|
||||
assert.match(
|
||||
html,
|
||||
new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
)
|
||||
// No blocked-site div, always safe
|
||||
assert.doesNotMatch(html, /"blocked-site"/)
|
||||
})
|
||||
|
||||
it('Rule adjustment, safe redirect, safelink is disabled so always safe', async () => {
|
||||
const urlToRedirect = 'https://www.instagram.com/teamseshbones/?hl=en'
|
||||
const url = new URL(`${baseUrl}/redirect`)
|
||||
url.searchParams.set('u', urlToRedirect)
|
||||
const res = await fetch(url, {redirect: 'manual'})
|
||||
assert.strictEqual(res.status, 200)
|
||||
const html = await res.text()
|
||||
assert.match(html, /meta http-equiv="refresh"/)
|
||||
assert.match(
|
||||
html,
|
||||
new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
)
|
||||
// No blocked-site div, always safe
|
||||
assert.doesNotMatch(html, /"blocked-site"/)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user