Security
Mirrors SECURITY.md in the repository.
Reporting a vulnerability
Please report suspected vulnerabilities privately via GitHub Security Advisories or by emailing the author (see package.json). Do not open public issues for security reports. You should receive a response within a few days.
Supported versions
Only the latest published 3.x release receives security fixes.
Threat model
Kerberos.js is an in-process authorization engine. Its trust boundaries are deliberately different from a networked authorization server:
- Policies are code. In-memory policies contain live JavaScript functions (conditions, variables, outputs) that run with the privileges of your process. Loading a policy is equivalent to loading a module — author and review policies with the same care as application code. Kerberos does not sandbox in-memory policy functions.
- Dynamic (cache-stored) policies are data. Policies loaded through the
cacheoption must be JSON. With the built-in safe codec (codec: { jsep }),{ "$expr": "..." }descriptors are evaluated by an eval-free AST allowlist interpreter: noeval, nonew Function, nofn.toString(); identifiers resolve only against{P, R, V, C}and curated safe builtins;__proto__/prototype/constructoraccess is blocked at the interpreter level regardless of spelling; expression length, nesting depth and the AST cache size are bounded by default (configurable viacodec: { maxExprLength, maxDepth, maxCachedExprs }). - A compromised policy store means compromised authorization decisions. The codec prevents code execution and resource exhaustion from a hostile store, but an attacker who can write policies can still grant or deny permissions. Protect the store (Redis/Mongo/Postgres) accordingly and use the
serializePolicyhelper to validate documents before writing them. - Call IDs are correlation identifiers, not security tokens. In insecure browser contexts (plain HTTP) where
crypto.randomUUIDis unavailable, IDs fall back to aMath.random-based pseudo UUID. - Relation tuples from a store are data. The built-in ReBAC resolver (
@alexify/kerberos/relations) applies the same model as dynamic policies: cached relation documents are plain JSON, entries not admitted by the compiled relation schema are skipped fail-closed, and recursion is bounded by a configurablemaxDepth. Data errors are never read as answers: a corrupt document throws a typedKerberosCodecErrorand a caveat whose condition throws raisesKerberosRelationsError(both propagate per the engine'sonErrorsemantics) — resolving them as "empty"/"not matched" would silently widen access in exclusion positions (viewer − editorwith an unreadable editor document). A caveat that cleanly evaluates tofalsesimply does not match. Caveat conditions authored as{ "$expr": "..." }go through the same eval-free codec interpreter (scoped to{P, ctx}roots). An attacker who can write tuples can still grant or deny relations — protect the store accordingly. - In-process ReBAC has no Zanzibar consistency. There are no revision tokens (zookies) or per-request consistency levels: the staleness window for cache-backed tuples equals your cache-invalidation window, so a just-revoked relation may briefly still pass on another host (the "new enemy" scenario). If that window matters for your threat model, keep revocation-sensitive relationships in static tuples, shorten TTLs/invalidation latency, or use a centralized authorization service.
- Query plans embed folded principal/constant values.
planResourcespartially evaluates policies against the fully-known principal, so the returnedfilter(andmeta.filterDebug) contains literal values derived from principal attributes, constants and variables (e.g.eq(request.resource.attr.owner, "<principal id>")). Treat plans as output for trusted sinks — your database translator or backend — and do not forward raw plans orfilterDebugstrings to untrusted clients without reviewing what they disclose. Operands are guaranteed JSON-safe: values that JSON transport would corrupt (undefined,NaN/Infinity,Dateobjects,BigInt) are never emitted — such conditions degrade to theopaqueoperator instead. - Compiled
$exprASTs are deeply frozen. Expression ASTs are cached and shared across every compiled closure of the same expression (and surfaced to the internal query planner). They are deep-frozen at parse time, so in-process code cannot mutate a cached AST to alter the behavior of other policies using the same expression.
Philosophy: opt-in safety layers
Kerberos targets users who know what they are doing; safety layers are explicit, composable opt-ins rather than built-in overhead:
- Validation is provided by the validation backend you pass (
z,ajv, orajv+typebox). Without a backend, policy shapes are accepted as-is for zero construction overhead — malformed policies then fail at evaluation time. If you load policies from anywhere you do not fully control, configure a validation backend. - Dynamic-policy safety is provided by using
codectogether withcache. Passingcachewithout a codec hands cached values to policy constructors unmodified — do that only when you fully trust the store. - Failure semantics are controlled by
onError('throw'by default;'deny'for fail-closed deployments). A throwing logger or telemetry backend can never affect authorization results — those integrations are internally guarded.