diff --git a/src/analyzer/tests/subresource-integrity.js b/src/analyzer/tests/subresource-integrity.js index 9300014..89d75c1 100644 --- a/src/analyzer/tests/subresource-integrity.js +++ b/src/analyzer/tests/subresource-integrity.js @@ -74,6 +74,16 @@ export function subresourceIntegrityTest( } // Track to see if any scripts were on foreign TLDs. let scriptsOnForeignOrigin = false; + + // Protocol-relative URLs (//cdn.example.com/…) inherit the page's scheme. + // They are only safe when the site ensures HTTP is never served: either + // there is no HTTP server at all, or HTTP always redirects to HTTPS. + const httpRedirects = requests.responses.httpRedirects; + const httpEnforcesHttps = + !requests.responses.http || + (httpRedirects.length > 1 && + httpRedirects.at(-1)?.url.protocol === "https:"); + for (const script of scripts) { const scriptSrc = getAttribute(script, "src"); if (scriptSrc) { @@ -91,7 +101,9 @@ export function subresourceIntegrityTest( if (relativeProtocolRegex.test(scriptSrc)) { // relative protocol(src="//example.com/script.js") relativeProtocol = true; - sameSecondLevelDomain = true; + sameSecondLevelDomain = + parse("https:" + scriptSrc).domain === + parse(requests.site.hostname).domain; } else if (fullUrlRegex.test(scriptSrc)) { // full URL (src="https://example.com/script.js") sameSecondLevelDomain = @@ -119,7 +131,8 @@ export function subresourceIntegrityTest( let secureScheme = false; if ( scheme === "https:" || - (relativeOrigin && requests.session?.url.protocol === "https:") + (relativeOrigin && requests.session?.url.protocol === "https:") || + (relativeProtocol && httpEnforcesHttps) ) { secureScheme = true; } diff --git a/test/subresource-integrity.test.js b/test/subresource-integrity.test.js index bdaebe8..ca5c626 100644 --- a/test/subresource-integrity.test.js +++ b/test/subresource-integrity.test.js @@ -35,9 +35,22 @@ describe("Subresource Integrity", () => { ); assert.isTrue(result.pass); - // On the same second-level domain, but without a protocol + // On the same second-level domain, but without a protocol — when HTTP + // redirects to HTTPS the protocol-relative URL is safe, so only -5 (issue #464). reqs = emptyRequests("test_content_sri_sameorigin3.html"); result = subresourceIntegrityTest(reqs); + assert.equal( + result.result, + Expectation.SriNotImplementedButExternalScriptsLoadedSecurely + ); + assert.isFalse(result.pass); + + // Without HTTP→HTTPS enforcement the protocol-relative URL is still penalised at -50. + reqs = emptyRequests("test_content_sri_sameorigin3.html"); + reqs.responses.httpRedirects = [ + { url: new URL("http://mozilla.org/"), status: 200 }, + ]; + result = subresourceIntegrityTest(reqs); assert.equal( result.result, Expectation.SriNotImplementedAndExternalScriptsNotLoadedSecurely @@ -115,8 +128,23 @@ describe("Subresource Integrity", () => { }); it("checks if implemented with external scripts and no protocol", function () { + // When HTTP redirects to HTTPS, //cdn.example.com/script.js always resolves to https://, + // so protocol-relative URLs should be treated the same as https:// (issue #464). reqs = emptyRequests("test_content_sri_impl_external_noproto.html"); let result = subresourceIntegrityTest(reqs); + assert.equal( + result.result, + Expectation.SriImplementedAndExternalScriptsLoadedSecurely + ); + assert.isTrue(result.pass); + + // When HTTP does NOT redirect to HTTPS, //cdn.example.com/script.js can resolve to http:// + // on an HTTP visit, so it must still be penalised. + reqs = emptyRequests("test_content_sri_impl_external_noproto.html"); + reqs.responses.httpRedirects = [ + { url: new URL("http://mozilla.org/"), status: 200 }, + ]; + result = subresourceIntegrityTest(reqs); assert.equal( result.result, Expectation.SriImplementedButExternalScriptsNotLoadedSecurely @@ -135,8 +163,23 @@ describe("Subresource Integrity", () => { }); it("checks if not implemented with external scripts and no protocol", function () { + // When HTTP redirects to HTTPS, //cdn.example.com/script.js always resolves to https://, + // so it should score like https:// (-5), not like http:// (-50) (issue #464). reqs = emptyRequests("test_content_sri_notimpl_external_noproto.html"); let result = subresourceIntegrityTest(reqs); + assert.equal( + result.result, + Expectation.SriNotImplementedButExternalScriptsLoadedSecurely + ); + assert.isFalse(result.pass); + + // When HTTP does NOT redirect to HTTPS, //cdn.example.com/script.js can resolve to http:// + // on an HTTP visit, so it must still be penalised at -50. + reqs = emptyRequests("test_content_sri_notimpl_external_noproto.html"); + reqs.responses.httpRedirects = [ + { url: new URL("http://mozilla.org/"), status: 200 }, + ]; + result = subresourceIntegrityTest(reqs); assert.equal( result.result, Expectation.SriNotImplementedAndExternalScriptsNotLoadedSecurely