Relationships
⊘ blocks #2947#2949 extension push: eval()/new Function() safety check is a substring match and blocks obj.eval( method calls in bundled libraries
Opened by skunk-ape · 10/2/2026· Shipped 10/2/2026
What happens
swamp extension push --dry-run (and a real push) refuses an extension with:
Safety errors (push blocked):
.../extensions/models/_lib/engine/studio_asset_app.ts: File contains eval() or new Function() which are not allowed.The file has no call to the global eval and no new Function. It embeds a bundled browser page that includes the cel-js CEL interpreter (npm:@marcbachmann/cel-js@7.6.1). cel-js names its tree-walking evaluator method eval, so the minified bundle contains about 32 member calls of the form f.eval(m.receiver, y) and one class method definition eval(t,n){return t.evaluate(this,t,n)}.
Why
The check (in the binary, CLI 20261001.095719.0-sha.f20ce9f1) is a plain substring test:
if (content.includes("eval(") || content.includes("new Function(")) { errors.push(...) }So any member call or method named eval blocks a push, along with identifiers ending in eval, such as retrieval( or interval(, and the text inside strings and comments. The same library imported through an npm: specifier passes, because the scanner reads only the extension's own source files, so the check blocks the safe case and lets the same code through when it's imported from npm.
Expected
Only flag real dynamic code execution: a call to the global eval (not preceded by . or an identifier character, and not a method definition), indirect forms such as (0, eval)(...) or globalThis.eval(...), and new Function( / Function( called as a constructor. A token-level check (or the TypeScript AST the bundler already has) would do it. Strings and comments should not count.
Reproduce
An extension model whose source contains any of:
const r = interpreter.eval(ast, ctx);
class Interp { eval(node) { return node; } }then swamp extension push manifest.yaml --dry-run.
Impact
Blocks @swamp/stagecraft (swamp-club #2947, release target 2026-10-14). Its studio page runs the factory engine in the browser, including CEL gate evaluation, under a default-src 'self' CSP that already forbids eval, so the page shows the library works without dynamic code.
Shipped
Click a lifecycle step above to view its details.
skunk-ape commented 10/2/2026, 3:36:16 PM
Also blocks swamp extension quality: it packages the extension first and stops with 'Extension has safety errors that must be resolved before pushing.' So @swamp/stagecraft can neither be scored nor dry-run pushed until this is fixed. With the bundle's .eval( member calls rewritten in a scratch copy (only to see what fails next), quality scores 12/12 and the dry-run builds a 1.3 MB archive with no other errors.
skunk-ape commented 10/2/2026, 6:33:48 PM
Fixed in swamp 20261002.182915.0-sha.e2b0a082 (PR #2803). The check now parses each extension file with @babel/parser instead of matching text. These no longer block a push or pull: eval text in strings, comments and regexes; method and property definitions named eval (eval(t,n){...}); private #eval; names like retrieval( and interval(. The safety error now lists line:column for each finding. stagecraft's manifest passes, because its .eval( calls sit inside the embedded browser bundle string.
One part of the request is deliberately not done: a member access named eval or Function, such as interpreter.eval(ast, ctx), is still flagged, whatever the receiver. During review it turned out the receiver can be the global object through routes no static rule can list: globalThis.valueOf(), a host-bound this in a callback, global. The old text check blocked all of them, so allowing member calls would have made the gate weaker. If your own extension source calls x.eval(...), rename the method or import the library from npm; npm imports are not scanned. A file that does not parse as a module falls back to the old text check. Remaining gaps are tracked in #2976.
Sign in to post a ripple.