AudioWorklet missing Trusted Types enforcement
Categories
(Core :: DOM: Security, defect)
Tracking
()
| 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)
-
Trusted Types enforcement: The
HostGetCodeForEvalcallback is NULL in Worklet'sSecurityCallbacksstruct (line 401), while Worker registersTrustedTypeUtils::HostGetCodeForEval(line 777-778). A page deployingrequire-trusted-types-for 'script'CSP has Trusted Types enforced in Workers but completely bypassed in AudioWorklets. This is reachable from web content. -
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 viaTrustedTypeUtils::HostGetCodeForEvalinJSSecurityCallbacks. Worklet'sSecurityCallbackshas 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)
- 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.
-
Open Firefox Nightly and navigate to
http://localhost:8888/poc-trustedtypes-bypass.html(attached) -
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
- 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
- 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.
- 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));
- 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
JSSecurityCallbackspath —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 PoCcontrol-worker-eval.js— Control test (Worker eval blocked)runtime-results.txt— Full runtime verification outputsource-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.
| Reporter | ||
Comment 1•6 months ago
|
||
| Reporter | ||
Comment 2•6 months ago
|
||
| Reporter | ||
Comment 3•6 months ago
|
||
| Reporter | ||
Comment 4•6 months ago
|
||
Updated•6 months ago
|
| Assignee | ||
Updated•5 months ago
|
| Assignee | ||
Updated•5 months ago
|
| Assignee | ||
Updated•5 months ago
|
Comment 5•5 months ago
|
||
FWIW, specs doesn't say anything about TT applying to worklets.
| Assignee | ||
Comment 6•5 months ago
|
||
| Assignee | ||
Comment 7•5 months ago
|
||
I don't think we need to worry about nsContentUtils::IsEvalAllowed. Worklets are not an interesting target to exploit.
| Assignee | ||
Updated•5 months ago
|
Comment 9•5 months ago
|
||
| Reporter | ||
Comment 10•5 months ago
|
||
Thanks for the fast turnaround.
Will this be included in a future MFSA advisory?
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Comment 12•3 months ago
|
||
Comment 13•3 months ago
|
||
Comment 14•3 months ago
|
||
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)
Comment 15•3 months ago
|
||
Comment 16•3 months ago
|
||
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?
| Assignee | ||
Comment 17•3 months ago
|
||
There is nothing to do, I just landed a test for this.
Updated•1 month ago
|
| Reporter | ||
Comment 18•1 month ago
|
||
Could you tell me how to verify whether the vulnerability reward was properly processed?
| Assignee | ||
Comment 19•1 month ago
|
||
For bounty questions please email security@mozilla.org.
Description
•