Enforcement-core hardening: fail-open & policy-widening classes in validatabl/limitabl/identifiabl/explicabl #41
Copy link
Copy link
Closed
Labels
bugSomething isn't workingSomething isn't working
Description
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 isundefined(GET, wrong content-type). Fix: wheninputSchemais configured, missing input must DENY (validateundefinedagainst the schema). - validatabl-core:
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. - validatabl-core: policy widening (
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. - limitabl-core: AgentGuard is client-opt-out and unbounded (
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. - 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
expnot required (src/index.ts:161-165). A token minted withoutexpis valid forever. Fix:requiredClaims:["exp"], reject empty issuer/audience at factory time. - explicabl: audit events carry no identity (
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. - explicabl:
/webhooks/auth0mounts the router FACTORY as middleware (src/index.ts:157) → requests hang forever, and the webhook is dead. Fix: callauth0LogsWebhook(). - explicabl: aborted requests emit no audit event (
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 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.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working
Activity
Enforcement-core hardening: fail-open & policy-widening fixes (#41)
Enforcement-core hardening: fail-open & policy-widening fixes (#41) (#51
davidcrowe commentedon Sep 22, 2026
Fixed on main by #51 (enforcement-core hardening: fail-open and policy-widening fixes).