Bug 1676024: Websockets triggered by extensions should not be subject to a page's CSP. r=ckerschb,mixedpuppy

Needs ReviewPublic

Differential Revision: https://phabricator.services.mozilla.com/D104671
This commit is contained in:
Niklas Goegge 2021-07-06 08:05:49 +00:00
parent f912325645
commit 6b49d6c6db
7 changed files with 228 additions and 18 deletions

View File

@ -74,6 +74,7 @@
#include "mozilla/dom/URLBinding.h"
#include "mozilla/dom/URLSearchParamsBinding.h"
#include "mozilla/dom/XMLHttpRequest.h"
#include "mozilla/dom/WebSocketBinding.h"
#include "mozilla/dom/XMLSerializerBinding.h"
#include "mozilla/dom/FormDataBinding.h"
#include "mozilla/dom/nsCSPContext.h"
@ -898,6 +899,8 @@ bool xpc::GlobalProperties::Parse(JSContext* cx, JS::HandleObject obj) {
URLSearchParams = true;
} else if (JS_LinearStringEqualsLiteral(nameStr, "XMLHttpRequest")) {
XMLHttpRequest = true;
} else if (JS_LinearStringEqualsLiteral(nameStr, "WebSocket")) {
WebSocket = true;
} else if (JS_LinearStringEqualsLiteral(nameStr, "XMLSerializer")) {
XMLSerializer = true;
} else if (JS_LinearStringEqualsLiteral(nameStr, "atob")) {
@ -1041,6 +1044,9 @@ bool xpc::GlobalProperties::Define(JSContext* cx, JS::HandleObject obj) {
if (XMLHttpRequest && !dom::XMLHttpRequest_Binding::GetConstructorObject(cx))
return false;
if (WebSocket && !dom::WebSocket_Binding::GetConstructorObject(cx))
return false;
if (XMLSerializer && !dom::XMLSerializer_Binding::GetConstructorObject(cx))
return false;

View File

@ -2242,6 +2242,7 @@ struct GlobalProperties {
bool URL : 1;
bool URLSearchParams : 1;
bool XMLHttpRequest : 1;
bool WebSocket : 1;
bool XMLSerializer : 1;
// Ad-hoc property names we implement.

View File

@ -248,14 +248,22 @@ LoadInfo::LoadInfo(
(nsContentUtils::IsPreloadType(mInternalContentPolicyType) &&
aLoadingContext->OwnerDoc()->GetBlockAllMixedContent(true));
// if the document forces all requests to be upgraded from http to https,
// then we should do that for all requests. If it only forces preloads to be
// upgraded then we should enforce upgrade insecure requests only for
// preloads.
mUpgradeInsecureRequests =
aLoadingContext->OwnerDoc()->GetUpgradeInsecureRequests(false) ||
(nsContentUtils::IsPreloadType(mInternalContentPolicyType) &&
aLoadingContext->OwnerDoc()->GetUpgradeInsecureRequests(true));
if (mLoadingPrincipal && BasePrincipal::Cast(mTriggeringPrincipal)
->OverridesCSP(mLoadingPrincipal)) {
// if the load is triggered by an addon which potentially overrides the
// CSP of the document, then do not force insecure requests to be
// upgraded.
mUpgradeInsecureRequests = false;
} else {
// if the document forces all requests to be upgraded from http to https,
// then we should do that for all requests. If it only forces preloads to
// be upgraded then we should enforce upgrade insecure requests only for
// preloads.
mUpgradeInsecureRequests =
aLoadingContext->OwnerDoc()->GetUpgradeInsecureRequests(false) ||
(nsContentUtils::IsPreloadType(mInternalContentPolicyType) &&
aLoadingContext->OwnerDoc()->GetUpgradeInsecureRequests(true));
}
if (nsContentUtils::IsUpgradableDisplayType(externalType)) {
if (mLoadingPrincipal->SchemeIs("https")) {
@ -480,11 +488,19 @@ LoadInfo::LoadInfo(dom::WindowGlobalParent* aParentWGP,
// store that bit for all requests on the loadinfo.
mBlockAllMixedContent = aParentWGP->GetDocumentBlockAllMixedContent();
// if the document forces all requests to be upgraded from http to https,
// then we should do that for all requests. If it only forces preloads to be
// upgraded then we should enforce upgrade insecure requests only for
// preloads.
mUpgradeInsecureRequests = aParentWGP->GetDocumentUpgradeInsecureRequests();
if (mTopLevelPrincipal && BasePrincipal::Cast(mTriggeringPrincipal)
->OverridesCSP(mTopLevelPrincipal)) {
// if the load is triggered by an addon which potentially overrides the
// CSP of the document, then do not force insecure requests to be
// upgraded.
mUpgradeInsecureRequests = false;
} else {
// if the document forces all requests to be upgraded from http to https,
// then we should do that for all requests. If it only forces preloads to
// be upgraded then we should enforce upgrade insecure requests only for
// preloads.
mUpgradeInsecureRequests = aParentWGP->GetDocumentUpgradeInsecureRequests();
}
mOriginAttributes = mLoadingPrincipal->OriginAttributesRef();
// We need to do this after inheriting the document's origin attributes

View File

@ -739,7 +739,7 @@ class UserScript extends Script {
sandboxPrototype: contentWindow,
sameZoneAs: contentWindow,
wantXrays: true,
wantGlobalProperties: ["XMLHttpRequest", "fetch"],
wantGlobalProperties: ["XMLHttpRequest", "fetch", "WebSocket"],
originAttributes: contentPrincipal.originAttributes,
metadata: {
"inner-window-id": context.innerWindowID,
@ -831,7 +831,7 @@ class ContentScriptContextChild extends BaseContext {
wantXrays: true,
isWebExtensionContentScript: true,
wantExportHelpers: true,
wantGlobalProperties: ["XMLHttpRequest", "fetch"],
wantGlobalProperties: ["XMLHttpRequest", "fetch", "WebSocket"],
originAttributes: attrs,
});
@ -850,11 +850,13 @@ class ContentScriptContextChild extends BaseContext {
this.content = {
XMLHttpRequest: window.XMLHttpRequest,
fetch: window.fetch.bind(window),
WebSocket: window.WebSocket,
};
window.JSON = JSON;
window.XMLHttpRequest = XMLHttpRequest;
window.fetch = fetch;
window.WebSocket = WebSocket;
`,
this.sandbox
);

View File

@ -119,6 +119,32 @@ function testScriptTag(data) {
});
}
async function testHttpRequestUpgraded(data = {}) {
let f = data.content ? content.fetch : fetch;
return f(data.url)
.then(() => "http:")
.catch(() => "https:");
}
async function testWebSocketUpgraded(data = {}) {
let ws = data.content ? content.WebSocket : WebSocket;
new ws(data.url);
}
function webSocketUpgradeListenerBackground() {
// Catch websocket requests and send the protocol back to be asserted.
browser.webRequest.onBeforeRequest.addListener(
details => {
// Send the protocol back as test result.
// This will either be "wss:", "ws:"
browser.test.sendMessage("result", new URL(details.url).protocol);
return { cancel: true };
},
{ urls: ["wss://example.com/*", "ws://example.com/*"] },
["blocking"]
);
}
// If the violation source is the extension the securitypolicyviolation event is not fired.
// If the page is the source, the event is fired and both the content script or page scripts
// will receive the event. If we're expecting a moz-extension report we'll fail in the
@ -292,6 +318,76 @@ let TESTS = [
data: { url: `${BASE_URL}/data/file_script_good.js` },
expect: false,
},
{
description: "content.WebSocket in content script is affected by page csp.",
version: 2,
pageCSP: `upgrade-insecure-requests;`,
data: { content: true, url: "ws://example.com/ws_dummy" },
script: testWebSocketUpgraded,
expect: "wss:", // we expect the websocket to be upgraded.
backgroundScript: webSocketUpgradeListenerBackground,
},
{
description:
"content.WebSocket in content script is affected by page csp. v3",
version: 3,
pageCSP: `upgrade-insecure-requests;`,
data: { content: true, url: "ws://example.com/ws_dummy" },
script: testWebSocketUpgraded,
expect: "wss:", // we expect the websocket to be upgraded.
backgroundScript: webSocketUpgradeListenerBackground,
},
{
description: "WebSocket in content script is not affected by page csp.",
version: 2,
pageCSP: `upgrade-insecure-requests;`,
data: { url: "ws://example.com/ws_dummy" },
script: testWebSocketUpgraded,
expect: "ws:", // we expect the websocket to not be upgraded.
backgroundScript: webSocketUpgradeListenerBackground,
},
{
description: "WebSocket in content script is not affected by page csp. v3",
version: 3,
pageCSP: `upgrade-insecure-requests;`,
data: { url: "ws://example.com/ws_dummy" },
script: testWebSocketUpgraded,
expect: "ws:", // we expect the websocket to not be upgraded.
backgroundScript: webSocketUpgradeListenerBackground,
},
{
description: "Http request in content script is not affected by page csp.",
version: 2,
pageCSP: `upgrade-insecure-requests;`,
data: { url: "http://example.com/plain.html" },
script: testHttpRequestUpgraded,
expect: "http:", // we expect the request to not be upgraded.
},
{
description:
"Http request in content script is not affected by page csp. v3",
version: 3,
pageCSP: `upgrade-insecure-requests;`,
data: { url: "http://example.com/plain.html" },
script: testHttpRequestUpgraded,
expect: "http:", // we expect the request to not be upgraded.
},
{
description: "content.fetch in content script is affected by page csp.",
version: 2,
pageCSP: `upgrade-insecure-requests;`,
data: { content: true, url: "http://example.com/plain.html" },
script: testHttpRequestUpgraded,
expect: "https:", // we expect the request to be upgraded.
},
{
description: "content.fetch in content script is affected by page csp. v3",
version: 3,
pageCSP: `upgrade-insecure-requests;`,
data: { content: true, url: "http://example.com/plain.html" },
script: testHttpRequestUpgraded,
expect: "https:", // we expect the request to be upgraded.
},
];
async function runCSPTest(test) {
@ -307,18 +403,22 @@ async function runCSPTest(test) {
js: ["content_script.js"],
},
],
permissions: ["<all_urls>"],
permissions: ["webRequest", "webRequestBlocking", "<all_urls>"],
background: { scripts: ["background.js"] },
},
files: {
"content_script.js": `
(${contentScript})(${JSON.stringify(test.report)}).then(() => {
browser.test.sendMessage("violationEvent");
});
(${test.script})(${JSON.stringify(test.data)}).then(result => {
browser.test.sendMessage("result", result);
if(result !== undefined) {
browser.test.sendMessage("result", result);
}
});
`,
"background.js": `(${test.backgroundScript || (() => {})})()`,
...test.files,
},
};
@ -330,8 +430,10 @@ async function runCSPTest(test) {
info(`running: ${test.description}`);
await extension.awaitMessage("violationEvent");
let result = await extension.awaitMessage("result");
equal(result, test.expect, test.description);
if (test.report) {
let report = await reportPromise;
for (let key of Object.keys(test.report)) {

View File

@ -0,0 +1,82 @@
"use strict";
const HOSTS = new Set(["example.com"]);
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
const server = createHttpServer({ hosts: HOSTS });
const BASE_URL = `http://example.com`;
const pageURL = `${BASE_URL}/plain.html`;
server.registerPathHandler("/plain.html", (request, response) => {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "text/html");
response.setHeader("Content-Security-Policy", "upgrade-insecure-requests;");
response.write("<!DOCTYPE html><html></html>");
});
async function testWebSocketInFrameUpgraded(data = {}) {
const frame = document.createElement("iframe");
frame.src = browser.runtime.getURL("frame.html");
document.documentElement.appendChild(frame);
}
async function test_webSocket(version) {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
manifest_version: version,
permissions: ["webRequest", "webRequestBlocking", "<all_urls>"],
content_scripts: [
{
matches: ["http://*/plain.html"],
run_at: "document_idle",
js: ["content_script.js"],
},
],
},
background() {
browser.webRequest.onBeforeRequest.addListener(
details => {
// the websockets should not be upgraded
browser.test.assertEq(
"ws:",
new URL(details.url).protocol,
"ws protocol worked"
);
browser.test.notifyPass("websocket");
},
{ urls: ["wss://example.com/*", "ws://example.com/*"] },
["blocking"]
);
},
files: {
"frame.html": `
<html>
<head>
<meta charset="utf-8"/>
<script type="application/javascript" src="frame_script.js"></script>
</head>
<body>
</body>
</html>
`,
"frame_script.js": `new WebSocket("ws://example.com/ws_dummy");`,
"content_script.js": `
(${testWebSocketInFrameUpgraded})()
`,
},
});
await extension.startup();
let contentPage = await ExtensionTestUtils.loadContentPage(pageURL);
await extension.awaitFinish("websocket");
await contentPage.close();
await extension.unload();
}
add_task(async function test_webSocket_upgrade_iframe() {
await test_webSocket(2);
await test_webSocket(3);
});

View File

@ -253,6 +253,7 @@ skip-if = os == "android" # Android: Bug 1680132
[test_ext_webRequest_suspend.js]
[test_ext_webRequest_userContextId.js]
[test_ext_webRequest_viewsource.js]
[test_ext_webSocket.js]
[test_ext_webRequest_webSocket.js]
skip-if = appname == "thunderbird"
[test_ext_xhr_capabilities.js]