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';