Allow new Function() with undefined source when eval is blocked#6344
Conversation
|
Now I have a thorough understanding of the change. Let me analyze:
When
This is a relaxation that fixes what is effectively a bug —
The change looks correct and well-tested. The only potential concern is whether this should be gated behind a compat flag, but given it's a bugfix (relaxing an overly-strict restriction where V8 itself indicates no code string is involved), I think it's fine without one. LGTM |
2616a90 to
2660afa
Compare
2660afa to
e7ddac9
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6344 +/- ##
=======================================
Coverage 70.68% 70.68%
=======================================
Files 420 420
Lines 113010 113025 +15
Branches 18531 18534 +3
=======================================
+ Hits 79881 79893 +12
Misses 22086 22086
- Partials 11043 11046 +3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
When V8's code generation callback receives an undefined source (e.g.
`new Function()` with no arguments, or `class Foo extends Function {
constructor() { super(); } }`), there is no dynamic code to evaluate.
Allow these cases even when eval is otherwise blocked, since they pose
no code-injection risk.
Narrow the non-string source check from !IsString() to !IsString() && !IsObject(), so only known-safe primitives (undefined, null, boolean, number, bigint, symbol) are allowed through. Object types are denied by default as a defense-in-depth measure per review feedback.
e7ddac9 to
b8989c3
Compare
There was a problem hiding this comment.
I'm Bonk, and I've done a quick review of your PR.
This PR allows new Function() with no arguments and class extends Function { constructor() { super(); } } to succeed even when eval/code-generation is blocked, by adding two guards to modifyCodeGenCallback.
One issue:
- [Medium] Defensive
IsStringcheck beforeAs<v8::String>()— Line 543 castssourcetov8::Stringguarded only byisCodeLike, without verifyingsource->IsString(). V8's current implementation guaranteessourceis a string whenisCodeLikeis true (sinceCreateDynamicFunctionalways synthesizes a string viaIncrementalStringBuilder), but this invariant is internal to V8 and not part of the public API contract (ModifyCodeGenerationFromStringsCallbacktakesLocal<Value>). If a future V8 update introduces a path whereisCodeLikeis true with a non-string source, theAs<v8::String>()would crash instead of gracefully denying. Addingsource->IsString()to the condition makes this fail-safe: a broken invariant falls through to the deny path rather than crashing the process.
|
Review posted successfully on PR #6344. Summary: The PR is well-designed with thorough security analysis and good test coverage. I found one medium-severity issue: the |
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
8faed41 to
d539c4e
Compare
|
Now let me understand the full context. The latest commit ( Let me also check if the previous bonk review's suggestion (adding Let me now carefully analyze the current state of the code for any remaining issues:
All the previous review feedback has been addressed:
The code is well-designed, all review feedback has been resolved, test coverage is thorough, and there are no remaining actionable issues. LGTM |
Summary
Allow
new Function()with no arguments andclass Foo extends Function { constructor() { super(); } }to succeed even when eval/code-generation-from-strings is blocked. These produce empty functions with no user-provided code, so they pose no code-injection risk.How it works
V8's
ModifyCodeGenerationFromStringsCallbackis invoked whenever code generation from strings is attempted. The callback receives asourcevalue and anisCodeLikeflag. This PR adds two guards before the existingevalAllowedcheck:Guard 1: Undefined source
When
sourceisundefined(e.g.eval()with no argument oreval(undefined)), there is no string to compile. V8 returnsundefinedas-is per spec ("If x is not a String, return x"). We allow it through without further checks. (Wasm compilation uses a separate callback —AllowWasmCodeGenerationCallback— and never reaches this path.)Other non-string primitives (
null,true,42, bigint, symbol) are converted to strings by V8 viaToString()before reaching this callback, so they hit the normal string-based code generation path and don't need special handling here.Guard 2: Empty-body
new Function()(exact string match +isCodeLike)When the
Functionconstructor is called with no arguments, V8'sCreateDynamicFunction(builtins-function.cc:46-71) synthesizes the full source string(function anonymous(\n) {\n\n})before calling the callback. This string contains no user-provided code. We match it exactly.Why this is not vulnerable to code injection
The combination of
isCodeLikegating and exact string matching prevents all known attack vectors:eval("<evil> {\n\n}")isCodeLikeis alwaysfalseon the eval path, so Guard 2 is never enterednew Function('a', 'b', 'return a+b')isCodeLikeisfalsewhen arguments are plain strings (not CodeLike objects), so Guard 2 is never entered. Even if it were, the synthesized source includes the body and would not match the exact patternnew Function('a', 'b', undefined)"undefined"viaToString().isCodeLikeisfalse. Source includes the body text. Blocked on both countsnew Function('a}){evil()}{(', '')isCodeLikeisfalse. Blocked on both countsnew Function(codeLikeObj)isCodeLikewould betrue, but the body isToString(codeLikeObj)which produces a non-matching source stringWe intentionally only match the no-parameter, no-body case. Any call to
new Function()with arguments produces a different synthesized source that will not matchkEmptyFunctionSource.V8 update safety
The
kEmptyFunctionSourceconstant is tied to V8'sCreateDynamicFunctionformat. If V8 ever changes this format:new Function()gets blocked (fails safe, never allows)setup-testandworker-testwill fail, signaling the constant needs updatingChanges
src/workerd/jsg/setup.c++— RewritemodifyCodeGenCallbackwith two guards: undefined source passthrough + exact-match for empty Functionsrc/workerd/jsg/setup-test.c++— C++ unit tests covering allowed and blocked casessrc/workerd/api/tests/worker-test.js— JS integration tests in a worker context where eval is blocked at request time