Skip to content

fix(mv2): [temporary workaround] privileged api leakage due to same-origin#1469

Open
cyfung1031 wants to merge 2 commits into
release/mv2from
fix/mv2/same-origin-privilege
Open

fix(mv2): [temporary workaround] privileged api leakage due to same-origin#1469
cyfung1031 wants to merge 2 commits into
release/mv2from
fix/mv2/same-origin-privilege

Conversation

@cyfung1031
Copy link
Copy Markdown
Collaborator

@cyfung1031 cyfung1031 commented May 25, 2026

See #1470 and #1448

We have no choice in MV2....


Summary

This PR adds a temporary Firefox MV2 workaround to reduce privileged API leakage caused by the same-origin sandbox setup used for background/scheduled scripts.

After #1448, Firefox MV2 needs sandbox="allow-same-origin allow-scripts" for compatibility, but that means the userscript execution context can still reach extension-origin globals. In particular, background scripts may access privileged APIs through chrome, browser, parent, top, or frameElement.

This change hardens the MV2 background/scheduled script runtime by shadowing and proxying those escape hatches.

Changes

src/runtime/content/exec_script.ts

  • Detects background and scheduled scripts using scriptRes.type === 2 || scriptRes.type === 3.
  • Enables a sandbox workaround flag for those script types.
  • Rewrites the script source before compilation by inserting a prelude before the first real code line.
  • The injected prelude shadows:
    • chrome as {}
    • browser as undefined
    • frameElement as null
    • parent as window
    • top as window
  • Also attempts to redefine corresponding window.* properties with guarded Object.defineProperty(...) calls.
  • Passes the sandbox flag into proxyContext(...).

src/runtime/content/utils.ts

  • Adds an optional isSandbox argument to proxyContext(...).
  • When sandbox mode is enabled:
    • top and parent resolve to the sandbox proxy instead of the real parent/top window.
    • frameElement resolves to null.
    • frameElement is reported as present by the has trap.
    • assignments to top, parent, and frameElement are rejected.

Motivation

Firefox MV2 currently lacks the sandbox manifest support needed for a cleaner isolation model. Because ScriptCat still needs to support Firefox MV2, this PR adds a runtime-level mitigation for the privileged API leakage described in #1470.

Security impact

This reduces accidental or direct access from background/scheduled userscripts to privileged extension APIs in Firefox MV2 same-origin execution contexts.

The workaround covers common access paths:

  • direct globals: chrome, browser, parent, top, frameElement
  • window.* access
  • proxy-context access
  • in checks for sandboxed globals
  • assignment attempts to protected globals

Scope

This workaround applies only to script types 2 and 3, which are treated here as background/scheduled scripts.

Normal content/page script execution should keep the previous proxy behavior.

Limitations

This is a temporary workaround, not a full browser-level sandbox replacement.

It relies on source rewriting and proxy traps, so future changes to script compilation or execution context creation should verify that these protections still run before userscript code executes.

Related


Verification Background Script

// ==UserScript==
// @name         New Userscript M9NO-1
// @namespace    https://docs.scriptcat.org/
// @version      0.1.0
// @description  try to take over the world!
// @author       You
// @background
// ==/UserScript==

const chrome_ = typeof chrome === "undefined" ? undefined : chrome;
const browser_ = typeof browser === "undefined" ? undefined : browser;

console.log("sandbox 1", {
    chrome: chrome_,
    browser: browser_,
    frameElement: frameElement,
    frameElementToWindow: frameElement?.ownerDocument.defaultView,
    parentIsWindow: parent === window,
    topIsWindow: top === window,
});

console.log("sandbox 2", {
    chrome: window.chrome,
    browser: window.browser,
    frameElement: window.frameElement,
    frameElementToWindow: window.frameElement?.ownerDocument.defaultView,
    parentIsWindow: window.parent === window,
    topIsWindow: window.top === window,
});

console.log("sandbox 3", {
    chrome: this.chrome,
    browser: this.browser,
    frameElement: this.frameElement,
    frameElementToWindow: this.frameElement?.ownerDocument.defaultView,
    parentIsWindow: this.parent === window,
    topIsWindow: this.top === window,
});


console.log("sandbox 1e", {
    chrome: eval("chrome"),
    browser: eval("browser"),
    frameElement: eval("frameElement"),
    frameElementToWindow: eval("frameElement?.ownerDocument.defaultView"),
    parentIsWindow: eval("parent === window"),
    topIsWindow: eval("top === window"),
});

console.log("sandbox 2e", {
    chrome: eval("window.chrome"),
    browser: eval("window.browser"),
    frameElement: eval("window.frameElement"),
    frameElementToWindow: eval("window.frameElement?.ownerDocument.defaultView"),
    parentIsWindow: eval("window.parent === window"),
    topIsWindow: eval("window.top === window"),
});

console.log("sandbox 3e", {
    chrome: eval("this.chrome"),
    browser: eval("this.browser"),
    frameElement: eval("this.frameElement"),
    frameElementToWindow: eval("this.frameElement?.ownerDocument.defaultView"),
    parentIsWindow: eval("this.parent === window"),
    topIsWindow: eval("this.top === window"),
});

return new Promise((resolve, reject) => {
    // Your code here...
    resolve();

});
Screenshot 2026-05-25 at 19 54 46

@cyfung1031 cyfung1031 marked this pull request as ready for review May 25, 2026 10:56
@cyfung1031 cyfung1031 added P0 🚑 需要紧急处理的内容 hotfix 需要尽快更新到扩展商店 FirefoxMV2 security labels May 25, 2026
@cyfung1031 cyfung1031 changed the title fix(mv2): privileged api leakage due to same-origin fix(mv2): [temporary workaround] privileged api leakage due to same-origin May 25, 2026
@cyfung1031 cyfung1031 linked an issue May 25, 2026 that may be closed by this pull request
@CodFrm CodFrm requested a review from Copilot May 26, 2026 05:53
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a Firefox MV2-focused hardening path for background/scheduled userscripts (script types 2 and 3) to mitigate privileged WebExtensions API leakage that can occur in same-origin sandbox execution contexts.

Changes:

  • Adds an isSandbox mode to proxyContext(...) to prevent escaping via top, parent, and frameElement.
  • For script types 2/3, rewrites the compiled script source to inject a prelude that attempts to shadow common escape hatches (chrome, browser, window.*, etc.).
  • Wires the sandbox flag from ExecScript into the proxied execution context.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/runtime/content/exec_script.ts Detects background/scheduled scripts and injects a defensive prelude; passes isSandbox into proxyContext.
src/runtime/content/utils.ts Extends proxyContext with isSandbox behavior for top/parent/frameElement get/has/set semantics.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 126 to 146
get(_, name): any {
switch (name) {
case "window":
case "self":
case "globalThis":
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return proxy;
case "top":
case "parent":
if (isSandbox) return proxy;
if (global[name] === global.self) {
return special.global || proxy;
}
return global[name];
// eslint-disable-next-line no-fallthrough
case "frameElement":
if (isSandbox) return null;
// eslint-disable-next-line no-fallthrough
default:
break;
}
Comment on lines +59 to +78
const scriptType = this.scriptRes.type;
let scriptCode = this.scriptRes.code;
if ((scriptType === 2 || scriptType === 3) && scriptCode.length > 0) {
isSandbox = true;
// background script / scheduled script
// we need to remove the access to chrome or browser in FirefoxMV2
// since sandbox manifest support is still missing.
const arr = scriptCode.split("\n");
for (let i = 0, l = arr.length; i < l; i++) {
const t = arr[i].trim();
if (!t || t.startsWith("//")) continue;
const codeGen = (target: string, property: string, value: string): string => {
return `try { Object.defineProperty(${target}, "${property}", { get() { return ${value}; }, configurable: false }); } catch {}`;
};
arr.splice(i, 0, `let chrome = {}, browser = undefined, frameElement = null, parent = window, top = window; try { window.parent = window; } catch {} ${codeGen("window", "parent", "this")} ${codeGen("window", "top", "this")} ${codeGen("window", "chrome", "{}")} ${codeGen("window", "browser", "undefined")} ${codeGen("window", "frameElement", "null")}`)
scriptCode = arr.join("\n");
break;
}
}
this.scriptFunc = compileScript(scriptCode);
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

} else {
// 构建脚本资源
this.scriptFunc = compileScript(this.scriptRes.code);
const scriptType = this.scriptRes.type;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

是不是应该只针对后台脚本进行处理,原来的脚本逻辑最好不动

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

FirefoxMV2 hotfix 需要尽快更新到扩展商店 P0 🚑 需要紧急处理的内容 security

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] FirefoxMV2: privileged api leakage due to same-origin

3 participants