Two exported functions in src/safe-evaluator.ts are weaker than the guarded path next to them, and neither is called anywhere in src/. The package exports every module through the ./* subpath, so both are reachable as @stacksjs/stx/safe-evaluator.
createSafeLoopFunction — interpolations are injected unchecked
src/safe-evaluator.ts:1104. The loop expression is checked with isForExpressionSafe. The loop body is not:
const processedBody = body
.replace(/`/g, '\\`')
.replace(/\$\{/g, '\\${')
.replace(/\{\{([^}]+)\}\}/g, (_match, expr) => `\${${expr}}`)Backticks and ${ are escaped so body text can't break out — and then is deliberately turned back into ${...}, splicing expr verbatim into a template literal that goes straight to Function(...). Nothing between that expr and the compiler: no prepareExpression, no denylist, no length limit. Whatever the escaping above was protecting is reopened by the line after it.
safeEvaluateCode — arbitrary statements behind the incomplete denylist
src/safe-evaluator.ts:1015 runs multi-statement code with Function(...keys, code) behind the same DANGEROUS_PATTERNS list, which (see the sandbox-escape issue I'm filing alongside this) doesn't include Bun, fetch or Buffer. For a single expression the denylist is at least the documented boundary; for a statement body it isn't close.
Why file it when nothing calls them
Because they're exported from a module named safe-evaluator, next to functions that are the security boundary, and both names promise the same guarantee. The next caller — a plugin, an app, a future directive — will reasonably assume they got checked.
Either delete them, or drop them from the public surface and rename them to say what they do (unsafeEvaluateCode). If createSafeLoopFunction is meant to come back, its interpolations need to go through prepareExpression like every other expression in the engine.
Found while auditing Stacks and stx for hq.training. @stacksjs/stx@0.2.534.