CI / Build & Quality Checks (pull_request) Successful in 3m6s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 9s
CI / Playwright smoke (e2e) (pull_request) Successful in 21m16s
Previews are built from the plain body, clicks follow formatted_body's hrefs, and a sender controls both: text "https://paypal.com" linked to evil.example showed a real PayPal card. Drop body URLs whose site is only claimed by the visible text of a mismatched link (linkSafety). Fixes #286 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
98 lines
4.2 KiB
TypeScript
98 lines
4.2 KiB
TypeScript
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { analyzeLink, hostFromText, previewUrlsFor, 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');
|
||
});
|
||
|
||
test('previewUrlsFor: no preview for a site a deceptive link only claims (#286)', () => {
|
||
const urls = ['https://paypal.com'];
|
||
assert.deepEqual(
|
||
previewUrlsFor(urls, '<a href="https://evil.example/login">https://paypal.com</a>'),
|
||
[],
|
||
);
|
||
// An honest link keeps its preview.
|
||
assert.deepEqual(
|
||
previewUrlsFor(urls, '<a href="https://www.paypal.com/x">https://paypal.com</a>'),
|
||
urls,
|
||
);
|
||
assert.deepEqual(previewUrlsFor(urls, undefined), urls);
|
||
// Other links in the message keep theirs.
|
||
assert.deepEqual(
|
||
previewUrlsFor(
|
||
['https://paypal.com', 'https://lotusguild.org'],
|
||
'see <a href="https://evil.example">paypal.com</a> and https://lotusguild.org',
|
||
),
|
||
['https://lotusguild.org'],
|
||
);
|
||
});
|