`document.open` in a sandbox realm triggers `SecurityError: The operation is insecure.`
Categories
(Remote Protocol :: WebDriver BiDi, defect, P2)
Tracking
(firefox133 fixed)
Tracking | Status | |
---|---|---|
firefox133 | --- | fixed |
People
(Reporter: yurys, Assigned: whimboo)
References
(Blocks 1 open bug)
Details
(Whiteboard: [webdriver:m13], [wptsync upstream])
Attachments
(2 files)
Steps to reproduce:
Run Playwright tests that use page.setContent
with Bidi Firefox as described on this page. E.g. use the following command to see the protocol log:
DEBUG=pw:protocol MAX_LOG_LENGTH=1000 npm run biditest -- --project='bidi-firefox-beta*' page-set-content:22
Actual results:
The test is failing when evaluating document.open();
in sandbox realm:
{"type":"success","id":9,"result":{"realm":"8699481d-e954-4590-bf31-1705c9fc8a57","type":"exception","exceptionDetails":{"columnNumber":19,"exception":{"type":"error"},"lineNumber":4,"stackTrace":{"callFrames":[{"columnNumber":19,"functionName":"","lineNumber":4,"url":"about:blank line 234 > eval"},{"columnNumber":16,"functionName":"evaluate","lineNumber":235,"url":"about:blank"},{"columnNumber":43,"functionName":"","lineNumber":0,"url":"about:blank"},{"columnNumber":62,"functionName":"","lineNumber":0,"url":"about:blank"}]},"text":"SecurityError: The operation is insecure."}}}
Assignee | ||
Comment 1•2 months ago
|
||
Yury, do you maybe have some example code and a log similar to bug 1918287? That would be pretty helpful. Thanks
Assignee | ||
Comment 2•2 months ago
|
||
I actually can replicate via a call like the following by triggering it from any loaded page:
await bidi_session.script.evaluate(
expression="""document.open(); document.write("<h1>Replaced</h1>"); document.close();""",
target=ContextTarget(top_context["context"], "sandbox"),
await_promise=False,
)
This most likely comes from:
https://searchfox.org/mozilla-central/rev/4a8bd8efdfaa43dd14a16d3cb15bf86796fd1def/dom/base/Document.cpp#9255-9259
Olli, I assume we do not allow the usage of document.open()
from within Sandboxes due to a specific reason?
Reporter | ||
Comment 3•2 months ago
|
||
(In reply to Henrik Skupin [:whimboo][⌚️UTC+2] from comment #1)
Yury, do you maybe have some example code and a log similar to bug 1918287? That would be pretty helpful. Thanks
Absolutely, here is the log:
pw:protocol SEND ► {"id":1,"method":"session.new","params":{"capabilities":{"alwaysMatch":{"acceptInsecureCerts":false,"unhandledPromptBehavior":{"default":"ignore"},"webSocketUrl":true}}}} +0ms
pw:protocol ◀ RECV {"type":"success","id":1,"result":{"sessionId":"ae29e95c-88a4-47f1-b1ac-5f32ac0058e9","capabilities":{"acceptInsecureCerts":false,"browserName":"firefox","browserVersion":"131.0","platformName":"mac","unhandledPromptBehavior":{"default":"ignore"},"userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:131.0) Gecko/20100101 Firefox/131.0","moz:buildID":"20240909091655","moz:headless":true,"moz:platformVersion":"23.6.0","moz:processID":99987,"moz:profile":"/var/folders/t3/_mbh6z155sz1cr7prkpn9lsh0000gn/T/playwright_bididev_profile-qDcwdS","moz:shutdownTimeout":60000,"proxy":{}}}} +767ms
pw:protocol SEND ► {"id":2,"method":"session.subscribe","params":{"events":["browsingContext","network","log","script"]}} +0ms
pw:protocol ◀ RECV {"type":"event","method":"script.realmCreated","params":{"realm":"5deae8cf-a743-40c5-a54d-ffab97f24a30","origin":"null","context":"442197e4-d926-4c70-a5f3-ebdd0d7280bb","type":"window"}} +15ms
pw:protocol ◀ RECV {"type":"success","id":2,"result":{}} +2ms
pw:protocol SEND ► {"id":3,"method":"browser.createUserContext","params":{}} +4ms
pw:protocol ◀ RECV {"type":"success","id":3,"result":{"userContext":"3ac63573-c5a2-4ae0-9306-cef50ad094be"}} +1ms
pw:protocol SEND ► {"id":4,"method":"browsingContext.create","params":{"type":"window","userContext":"3ac63573-c5a2-4ae0-9306-cef50ad094be"}} +23ms
pw:protocol ◀ RECV {"type":"event","method":"browsingContext.contextCreated","params":{"children":null,"context":"38b06626-c4f6-41c9-81f5-96b6672016e9","originalOpener":null,"url":"about:blank","userContext":"3ac63573-c5a2-4ae0-9306-cef50ad094be","parent":null}} +47ms
pw:protocol SEND ► {"id":5,"method":"browsingContext.setViewport","params":{"context":"38b06626-c4f6-41c9-81f5-96b6672016e9","viewport":{"width":1280,"height":720},"devicePixelRatio":1}} +1ms
pw:protocol ◀ RECV {"type":"event","method":"script.realmCreated","params":{"realm":"d6625703-5f81-4149-8246-feb4be640493","origin":"null","context":"38b06626-c4f6-41c9-81f5-96b6672016e9","type":"window"}} +132ms
pw:protocol SEND ► {"id":6,"method":"script.evaluate","params":{"expression":"1 + 1","target":{"context":"38b06626-c4f6-41c9-81f5-96b6672016e9","sandbox":"__playwright_utility_world__"},"serializationOptions":{"maxObjectDepth":10,"maxDomDepth":10},"awaitPromise":true,"userActivation":true}} +0ms
pw:protocol ◀ RECV {"type":"event","method":"browsingContext.navigationStarted","params":{"context":"38b06626-c4f6-41c9-81f5-96b6672016e9","navigation":"24c941c6-72fa-4864-8e52-5b1b40c85146","timestamp":1726160670676,"url":"about:blank"}} +1ms
pw:protocol ◀ RECV {"type":"success","id":5,"result":{}} +0ms
pw:protocol ◀ RECV {"type":"event","method":"script.realmCreated","params":{"realm":"89fd9e29-73b6-4679-885d-01ce22d2e9a7","origin":"null","context":"38b06626-c4f6-41c9-81f5-96b6672016e9","type":"window","sandbox":"__playwright_utility_world__"}} +2ms
pw:protocol ◀ RECV {"type":"success","id":6,"result":{"realm":"89fd9e29-73b6-4679-885d-01ce22d2e9a7","type":"success","result":{"type":"number","value":2}}} +1ms
pw:protocol ◀ RECV {"type":"event","method":"script.realmDestroyed","params":{"realm":"d6625703-5f81-4149-8246-feb4be640493"}} +1ms
pw:protocol ◀ RECV {"type":"event","method":"script.realmDestroyed","params":{"realm":"89fd9e29-73b6-4679-885d-01ce22d2e9a7"}} +0ms
pw:protocol ◀ RECV {"type":"event","method":"script.realmCreated","params":{"realm":"137fa5b0-ff83-48f8-a079-984db6b0c7cd","origin":"null","context":"38b06626-c4f6-41c9-81f5-96b6672016e9","type":"window"}} +0ms
pw:protocol SEND ► {"id":7,"method":"script.evaluate","params":{"expression":"1 + 1","target":{"context":"38b06626-c4f6-41c9-81f5-96b6672016e9","sandbox":"__playwright_utility_world__"},"serializationOptions":{"maxObjectDepth":10,"maxDomDepth":10},"awaitPromise":true,"userActivation":true}} +0ms
pw:protocol ◀ RECV {"type":"event","method":"script.realmCreated","params":{"realm":"6a5e688a-426a-430e-96bc-2b4c988e65ae","origin":"null","context":"38b06626-c4f6-41c9-81f5-96b6672016e9","type":"window","sandbox":"__playwright_utility_world__"}} +6ms
pw:protocol ◀ RECV {"type":"success","id":7,"result":{"realm":"6a5e688a-426a-430e-96bc-2b4c988e65ae","type":"success","result":{"type":"number","value":2}}} +0ms
pw:protocol ◀ RECV {"type":"event","method":"browsingContext.domContentLoaded","params":{"context":"38b06626-c4f6-41c9-81f5-96b6672016e9","timestamp":1726160670686,"url":"about:blank","navigation":"24c941c6-72fa-4864-8e52-5b1b40c85146"}} +1ms
pw:protocol ◀ RECV {"type":"event","method":"browsingContext.load","params":{"context":"38b06626-c4f6-41c9-81f5-96b6672016e9","timestamp":1726160670686,"url":"about:blank","navigation":"24c941c6-72fa-4864-8e52-5b1b40c85146"}} +0ms
pw:protocol ◀ RECV {"type":"success","id":4,"result":{"context":"38b06626-c4f6-41c9-81f5-96b6672016e9"}} +1ms
pw:protocol SEND ► {"id":8,"method":"script.evaluate","params":{"expression":"\n (() => {\n const module = {};\n \nvar __commonJS = obj => {\n let required = false;\n let result;\n return function __require() {\n if (!required) {\n required = true;\n let fn;\n for (const name in obj) { fn = obj[name]; break; }\n const module = { exports: {} };\n fn(module.exports, module);\n result = module.exports;\n }\n return result;\n }\n};\nvar __export = (target, <<<<<( LOG TRUNCATED )>>>>> (_a = window.__pwClock) == null ? void 0 : _a.builtin.Date) || Date;\n window.builtinPerformance = ((_b = window.__pwClock) == null ? void 0 : _b.builtin.performance) || performance;\n }\n};\n\n return new (module.exports.UtilityScript())(true);\n })();","target":{"context":"38b06626-c4f6-41c9-81f5-96b6672016e9","sandbox":"__playwright_utility_world__"},"resultOwnership":"root","serializationOptions":{"maxObjectDepth":0,"maxDomDepth":0},"awaitPromise":true,"userActivation":true}} +57ms
pw:protocol ◀ RECV {"type":"success","id":8,"result":{"realm":"6a5e688a-426a-430e-96bc-2b4c988e65ae","type":"success","result":{"type":"object","handle":"af692862-1978-4cd1-b806-7970f5608ccf"}}} +7ms
pw:protocol SEND ► {"id":9,"method":"script.callFunction","params":{"functionDeclaration":"(utilityScript, ...args) => utilityScript.evaluate(...args)","target":{"context":"38b06626-c4f6-41c9-81f5-96b6672016e9","sandbox":"__playwright_utility_world__"},"arguments":[{"handle":"af692862-1978-4cd1-b806-7970f5608ccf"},{"type":"boolean","value":true},{"type":"boolean","value":true},{"type":"string","value":"({\n html,\n tag\n }) => {\n document.open();\n console.debug(tag); // <<<<<( LOG TRUNCATED )>>>>> "type":"string","value":"k"},{"type":"string","value":"html"}],[{"type":"string","value":"v"},{"type":"string","value":"<div>hello</div>"}]]},{"type":"object","value":[[{"type":"string","value":"k"},{"type":"string","value":"tag"}],[{"type":"string","value":"v"},{"type":"string","value":"--playwright--set--content--38b06626-c4f6-41c9-81f5-96b6672016e9--1--"}]]}]}],[{"type":"string","value":"id"},{"type":"number","value":1}]]}],"serializationOptions":{},"awaitPromise":true,"userActivation":true}} +1ms
pw:protocol ◀ RECV {"type":"success","id":9,"result":{"realm":"6a5e688a-426a-430e-96bc-2b4c988e65ae","type":"exception","exceptionDetails":{"columnNumber":19,"exception":{"type":"error"},"lineNumber":4,"stackTrace":{"callFrames":[{"columnNumber":19,"functionName":"","lineNumber":4,"url":"about:blank line 234 > eval"},{"columnNumber":16,"functionName":"evaluate","lineNumber":235,"url":"about:blank"},{"columnNumber":43,"functionName":"","lineNumber":0,"url":"about:blank"},{"columnNumber":62,"functionName":"","lineNumber":0,"url":"about:blank"}]},"text":"SecurityError: The operation is insecure."}}} +2ms
pw:protocol SEND ► {"id":10,"method":"browser.removeUserContext","params":{"userContext":"3ac63573-c5a2-4ae0-9306-cef50ad094be"}} +5ms
pw:protocol ◀ RECV {"type":"success","id":10,"result":{}} +5ms
pw:protocol ◀ RECV {"type":"event","method":"browsingContext.contextDestroyed","params":{"children":null,"context":"38b06626-c4f6-41c9-81f5-96b6672016e9","originalOpener":null,"url":"about:blank","userContext":null,"parent":null}} +5ms
pw:protocol ◀ RECV {"type":"success","id":0,"result":{}} +4ms
It could be obtained by running the steps from my original report above.
Assignee | ||
Comment 4•2 months ago
|
||
(In reply to Henrik Skupin [:whimboo][⌚️UTC+2] from comment #2)
This most likely comes from:
https://searchfox.org/mozilla-central/rev/4a8bd8efdfaa43dd14a16d3cb15bf86796fd1def/dom/base/Document.cpp#9255-9259
That was the wrong location. It should actually come from one of these two checks:
https://searchfox.org/mozilla-central/rev/26a98a7ba56f315df146512c43449412f0592942/dom/base/Document.cpp#9955-9974
So whether it means we do not have an entry document or the origin is not same-origin.
Assignee | ||
Comment 5•2 months ago
|
||
So it's the check for the entryDocument
. The call to GetEntryDocument
returns null
because the call to do_QueryInterface(global)
doesn't result in a valid entryWin
even through the global is a valid pointer.
Assignee | ||
Comment 6•2 months ago
|
||
As per HTML specification I cannot see that this step could fail:
https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#document-open-steps
Comment 7•2 months ago
•
|
||
Per spec this should fail.
"If document's origin is not same origin to entryDocument's origin, then throw a "SecurityError" DOMException."
If entryDocument is null (which it can't be in HTML context), that part of the algorithm fails.
So we need to do something against the HTML spec, something which doesn't break in normal web context.
What sort of caller do we have there? Can we detect this non-web caller reliably and get principal from it?
There is GetEntryGlobal() which returns nsIGlobalObject, and that has PrincipalOrNull().
But that works only if the PrincipalOrNull() returns some reasonable principal.
Assignee | ||
Comment 8•2 months ago
|
||
(In reply to Olli Pettay [:smaug][bugs@pettay.fi] from comment #7)
Per spec this should fail.
"If document's origin is not same origin to entryDocument's origin, then throw a "SecurityError" DOMException."
If entryDocument is null (which it can't be in HTML context), that part of the algorithm fails.
That is step 4, but I was talking about step 3. As I read it there should always be an entryDocument
available given that there is no null
check:
Let entryDocument be the entry global object's associated Document.
So we need to do something against the HTML spec, something which doesn't break in normal web context.
What sort of caller do we have there? Can we detect this non-web caller reliably and get principal from it?
There is GetEntryGlobal() which returns nsIGlobalObject, and that has PrincipalOrNull().
But that works only if the PrincipalOrNull() returns some reasonable principal.
We call executeInGlobal()
on the makeGlobalObjectReference wrapped global of the sandbox:
That code runs as privileged code in the content process.
Comment 9•2 months ago
|
||
(In reply to Henrik Skupin [:whimboo][⌚️UTC+2] from comment #8)
That is step 4, but I was talking about step 3. As I read it there should always be an
entryDocument
available given that there is nonull
check:
Yes, exactly. Whoever is calling document.write without entryDocument is doing something non-standard.
We call
executeInGlobal()
on the makeGlobalObjectReference wrapped global of the sandbox:That code runs as privileged code in the content process.
What does that mean? What sort of principal do we have? ExpandedPrincipal with some content principal in it or system principal or what?
(It is a bit surprising that document.open/write is being used given the rather strong hint in the spec that it shouldn't be
https://html.spec.whatwg.org/#document.write() )
Reporter | ||
Comment 10•2 months ago
|
||
(In reply to Olli Pettay [:smaug][bugs@pettay.fi] from comment #9)
(It is a bit surprising that document.open/write is being used given the rather strong hint in the spec that it shouldn't be
https://html.spec.whatwg.org/#document.write() )
Ideally, there would be a command for setting page content in the Bidi spec, but it looks like the current recommendation is to use document.write, see this comment.
Assignee | ||
Updated•2 months ago
|
Assignee | ||
Comment 11•2 months ago
•
|
||
(In reply to Olli Pettay [:smaug][bugs@pettay.fi] from comment #9)
(In reply to Henrik Skupin [:whimboo][⌚️UTC+2] from comment #8)
That is step 4, but I was talking about step 3. As I read it there should always be an
entryDocument
available given that there is nonull
check:Yes, exactly. Whoever is calling document.write without entryDocument is doing something non-standard.
But the failure does not happen when calling document.write()
. It's happening when calling document.open()
.
We call
executeInGlobal()
on the makeGlobalObjectReference wrapped global of the sandbox:That code runs as privileged code in the content process.
What does that mean? What sort of principal do we have? ExpandedPrincipal with some content principal in it or system principal or what?
I had a quick discussion with Olli on Matrix yesterday and realized that when we create the sandbox, we're passing the window global to the Sandbox constructor but not a principal:
return new Cu.Sandbox(win, opts);
This suggests that the inherited principal might be the system principal, which could be causing the failure. I tried using win.document.nodePrincipal
instead, but it didn't make any difference—it's still failing in Document::Open
.
I also explored an approach similar to what WebExtensions are doing, where they check for a system principal and create a null principal. However, that also failed.
It seems we might indeed need a content principal in this case, so I attempted to use the following code:
const contentPrincipal = Services.scriptSecurityManager.createContentPrincipal(
Services.io.newURI(win.location.href),
win.document.nodePrincipal.originAttributes
);
Unfortunately, this also triggered the same failure.
Here some steps to easily reproduce:
- Launch Firefox with
mach run --setpref="remote.log.level=Trace" --remote-debugging-port --remote-allow-origins=https://juliandescottes.github.io about:blank
- Go to your normal Firefox process and open https://juliandescottes.github.io/bidi-web-client/web/ in a new tab
- Click the connect button
- Click the
send
button (the very first one) for thesession.new
command. - Open the DevTools console, and run the following code:
async function evaluate(context, expression, options = {}) {
const { awaitPromise = true, sandbox = null } = options;
const res = await sendCommand("script.evaluate", {
expression,
target: { context, sandbox },
awaitPromise,
});
return res.result.result;
}
(async function () {
await sendCommand("session.new", { capabilities: {} });
let tab1 = await sendCommand("browsingContext.create", { type: "tab" });
let context = tab1.result.context;
await sendCommand("browsingContext.navigate", {
context,
url: "https://example.org",
wait: "complete",
});
await evaluate(
context,
"document.open(); document.write('test'); document.close();",
{ sandbox: "test" },
);
})()
Comment 12•2 months ago
|
||
Yes, just changing the principal doesn't help, since one needs to have some entry document around. And JS sandboxes don't have.
Assignee | ||
Comment 13•2 months ago
|
||
(In reply to Yury Semikhatsky from comment #10)
Ideally, there would be a command for setting page content in the Bidi spec, but it looks like the current recommendation is to use document.write, see this comment.
Yury, is there a specific reason why this script evaluation is done by using a sandbox? In Puppeteer the setContent
command runs without a sandbox and that works fine.
Comment 14•2 months ago
|
||
Assignee | ||
Comment 15•2 months ago
|
||
Updated•2 months ago
|
Reporter | ||
Comment 16•2 months ago
|
||
(In reply to Henrik Skupin [:whimboo][⌚️UTC+2] from comment #13)
Yury, is there a specific reason why this script evaluation is done by using a sandbox? In Puppeteer the
setContent
command runs without a sandbox and that works fine.
We cannot trust the APIs to be pristine in the page world. Consider user's code doing 'document.open = undefined;' in the page, we still want our page.setContent() to work in that case.
Updated•2 months ago
|
Assignee | ||
Updated•2 months ago
|
Updated•1 month ago
|
Updated•1 month ago
|
Updated•1 month ago
|
Comment 17•1 month ago
|
||
Comment 19•1 month ago
|
||
bugherder |
https://hg.mozilla.org/mozilla-central/rev/643406ca1194
https://hg.mozilla.org/mozilla-central/rev/ae9c76aad44a
Description
•