mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-17 15:25:52 +00:00
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:
parent
f912325645
commit
6b49d6c6db
@ -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;
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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)) {
|
||||
|
@ -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);
|
||||
});
|
@ -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]
|
||||
|
Loading…
Reference in New Issue
Block a user