Skip to content

Commit 08e7de1

Browse files
committed
Bug 2017797 - Part 3: Preserve legacy structuredClone behaviour for WebExtensions, r=robwu,asuth
This change attempts to preserve the behaviour from before bug 2013389 for WebExtensions, putting the janky type-dependant behaviour behind a pref. This is done by making the webextension content script globals shadow 'structuredClone' with an alternative implementation based in the content script's global. This alternative implementation will, when being called from a webextension content script, only change the global of the cloned object when called on one of the set of DOM objects which previously were impacted by the bug. Note that previously 'structuredClone' would do this odd behaviour even for a nested object, but this is very unlikely to happen intentionally (and would be much more difficult to implement), so that behaviour has not been preserved. Differential Revision: https://phabricator.services.mozilla.com/D284524
1 parent 4a57bda commit 08e7de1

File tree

6 files changed

+215
-5
lines changed

6 files changed

+215
-5
lines changed

js/xpconnect/src/Sandbox.cpp

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,29 @@ static bool SandboxCreateStorage(JSContext* cx, JS::HandleObject obj) {
379379
return JS_DefineProperty(cx, obj, "storage", wrapped, JSPROP_ENUMERATE);
380380
}
381381

382+
// Prior to bug 2013389, the following DOM objects would be structured-cloned
383+
// into the inner window's realm when `structuredClone` is called from an
384+
// extension content script. All other objects would remain within the content
385+
// script's realm. This method is used to retain this historic behaviour.
386+
//
387+
// See bug 2017797 for discussion about this behaviour.
388+
static bool LegacyShouldCloneIntoWindow(JSObject* obj) {
389+
return IS_INSTANCE_OF(Blob, obj) || IS_INSTANCE_OF(Directory, obj) ||
390+
IS_INSTANCE_OF(FileList, obj) || IS_INSTANCE_OF(FormData, obj) ||
391+
IS_INSTANCE_OF(ImageBitmap, obj) || IS_INSTANCE_OF(VideoFrame, obj) ||
392+
IS_INSTANCE_OF(EncodedVideoChunk, obj) ||
393+
IS_INSTANCE_OF(AudioData, obj) ||
394+
IS_INSTANCE_OF(EncodedAudioChunk, obj) ||
395+
IS_INSTANCE_OF(RTCEncodedVideoFrame, obj) ||
396+
IS_INSTANCE_OF(RTCEncodedAudioFrame, obj) ||
397+
IS_INSTANCE_OF(MessagePort, obj) ||
398+
IS_INSTANCE_OF(OffscreenCanvas, obj) ||
399+
IS_INSTANCE_OF(ReadableStream, obj) ||
400+
IS_INSTANCE_OF(WritableStream, obj) ||
401+
IS_INSTANCE_OF(TransformStream, obj) ||
402+
IS_INSTANCE_OF(RTCDataChannel, obj);
403+
}
404+
382405
static bool SandboxStructuredClone(JSContext* cx, unsigned argc, Value* vp) {
383406
CallArgs args = CallArgsFromVp(argc, vp);
384407

@@ -387,25 +410,43 @@ static bool SandboxStructuredClone(JSContext* cx, unsigned argc, Value* vp) {
387410
}
388411

389412
RootedDictionary<dom::StructuredSerializeOptions> options(cx);
390-
BindingCallContext callCx(cx, "structuredClone");
391413
if (!options.Init(cx, args.hasDefined(1) ? args[1] : JS::NullHandleValue,
392414
"Argument 2", false)) {
393415
return false;
394416
}
395417

396-
nsIGlobalObject* global = CurrentNativeGlobal(cx);
418+
// NOTE: A spec-compliant structuredClone should determine & use the relevant
419+
// global instead of the current global.
420+
nsCOMPtr<nsIGlobalObject> global = CurrentNativeGlobal(cx);
397421
if (!global) {
398422
JS_ReportErrorASCII(cx, "structuredClone: Missing global");
399423
return false;
400424
}
401425

426+
// If this is a content script, we may want to clone into that window instead.
427+
// See the comment on LegacyShouldCloneIntoWindow for details.
428+
if (IsWebExtensionContentScriptSandbox(global->GetGlobalJSObject()) &&
429+
StaticPrefs::extensions_webextensions_legacyStructuredCloneBehavior() &&
430+
args[0].isObject() && LegacyShouldCloneIntoWindow(&args[0].toObject())) {
431+
if (nsGlobalWindowInner* window =
432+
SandboxWindowOrNull(global->GetGlobalJSObject(), cx)) {
433+
global = window;
434+
}
435+
}
436+
402437
JS::Rooted<JS::Value> result(cx);
403438
ErrorResult rv;
404439
nsContentUtils::StructuredClone(cx, global, args[0], options, &result, rv);
405440
if (rv.MaybeSetPendingException(cx)) {
406441
return false;
407442
}
408443

444+
// Because we specified a custom `global`, the returned value may not be in
445+
// our realm.
446+
if (!mozilla::dom::MaybeWrapValue(cx, &result)) {
447+
return false;
448+
}
449+
409450
MOZ_ASSERT_IF(result.isGCThing(),
410451
!JS::GCThingIsMarkedGray(result.toGCCellPtr()));
411452
args.rval().set(result);

modules/libpref/init/StaticPrefList.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6210,6 +6210,16 @@
62106210
#endif
62116211
mirror: always
62126212

6213+
# Prior to bug 2013389 and bug 2017797, calling structuredClone within a
6214+
# content script would clone most objects within the content script's realm,
6215+
# however a subset of DOM objects would be cloned into the underlying content
6216+
# window's realm. When enabled, this pref preserves this legacy behaviour for
6217+
# compatibility with existing content scripts.
6218+
- name: extensions.webextensions.legacyStructuredCloneBehavior
6219+
type: bool
6220+
value: true
6221+
mirror: always
6222+
62136223
# This pref governs whether we run webextensions in a separate process (true)
62146224
# or the parent/main process (false)
62156225
- name: extensions.webextensions.remote

toolkit/components/extensions/ExtensionContent.sys.mjs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,16 +1039,20 @@ export class ContentScriptContextChild extends BaseContext {
10391039
addonId: extensionPrincipal.addonId,
10401040
};
10411041

1042+
// Within a content script, we want the 'structuredClone' method to clone
1043+
// the object within the content script's global, rather than cloning it
1044+
// into the the embedding scope. (see bug 2017797)
1045+
let wantGlobalProperties = ["structuredClone"];
1046+
10421047
let isMV2 = extension.manifestVersion == 2;
1043-
let wantGlobalProperties;
10441048
let sandboxContentSecurityPolicy;
10451049
if (isMV2) {
10461050
// In MV2, fetch/XHR support cross-origin requests.
10471051
// WebSocket was also included to avoid CSP effects (bug 1676024).
1048-
wantGlobalProperties = ["XMLHttpRequest", "fetch", "WebSocket"];
1052+
wantGlobalProperties.push("XMLHttpRequest", "fetch", "WebSocket");
10491053
} else {
10501054
// In MV3, fetch/XHR have the same capabilities as the web page.
1051-
wantGlobalProperties = [];
1055+
10521056
// In MV3, the base CSP is enforced for content scripts. Overrides are
10531057
// currently not supported, but this was considered at some point, see
10541058
// https://bugzilla.mozilla.org/show_bug.cgi?id=1581611#c10

toolkit/components/extensions/ExtensionUserScriptsContent.sys.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,10 @@ class WorldCollection {
258258
wantXrays: true,
259259
isWebExtensionContentScript: true,
260260
wantExportHelpers: true,
261+
// The 'structuredClone' method in content scripts should clone within the
262+
// content script global by default, rather than cloning into the target
263+
// contentWindow.
264+
wantGlobalProperties: ["structuredClone"],
261265
originAttributes: docPrincipal.originAttributes,
262266
});
263267

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"use strict";
2+
3+
const server = createHttpServer({ hosts: ["example.com"] });
4+
server.registerPathHandler("/test", (request, response) => {
5+
response.setStatusLine(request.httpVersion, 200, "OK");
6+
response.setHeader("Content-Type", "text/html", false);
7+
response.write("<!DOCTYPE html><html><body></body></html>");
8+
});
9+
10+
async function testStructuredClone(legacyPref) {
11+
Services.prefs.setBoolPref(
12+
"extensions.webextensions.legacyStructuredCloneBehavior",
13+
legacyPref
14+
);
15+
16+
let extension = ExtensionTestUtils.loadExtension({
17+
manifest: {
18+
content_scripts: [
19+
{
20+
matches: ["http://example.com/test"],
21+
js: ["cs.js"],
22+
},
23+
],
24+
},
25+
files: {
26+
"cs.js"() {
27+
/* globals cloneInto, structuredClone */
28+
browser.test.assertTrue(
29+
location.hash == "#true" || location.hash == "#false",
30+
"unexpected legacy pref value"
31+
);
32+
33+
function isWindow(obj, desc) {
34+
browser.test.assertTrue(
35+
obj instanceof window.Object,
36+
`${desc}: is a window.Object`
37+
);
38+
browser.test.assertFalse(
39+
obj instanceof Object,
40+
`${desc}: is not a sandbox.Object`
41+
);
42+
}
43+
function isSandbox(obj, desc) {
44+
browser.test.assertTrue(
45+
obj instanceof Object,
46+
`${desc}: is a sandbox.Object`
47+
);
48+
browser.test.assertFalse(
49+
obj instanceof window.Object,
50+
`${desc}: is not a window.Object`
51+
);
52+
}
53+
54+
let sbObj = { a: 1 };
55+
let winObj = cloneInto(sbObj, window);
56+
let winBlob = new Blob(["test"]);
57+
let sbBlob = cloneInto(winBlob, globalThis);
58+
59+
// Confirm that our initial objects are in the expected globals.
60+
// NOTE: The original blob was constructed in the Window (as the 'Blob'
61+
// constructor is provided by Window).
62+
isWindow(winObj, "winObj");
63+
isSandbox(sbObj, "sbObj");
64+
isWindow(winBlob, "winBlob");
65+
isSandbox(sbBlob, "sbBlob");
66+
67+
// structuredClone(obj) should always clone into the sandbox
68+
isSandbox(structuredClone(winObj), "structuredClone(winObj)");
69+
isSandbox(
70+
globalThis.structuredClone(winObj),
71+
"globalThis.structuredClone(winObj)"
72+
);
73+
isSandbox(structuredClone(sbObj), "structuredClone(sbObj)");
74+
isSandbox(
75+
globalThis.structuredClone(sbObj),
76+
"globalThis.structuredClone(sbObj)"
77+
);
78+
79+
// structuredClone(blob) depends on the legacy pref
80+
let blobAssertion = location.hash == "#true" ? isWindow : isSandbox;
81+
blobAssertion(structuredClone(winBlob), "structuredClone(winBlob)");
82+
blobAssertion(
83+
globalThis.structuredClone(winBlob),
84+
"globalThis.structuredClone(winBlob)"
85+
);
86+
blobAssertion(structuredClone(sbBlob), "structuredClone(sbBlob)");
87+
blobAssertion(
88+
globalThis.structuredClone(sbBlob),
89+
"globalThis.structuredClone(sbBlob)"
90+
);
91+
92+
// window.structuredClone(...) always clones into the window global
93+
isWindow(
94+
window.structuredClone(winObj),
95+
"window.structuredClone(winObj)"
96+
);
97+
isWindow(
98+
window.structuredClone(sbObj),
99+
"window.structuredClone(sbObj)"
100+
);
101+
isWindow(
102+
window.structuredClone(winBlob),
103+
"window.structuredClone(winBlob)"
104+
);
105+
isWindow(
106+
window.structuredClone(sbBlob),
107+
"window.structuredClone(sbBlob)"
108+
);
109+
110+
// Also test the behaviour of returned xrays. In non-window cases,
111+
// there should be no xrays, but when calling window.structuredClone
112+
// there are xrays, which will prevent adding function properties.
113+
structuredClone(winObj).x = function () {};
114+
globalThis.structuredClone(winObj).x = function () {};
115+
browser.test.assertThrows(
116+
() => (window.structuredClone(winObj).x = function () {}),
117+
/Not allowed to define cross-origin object as property/,
118+
"window.structuredClone(winObj) should be an xray wrapper"
119+
);
120+
121+
structuredClone(sbObj).x = function () {};
122+
globalThis.structuredClone(sbObj).x = function () {};
123+
browser.test.assertThrows(
124+
() => (window.structuredClone(sbObj).x = function () {}),
125+
/Not allowed to define cross-origin object as property/,
126+
"window.structuredClone(sbObj) should be an xray wrapper"
127+
);
128+
129+
browser.test.sendMessage("done");
130+
},
131+
},
132+
});
133+
134+
await extension.startup();
135+
let contentPage = await ExtensionTestUtils.loadContentPage(
136+
`http://example.com/test#${legacyPref}`
137+
);
138+
await extension.awaitMessage("done");
139+
await contentPage.close();
140+
await extension.unload();
141+
}
142+
143+
add_task(async function test_structuredClone_legacy() {
144+
await testStructuredClone(true);
145+
});
146+
147+
add_task(async function test_structuredClone_nonlegacy() {
148+
await testStructuredClone(false);
149+
});

toolkit/components/extensions/test/xpcshell/xpcshell-common.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ skip-if = [
191191

192192
["test_ext_contentscript_slow_frame.js"]
193193

194+
["test_ext_contentscript_structured_clone.js"]
195+
194196
["test_ext_contentscript_teardown.js"]
195197
skip-if = [
196198
"tsan", # Bug 1683730

0 commit comments

Comments
 (0)