diff --git a/scripts/lib/uri-safety.mjs b/scripts/lib/uri-safety.mjs index 7672d1a4..9814ef95 100644 --- a/scripts/lib/uri-safety.mjs +++ b/scripts/lib/uri-safety.mjs @@ -120,6 +120,33 @@ export function activeScheme(value) { */ export function remoteTarget(value) { const normalized = normalizeUri(value); + // A scheme does not make a value absolute on its own: the parser only + // enters the relative states when the value's scheme equals the base + // document's. The site is served over https (`url` in + // docusaurus.config.js), so `https:local.png` is the path `local.png` on + // this origin and is correctly left alone below. + // + // `http:` is a *different* special scheme, so it never reaches those + // states. It goes to "special authority ignore slashes", which skips + // however many `/` or `\` follow -- including none -- and reads what comes + // next as the host. `http:evil.example/b.png`, `http:/evil.example/b.png` + // and `http:\evil.example/b.png` therefore load from evil.example exactly + // as `http://evil.example/b.png` does, while matching neither the `//` + // test nor the two-or-more-separator rewrite below. + // + // Being cleartext does not make the reference harmless: a browser that + // blocks the mixed-content subresource has already been told to, and one + // that auto-upgrades it to https still sends the request -- with the + // visitor's IP, User-Agent and Referer -- to the host named here. + // + // Lookahead: a value with nothing after the separators (`http:`, `http:/`, + // `http://`) has no host, and one continuing with `?` or `#` is not a URL + // the parser accepts, so neither fetches anything to report. + const insecureAuthority = /^http:[/\\]*(?=[^/\\?#])/.exec(normalized); + if (insecureAuthority) { + return `http://${normalized.slice(insecureAuthority[0].length)}`; + } + // The URL parser treats `\` as `/` in the scheme and authority prefix of a // special-scheme URL, and the site is served over https, so every relative // reference resolves against a special-scheme base. `\\host`, `/\host`, diff --git a/tests/svg-active-content.test.mjs b/tests/svg-active-content.test.mjs index 2d4c98d9..a838e26e 100644 --- a/tests/svg-active-content.test.mjs +++ b/tests/svg-active-content.test.mjs @@ -507,6 +507,50 @@ for (const [label, value] of [ }); } +// `http:` is not the scheme this site is served over, so the parser never +// treats it as relative: it skips however many separators follow -- including +// none -- and reads the next component as the host. Each value below loads +// from evil.example in a browser exactly as `http://evil.example` does, and a +// browser that auto-upgrades the mixed-content request still sends the +// visitor's IP, User-Agent and Referer to that host. +for (const [value, expected] of [ + ['http:evil.example/b.png', 'http://evil.example/b.png'], + ['http:/evil.example/b.png', 'http://evil.example/b.png'], + ['http:\\evil.example/b.png', 'http://evil.example/b.png'], + ['http:///evil.example/b.png', 'http://evil.example/b.png'], + ['HTTP:evil.example/b.png', 'http://evil.example/b.png'], +]) { + test(`detects a slashless http: authority: ${value}`, () => { + const svg = ``; + assert.deepEqual(findRemoteReferences(svg), [ + `references a remote resource in href: ${expected}`, + ]); + }); +} + +// The same shape under the *site's own* scheme is relative, and a scheme with +// no host after it is not a URL the parser accepts. Flagging either would +// fail a diagram that references its own sibling assets. +for (const [label, value] of [ + ['an https: value with no separator, which is a sibling path', 'https:a.png'], + ['an http: scheme with no host at all', 'http:'], + ['an http: scheme followed only by a query', 'http:?q'], + ['an http: scheme followed only by a fragment', 'http:#f'], +]) { + test(`does not flag ${label}`, () => { + const svg = ``; + assert.deepEqual(findRemoteReferences(svg), []); + }); +} + +test('detects a slashless http: authority in a CSS url()', () => { + const svg = + ''; + assert.deepEqual(findRemoteReferences(svg), [ + 'references a remote resource in a a';