Skip to content

Enforcement-core hardening: fail-open & policy-widening classes in validatabl/limitabl/identifiabl/explicabl #41

Description

@davidcrowe

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions