Context
Enforcement-core security review (2026-07-23) of the OSS packages. These live in code that a star-push evaluator WILL read. Note: production's coding-agent hook path uses a private engine, so most of these are reputational/external-consumer correctness rather than live prod exposure — but the repo is the trust artifact, so they matter here.
Findings (each with fix direction)
- validatabl-core: schema-only config is fail-open when input is absent (
src/decision.ts:101). if (options.inputSchema && request.input !== undefined) skips validation when input is undefined (GET, wrong content-type). Fix: when inputSchema is configured, missing input must DENY (validate undefined against the schema).
- validatabl-core:
hasScope builds a regex from the unescaped scope string (src/scopes.ts:30). A required scope with . matches any char (tool.write satisfied by toolXwrite); metachars throw → 500. Fix: getScopeStringFromClaims(...).split(" ").includes(scope) — no regex.
- validatabl-core: policy widening (
src/policy.ts): unanchored matches ("search" allows unsafe-search-exec), in array/String() coercion, empty-conditions matches every request. Fix: anchor by default, strict string typecheck, reject empty-condition rules at load.
- limitabl-core: AgentGuard is client-opt-out and unbounded (
src/preflight.ts:78, key from x-workflow-id). Omit header → guard never runs; rotate IDs → counters reset + Map grows unbounded. Fix: derive the key server-side, deny when guard configured but no key resolves, add TTL cleanup.
- limitabl: singleton engine ignores every config after the first (
src/index.ts:26-33). A stricter later mount silently reuses the first (laxer) config. Fix: key the cache per-config or build one engine per invocation.
- identifiabl-core: JWT
exp not required (src/index.ts:161-165). A token minted without exp is valid forever. Fix: requiredClaims:["exp"], reject empty issuer/audience at factory time.
- explicabl: audit events carry no identity (
src/index.ts:106-109) — reads res.locals.gatewayContext which nothing sets; identity lives in ALS. Every audit line logs context: undefined. Fix: read getGatewayContext() in the finish handler; warn once if absent.
- explicabl:
/webhooks/auth0 mounts the router FACTORY as middleware (src/index.ts:157) → requests hang forever, and the webhook is dead. Fix: call auth0LogsWebhook().
- explicabl: aborted requests emit no audit event (
src/index.ts:96, only finish hooked). Fix: also hook close with a fired-once guard.
Missing tests to lock the guarantees
decision() with inputSchema + undefined input denies; hasScope("a.b") doesn't match aXb; empty-conditions rule rejected; JWT round-trips reject alg:none/expired/missing-exp/wrong-aud; two limitabl mounts enforce different limits; redactPii overlap; client-abort emits audit.
Context
Enforcement-core security review (2026-07-23) of the OSS packages. These live in code that a star-push evaluator WILL read. Note: production's coding-agent hook path uses a private engine, so most of these are reputational/external-consumer correctness rather than live prod exposure — but the repo is the trust artifact, so they matter here.
Findings (each with fix direction)
src/decision.ts:101).if (options.inputSchema && request.input !== undefined)skips validation when input isundefined(GET, wrong content-type). Fix: wheninputSchemais configured, missing input must DENY (validateundefinedagainst the schema).hasScopebuilds a regex from the unescaped scope string (src/scopes.ts:30). A required scope with.matches any char (tool.writesatisfied bytoolXwrite); metachars throw → 500. Fix:getScopeStringFromClaims(...).split(" ").includes(scope)— no regex.src/policy.ts): unanchoredmatches("search"allowsunsafe-search-exec),inarray/String()coercion, empty-conditionsmatches every request. Fix: anchor by default, strict string typecheck, reject empty-condition rules at load.src/preflight.ts:78, key fromx-workflow-id). Omit header → guard never runs; rotate IDs → counters reset + Map grows unbounded. Fix: derive the key server-side, deny when guard configured but no key resolves, add TTL cleanup.src/index.ts:26-33). A stricter later mount silently reuses the first (laxer) config. Fix: key the cache per-config or build one engine per invocation.expnot required (src/index.ts:161-165). A token minted withoutexpis valid forever. Fix:requiredClaims:["exp"], reject empty issuer/audience at factory time.src/index.ts:106-109) — readsres.locals.gatewayContextwhich nothing sets; identity lives in ALS. Every audit line logscontext: undefined. Fix: readgetGatewayContext()in the finish handler; warn once if absent./webhooks/auth0mounts the router FACTORY as middleware (src/index.ts:157) → requests hang forever, and the webhook is dead. Fix: callauth0LogsWebhook().src/index.ts:96, onlyfinishhooked). Fix: also hookclosewith a fired-once guard.Missing tests to lock the guarantees
decision() with inputSchema + undefined input denies;
hasScope("a.b")doesn't matchaXb; empty-conditions rule rejected; JWT round-trips reject alg:none/expired/missing-exp/wrong-aud; two limitabl mounts enforce different limits; redactPii overlap; client-abort emits audit.