testing for safelink
This commit is contained in:
Vendored
+3
-3
@@ -75,7 +75,7 @@ export class EventCache {
|
|||||||
`[EventCache] smartUpdateUrl called for url: ${event.url}, action: ${event.action}`,
|
`[EventCache] smartUpdateUrl called for url: ${event.url}, action: ${event.action}`,
|
||||||
)
|
)
|
||||||
if (event.action === ToolsOzoneSafelinkDefs.REMOVERULE) {
|
if (event.action === ToolsOzoneSafelinkDefs.REMOVERULE) {
|
||||||
// If the action is to remove the rule, delete it from the cache
|
// If the action is to remove the rule, update it from the cache
|
||||||
this.insert(event.url, event)
|
this.insert(event.url, event)
|
||||||
redirectLogger.info(`[EventCache] Removed rule for url: ${event.url}`)
|
redirectLogger.info(`[EventCache] Removed rule for url: ${event.url}`)
|
||||||
return
|
return
|
||||||
@@ -102,13 +102,13 @@ export class EventCache {
|
|||||||
smartUpdate(event: ToolsOzoneSafelinkDefs.Event) {
|
smartUpdate(event: ToolsOzoneSafelinkDefs.Event) {
|
||||||
if (event.pattern === ToolsOzoneSafelinkDefs.DOMAIN) {
|
if (event.pattern === ToolsOzoneSafelinkDefs.DOMAIN) {
|
||||||
redirectLogger.info(
|
redirectLogger.info(
|
||||||
`[EventCache] smartUpdate called for domain event: ${event.url}, performing$ ${event.action}`,
|
`[EventCache] smartUpdate called for domain event: ${event.url}, performing ${event.action}`,
|
||||||
)
|
)
|
||||||
return this.smartUpdateDomain(event)
|
return this.smartUpdateDomain(event)
|
||||||
}
|
}
|
||||||
if (event.pattern === ToolsOzoneSafelinkDefs.URL) {
|
if (event.pattern === ToolsOzoneSafelinkDefs.URL) {
|
||||||
redirectLogger.info(
|
redirectLogger.info(
|
||||||
`[EventCache] smartUpdate called for domain event: ${event.url}`,
|
`[EventCache] smartUpdate called for url event: ${event.url}`,
|
||||||
)
|
)
|
||||||
return this.smartUpdateUrl(event)
|
return this.smartUpdateUrl(event)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,11 +55,16 @@ export default function (ctx: AppContext, app: Express) {
|
|||||||
rulePresent.eventType === ToolsOzoneSafelinkDefs.REMOVERULE
|
rulePresent.eventType === ToolsOzoneSafelinkDefs.REMOVERULE
|
||||||
) {
|
) {
|
||||||
redirectLogger.info(
|
redirectLogger.info(
|
||||||
`No rule or remove rule matched for ${rulePresent.url}`,
|
`No rule or Remove rule matched for ${rulePresent.url}`,
|
||||||
)
|
)
|
||||||
const escaped = escapeHTML(url.href)
|
const escaped = escapeHTML(url.href)
|
||||||
|
const html = safe_redirect(escaped)
|
||||||
return safe_redirect(escaped)
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'text/html',
|
||||||
|
'Content-Length': Buffer.byteLength(html),
|
||||||
|
})
|
||||||
|
res.end(html) // Critical - must call end()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -68,8 +73,13 @@ export default function (ctx: AppContext, app: Express) {
|
|||||||
) {
|
) {
|
||||||
redirectLogger.info(`Whitelist rule matched for ${rulePresent.url}`)
|
redirectLogger.info(`Whitelist rule matched for ${rulePresent.url}`)
|
||||||
const escaped = escapeHTML(url.href)
|
const escaped = escapeHTML(url.href)
|
||||||
|
const html = safe_redirect(escaped)
|
||||||
return safe_redirect(escaped)
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'text/html',
|
||||||
|
'Content-Length': Buffer.byteLength(html),
|
||||||
|
})
|
||||||
|
res.end(html) // Critical - must call end()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -78,39 +88,50 @@ export default function (ctx: AppContext, app: Express) {
|
|||||||
) {
|
) {
|
||||||
redirectLogger.info(`Block rule matched for ${rulePresent.url}`)
|
redirectLogger.info(`Block rule matched for ${rulePresent.url}`)
|
||||||
res.setHeader('Cache-Control', 'no-store')
|
res.setHeader('Cache-Control', 'no-store')
|
||||||
res.status(403)
|
const html = warnRedirect(
|
||||||
return res.send(
|
'Blocked Link',
|
||||||
warnRedirect(
|
'This link has been identified as malicious, it has been blocked to protect your account and data',
|
||||||
'Blocked Link',
|
'Go Back To BlueSky',
|
||||||
'This link has been identified as malicious, it has been blocked to protect your account and data',
|
'DANGER',
|
||||||
'Go Back To BlueSky',
|
escapeHTML(url.toString()),
|
||||||
'DANGER',
|
`https://${ctx.cfg.service.appHostname}`,
|
||||||
escapeHTML(url.toString()),
|
|
||||||
`https://${ctx.cfg.service.appHostname}`,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
res.writeHead(403, {
|
||||||
|
'Content-Type': 'text/html',
|
||||||
|
'Content-Length': Buffer.byteLength(html),
|
||||||
|
})
|
||||||
|
res.end(html) // Critical - must call end()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rulePresent && rulePresent.action === ToolsOzoneSafelinkDefs.WARN) {
|
if (rulePresent && rulePresent.action === ToolsOzoneSafelinkDefs.WARN) {
|
||||||
redirectLogger.info(`Warn rule matched for ${rulePresent.url}`)
|
redirectLogger.info(`Warn rule matched for ${rulePresent.url}`)
|
||||||
res.setHeader('Cache-Control', 'no-store')
|
res.setHeader('Cache-Control', 'no-store')
|
||||||
res.status(403)
|
const html = warnRedirect(
|
||||||
const escaped = escapeHTML(url.href)
|
'Blocked Link',
|
||||||
return res.send(
|
'This link has been identified as malicious, it has been blocked to protect your account and data',
|
||||||
warnRedirect(
|
'Go Back To BlueSky',
|
||||||
'Warning: Potentially Malicious Link',
|
'DANGER',
|
||||||
'This link could be malicious, proceed at your own risk',
|
escapeHTML(url.toString()),
|
||||||
'Proceed at Your Own Risk',
|
`https://${ctx.cfg.service.appHostname}`,
|
||||||
'WARNING',
|
|
||||||
escapeHTML(url.toString()),
|
|
||||||
escaped, // Pass the actual URL as returnUrl so the button proceeds to the link
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
res.writeHead(403, {
|
||||||
|
'Content-Type': 'text/html',
|
||||||
|
'Content-Length': Buffer.byteLength(html),
|
||||||
|
})
|
||||||
|
res.end(html) // Critical - must call end()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const escaped = escapeHTML(url.href)
|
const escaped = escapeHTML(url.href)
|
||||||
return safe_redirect(escaped)
|
const html = safe_redirect(escaped)
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'text/html',
|
||||||
|
'Content-Length': Buffer.byteLength(html),
|
||||||
|
})
|
||||||
|
res.end(html) // Critical - must call end()
|
||||||
|
return
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+133
-2
@@ -1,7 +1,9 @@
|
|||||||
import assert from 'node:assert'
|
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 {after, before, describe, it} from 'node:test'
|
||||||
|
|
||||||
|
import {ToolsOzoneSafelinkDefs} from '@atproto/api'
|
||||||
|
|
||||||
import {Database, envToCfg, LinkService, readEnv} from '../src/index.js'
|
import {Database, envToCfg, LinkService, readEnv} from '../src/index.js'
|
||||||
|
|
||||||
describe('link service', async () => {
|
describe('link service', async () => {
|
||||||
@@ -15,6 +17,10 @@ describe('link service', async () => {
|
|||||||
appHostname: 'test.bsky.app',
|
appHostname: 'test.bsky.app',
|
||||||
dbPostgresSchema: 'link_test',
|
dbPostgresSchema: 'link_test',
|
||||||
dbPostgresUrl: process.env.DB_POSTGRES_URL,
|
dbPostgresUrl: process.env.DB_POSTGRES_URL,
|
||||||
|
safelink: 1,
|
||||||
|
ozoneUrl: 'http://localhost:2583',
|
||||||
|
ozoneAgentHandle: 'mod-authority.test',
|
||||||
|
ozoneAgentPass: 'hunter2',
|
||||||
})
|
})
|
||||||
const migrateDb = Database.postgres({
|
const migrateDb = Database.postgres({
|
||||||
url: cfg.db.url,
|
url: cfg.db.url,
|
||||||
@@ -26,8 +32,79 @@ describe('link service', async () => {
|
|||||||
await linkService.start()
|
await linkService.start()
|
||||||
const {port} = linkService.server?.address() as AddressInfo
|
const {port} = linkService.server?.address() as AddressInfo
|
||||||
baseUrl = `http://localhost:${port}`
|
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.BLOCK,
|
||||||
|
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 by due to an error',
|
||||||
|
})
|
||||||
|
// Ensure 'later' is after 'now'
|
||||||
|
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',
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
linkService.ctx.cfg.eventCache.smartGet(
|
||||||
|
'https://www.instagram.com/teamseshbones/?hl=en',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
after(async () => {
|
after(async () => {
|
||||||
await linkService?.destroy()
|
await linkService?.destroy()
|
||||||
})
|
})
|
||||||
@@ -76,6 +153,60 @@ describe('link service', async () => {
|
|||||||
assert.strictEqual(json.message, 'Link not found')
|
assert.strictEqual(json.message, 'Link not found')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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'})
|
||||||
|
// The server returns an HTML meta refresh, not a real HTTP redirect
|
||||||
|
// So status will be 200, not 301/303, and there is no Location header
|
||||||
|
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, '\\$&')),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
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'})
|
||||||
|
// The server returns an HTML meta refresh, not a real HTTP redirect
|
||||||
|
// So status will be 200, not 301/303, and there is no Location header
|
||||||
|
assert.strictEqual(res.status, 403)
|
||||||
|
const html = await res.text()
|
||||||
|
assert.match(
|
||||||
|
html,
|
||||||
|
new RegExp(urlToRedirect.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
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'})
|
||||||
|
// The server returns an HTML meta refresh, not a real HTTP redirect
|
||||||
|
// So status will be 200, not 301/303, and there is no Location header
|
||||||
|
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]> {
|
async function getRedirect(link: string): Promise<[number, string]> {
|
||||||
const url = new URL(link)
|
const url = new URL(link)
|
||||||
const base = new URL(baseUrl)
|
const base = new URL(baseUrl)
|
||||||
|
|||||||
Reference in New Issue
Block a user