CI / Build & Quality Checks (pull_request) Successful in 1m47s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 7s
CI / Playwright smoke (e2e) (pull_request) Failing after 11m30s
Two easy bypasses of the "this link goes somewhere else" check: - Any whitespace in the link text disabled it, so "Log in at chat.lotusguild.org" → evil.example wasn't flagged. hostsFromText now scans each word for host-shaped tokens; a link is flagged if any named host is a different site. Bare file names (main.py, report.pdf) and version numbers are skipped inside sentences so ordinary links aren't flagged. - Shared-hosting suffixes counted as one site (alice.github.io vs evil.github.io, *.pages.dev, netlify.app, vercel.app, …). registrableDomain now treats a curated list of them as public suffixes (longest match, incl. s3.amazonaws.com). Curated rather than the full PSL to stay inside the bundle budget. Verified with unit tests and headless on real messages: the four bypass cases are flagged; the honest sentence, file/version text and "click here" are not. Fixes #335 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
76 lines
3.5 KiB
TypeScript
76 lines
3.5 KiB
TypeScript
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { analyzeLink, hostFromText, registrableDomain } from './linkSafety';
|
||
|
||
test('visible text that names another site is a mismatch', () => {
|
||
const r = analyzeLink('https://matrix.lotusguild.org/login', 'https://evil.example/login');
|
||
assert.equal(r?.mismatch, true);
|
||
assert.equal(r?.shownHost, 'matrix.lotusguild.org');
|
||
assert.equal(r?.realHost, 'evil.example');
|
||
});
|
||
|
||
test('honest links are not flagged: same registrable domain, plain words, non-http', () => {
|
||
assert.equal(
|
||
analyzeLink('youtube.com/watch?v=1', 'https://www.youtube.com/watch?v=1')?.mismatch,
|
||
false,
|
||
);
|
||
assert.equal(analyzeLink('bbc.co.uk', 'https://news.bbc.co.uk/x')?.mismatch, false);
|
||
assert.equal(analyzeLink('click here', 'https://evil.example')?.mismatch, false);
|
||
assert.equal(analyzeLink('evil.example', 'mailto:someone@evil.example'), null);
|
||
assert.equal(analyzeLink('elsewhere.org', 'https://matrix.to/#/#room:x'), null);
|
||
assert.equal(
|
||
analyzeLink('lotusguild.org', 'https://chat.lotusguild.org/home/!r:x')?.mismatch,
|
||
false,
|
||
);
|
||
});
|
||
|
||
test('registrable domain handles two-label suffixes', () => {
|
||
assert.equal(registrableDomain('news.bbc.co.uk'), 'bbc.co.uk');
|
||
assert.equal(registrableDomain('www.example.com'), 'example.com');
|
||
assert.equal(registrableDomain('example.com'), 'example.com');
|
||
});
|
||
|
||
test('hostFromText only accepts URL/host-shaped text; IDN becomes punycode', () => {
|
||
assert.equal(hostFromText('Visit https://a.example/path'), null);
|
||
assert.equal(hostFromText('a.example/path'), 'a.example');
|
||
assert.equal(hostFromText('user@a.example:8448/x'), 'a.example');
|
||
assert.equal(hostFromText('pаypal.com'), 'xn--pypal-4ve.com'); // Cyrillic а
|
||
assert.equal(analyzeLink('paypal.com', 'https://xn--pypal-4ve.com/')?.punycode, true);
|
||
assert.equal(analyzeLink('paypal.com', 'https://xn--pypal-4ve.com/')?.mismatch, true);
|
||
});
|
||
|
||
test('a trusted name inside a sentence still exposes a different destination (#335)', () => {
|
||
const a = analyzeLink('Log in at chat.lotusguild.org', 'https://evil.example/');
|
||
assert.equal(a?.mismatch, true);
|
||
assert.equal(a?.shownHost, 'chat.lotusguild.org');
|
||
assert.equal(
|
||
analyzeLink('chat.lotusguild.org (official)', 'https://evil.example/')?.mismatch,
|
||
true,
|
||
);
|
||
assert.equal(
|
||
analyzeLink('Open https://paypal.com/login now', 'https://evil.example/')?.mismatch,
|
||
true,
|
||
);
|
||
// honest sentences are not flagged
|
||
assert.equal(
|
||
analyzeLink('Log in at chat.lotusguild.org', 'https://chat.lotusguild.org/login')?.mismatch,
|
||
false,
|
||
);
|
||
assert.equal(analyzeLink('read the docs', 'https://evil.example/')?.mismatch, false);
|
||
assert.equal(analyzeLink('see main.py and report.pdf', 'https://github.com/x')?.mismatch, false);
|
||
assert.equal(analyzeLink('release v1.2.3 notes', 'https://github.com/x')?.mismatch, false);
|
||
});
|
||
|
||
test('shared hosting suffixes are separate sites (#335)', () => {
|
||
assert.equal(registrableDomain('alice.github.io'), 'alice.github.io');
|
||
assert.equal(registrableDomain('www.alice.github.io'), 'alice.github.io');
|
||
assert.equal(registrableDomain('bucket.s3.amazonaws.com'), 'bucket.s3.amazonaws.com');
|
||
assert.equal(analyzeLink('alice.github.io', 'https://evil.github.io/')?.mismatch, true);
|
||
assert.equal(analyzeLink('lotusguild.pages.dev', 'https://evil.pages.dev/')?.mismatch, true);
|
||
assert.equal(
|
||
analyzeLink('alice.github.io/blog', 'https://alice.github.io/blog/1')?.mismatch,
|
||
false,
|
||
);
|
||
assert.equal(registrableDomain('news.bbc.co.uk'), 'bbc.co.uk');
|
||
});
|