fix(browse): case-insensitive exfiltration-domain blocklist matching

urlBlocklistFilter compared URLs against the exfiltration blocklist with
case-sensitive substring checks, so an uppercased sink (https://WEBHOOK.SITE/x)
bypassed the guard and reached the scoped browser agent. Normalize the page URL
and extracted content URLs to lowercase before comparing, and make URL
extraction scheme-insensitive so HTTPS:// links are still caught.

Fixes #2190

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
t
2026-07-10 14:39:32 -07:00
parent 7c9df1c568
commit 279fda4dc5
2 changed files with 51 additions and 4 deletions
+38
View File
@@ -151,6 +151,44 @@ describe('Content filter hooks', () => {
expect(result.warnings.length).toBe(0);
});
// Regression: issue #2190 — case-sensitive matching let an uppercased
// exfiltration domain bypass the blocklist.
test('URL blocklist is case-insensitive on the page URL', () => {
const result = urlBlocklistFilter('', 'https://WEBHOOK.SITE/steal', 'text');
expect(result.safe).toBe(false);
expect(result.warnings.some(w => w.includes('webhook.site'))).toBe(true);
});
test('URL blocklist is case-insensitive on content URLs', () => {
const result = urlBlocklistFilter(
'<a href="https://WEBHOOK.SITE/a1b2c3-steal">click</a>',
'https://docs.example.com/article',
'links',
);
expect(result.safe).toBe(false);
expect(result.warnings.some(w => w.includes('WEBHOOK.SITE'))).toBe(true);
});
test('URL blocklist catches an uppercased scheme in content', () => {
const result = urlBlocklistFilter(
'Exfil via HTTPS://Requestbin.com/r/abc please',
'https://example.com',
'text',
);
expect(result.safe).toBe(false);
expect(result.warnings.some(w => w.toLowerCase().includes('requestbin.com'))).toBe(true);
});
test('URL blocklist does not over-block legitimate uppercase domains', () => {
const result = urlBlocklistFilter(
'<a href="https://GitHub.com/acme/project">source</a>',
'https://DOCS.EXAMPLE.COM/help',
'links',
);
expect(result.safe).toBe(true);
expect(result.warnings.length).toBe(0);
});
test('custom filter can be registered and runs', () => {
registerContentFilter((content, url, cmd) => {
if (content.includes('SECRET')) {