Security Considerations
AccessControl sits on the authorization path of production systems, so v3 is hardened against the classes of bug that matter for an access‑control library — and documents the few decisions that remain yours.
Fail-closed Checks
Section titled “Fail-closed Checks”A denial returns granted: false. An error (invalid query, a strict
violation, a misused async condition) is thrown. The danger is a caller that
wraps a check in try/catch and lets the catch fall through to “allow”.
Use tryCan() on the hot path: it never throws — every failure resolves to
granted: false (the error event still fires for observability). Keep
can() for boot/config and tests, where you want a typo to blow up.
// ✅ fail-closed: an unknown role, bad input, or async-required all denyif (ac.tryCan(role).readAny('post').granted) { show();} else { deny();}See Best Practices › can vs tryCan.
Prototype-pollution & Inherited Keys
Section titled “Prototype-pollution & Inherited Keys”Names (roles, resources, actions, groups, categories) are user/data‑controlled strings used as object keys — the classic prototype‑pollution surface.
ac.can('user').readAny('toString').granted; // false (never throws)ac.grant('__proto__'); // throws RESERVED_NAMEConditions from Untrusted Sources
Section titled “Conditions from Untrusted Sources”If your grants/conditions are authored in code (the common case), they are trusted input. If they come from a store that lower‑privileged users can edit, treat conditions as untrusted and note the following.
Regular Expressions (ReDoS) — Opt-in
Section titled “Regular Expressions (ReDoS) — Opt-in”The matches operator compiles a regular expression. A malicious pattern can
cause catastrophic backtracking (CPU DoS).
Condition Depth
Section titled “Condition Depth”Deeply nested and/or/not trees are rejected at compile time (> 100
levels, err.code === 'INVALID_CONDITION') so a pathological condition from a
store cannot exhaust the stack on the auth path.
Error Messages & Information Disclosure
Section titled “Error Messages & Information Disclosure”By default (engine.safeErrors: true) error messages omit caller‑supplied
values (names, the raw query/grants object) so request data doesn’t leak into
logs. The values stay available programmatically.
const e = grab(() => ac.can('ghost').readAny('post').granted);e.message; // "Role not found." (redacted)e.code; // "ROLE_NOT_FOUND"e.role; // "ghost" (structured field)Also return uniform responses for denials (same status/body) so “doesn’t exist” vs “denied” isn’t observable to a client.
Names & Homographs (Charset)
Section titled “Names & Homographs (Charset)”Names are restricted to ASCII [A-Za-z0-9_-] by default — which also rules out
Unicode homograph attacks (visually identical names with different code
points).
Immutability
Section titled “Immutability”getGrants(), getGrantsList() and getRequirements() return detached deep
copies — mutating a result can never alter the live model or neuter a
require() gate. lock() deep‑freezes the model; after it, every mutator
throws err.code === 'LOCKED'. Permission.attributes / .roles are frozen
copies too.
Timing Side-channels
Section titled “Timing Side-channels”Authorization isn’t constant‑time (more roles/rules/conditions ⇒ more work). In practice this is buried under network and DB latency, so it’s effectively unexploitable for server‑side checks — we treat constant‑time evaluation as a non‑goal. If it’s in your threat model: rate‑limit auth‑sensitive endpoints and keep denial responses uniform.
Supply Chain
Section titled “Supply Chain”The published package’s only runtime dependency is
notation — although from the same author,
it is pinned to an exact version — and there are zero production advisories
(npm audit --omit=dev). Recommended for consumers:
npm audit --omit=dev # audit only what actually shipsWhat Testing Can and Cannot Prove
Section titled “What Testing Can and Cannot Prove”100% coverage and mutation testing prove the written code behaves; they cannot prove the engine is safe against inputs the code never anticipated. AccessControl therefore also ships:
- an adversarial suite (prototype gadgets, inherited‑key names, context spoofing, notation‑path pollution, lock/immutability, deny/wildcard leakage), and
- a seeded property fuzzer asserting engine invariants over thousands of random policies (determinism, possession cascade, multi‑role = union, serialization round‑trip, deny/require monotonicity, filter idempotence).
See Quality & testing.