Closed Bug 1918288 Opened 2 months ago Closed 1 month ago

`document.open` in a sandbox realm triggers `SecurityError: The operation is insecure.`

Categories

(Remote Protocol :: WebDriver BiDi, defect, P2)

Firefox 131
defect
Points:
2

Tracking

(firefox133 fixed)

RESOLVED FIXED
133 Branch
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."}}}

Yury, do you maybe have some example code and a log similar to bug 1918287? That would be pretty helpful. Thanks

Flags: needinfo?(yurys)

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?

Status: UNCONFIRMED → NEW
Ever confirmed: true
Flags: needinfo?(yurys) → needinfo?(smaug)

(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.

(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.

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.

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

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.

Flags: needinfo?(smaug)

(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:

https://searchfox.org/mozilla-central/rev/26a98a7ba56f315df146512c43449412f0592942/remote/shared/Realm.sys.mjs#269-329

That code runs as privileged code in the content process.

(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 no null 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:

https://searchfox.org/mozilla-central/rev/26a98a7ba56f315df146512c43449412f0592942/remote/shared/Realm.sys.mjs#269-329

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() )

(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.

(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 no null 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:

https://searchfox.org/mozilla-central/rev/26a98a7ba56f315df146512c43449412f0592942/remote/shared/Realm.sys.mjs#269-329

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:

  1. Launch Firefox with mach run --setpref="remote.log.level=Trace" --remote-debugging-port --remote-allow-origins=https://juliandescottes.github.io about:blank
  2. Go to your normal Firefox process and open https://juliandescottes.github.io/bidi-web-client/web/ in a new tab
  3. Click the connect button
  4. Click the send button (the very first one) for the session.new command.
  5. 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" },
  );
})()
Blocks: 1917540

Yes, just changing the principal doesn't help, since one needs to have some entry document around. And JS sandboxes don't have.

(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.

Assignee: nobody → hskupin
Status: NEW → ASSIGNED

(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.

Attachment #9425598 - Attachment description: WIP: Bug 1918288, let same origin js sandboxes use document.open → Bug 1918288, let same origin js sandboxes use document.open
Severity: -- → S3
Points: --- → 2
Priority: -- → P2
Whiteboard: [webdriver:m12]
Attachment #9425598 - Attachment description: Bug 1918288, let same origin js sandboxes use document.open → WIP: Bug 1918288, let same origin js sandboxes use document.open
Attachment #9425598 - Attachment description: WIP: Bug 1918288, let same origin js sandboxes use document.open → Bug 1918288, let same origin js sandboxes use document.open, r=farre
Whiteboard: [webdriver:m12] → [webdriver:m13]
Pushed by hskupin@mozilla.com: https://hg.mozilla.org/integration/autoland/rev/643406ca1194 let same origin js sandboxes use document.open, r=dom-core,farre https://hg.mozilla.org/integration/autoland/rev/ae9c76aad44a [wdspec] Extend navigation tests for document.write in sandboxes. r=webdriver-reviewers,jdescottes
Created web-platform-tests PR https://github.com/web-platform-tests/wpt/pull/48406 for changes under testing/web-platform/tests
Whiteboard: [webdriver:m13] → [webdriver:m13], [wptsync upstream]
Status: ASSIGNED → RESOLVED
Closed: 1 month ago
Resolution: --- → FIXED
Target Milestone: --- → 133 Branch
Upstream PR merged by moz-wptsync-bot
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: