# Authz follow-ups Findings from the reviews on branch `security/0.8.4-audit-and-authz-design` that were adjudicated as non-blocking. None is an authorization bypass. Recorded here because the review workspace is scratch and git history does not carry the reasoning. ## Worth a ticket **`guardPermission` turns a 403 into a 500 when the middleware is missing.** `packages/authz/src/middleware.ts` — the `getResource` catch now calls `readAuthz(ctx)` to reach the audit sink, and `readAuthz` throws `WRN-AUTHZ-SETUP` when `authzMiddleware` was never registered. That configuration is already broken, and a throw denies rather than grants, but it converts a clean denial into a framework 500. Read the sink defensively instead of destructuring `readAuthz`. **`deniedBy()` fails open in isolation.** `packages/authz/src/engine.ts` returns `false` for a non-array argument. Safe for the one in-repo caller, which pre-validates, but `deniedBy` is on the public surface and an external caller passing a string gets a silent `false`. Throwing a `TypeError` would make the guard self-contained. **`permissionsFor()` still reads `denies` unguarded.** Same shape the engine's `decide()` was hardened against: a store omitting `denies` throws a raw `TypeError`. Not fail-open, and the doc comment already says never to gate on this result, but it is inconsistent with the fix applied next to it. ## Behaviour to carry into release notes **`authorizeDecision`'s 403 body no longer contains `reason` or `policy`.** Approved breaking change — policy names describe internal authorization structure. Opt back in with `{ exposeReason: true }`. No in-repo caller relied on the old shape. **RBAC namespace wildcards now match at every depth.** `post:comment:*` previously did not grant `post:comment:delete`. The fix is correct, but it _widens_ access for any app that relied on the old first-segment-only behaviour. **`Router` gained a required `authz` field.** Compile-time break for anything constructing a `Router` object literal — custom deployment adapters, test fixtures. Consider making it optional. **`subject.id` must be a non-empty string.** Integer primary keys deny every request and log to stderr. Documented in the authz README; worth a release-note line too. ## Known gaps, deliberately accepted **`listSubjects` and `assignmentsFor` disagree about "in this tenant".** Reads union global and tenant scope; `listSubjects` matches the scope key exactly. An admin UI built on `listSubjects` omits globally-granted superusers. Both adapters agree with each other, so this is a model choice, not drift — but it is on the public `PermissionStore` interface. **DNS pinning has no real-TLS test.** Every test in `ssrf-regression.test.ts` stubs `globalThis.fetch`, so `tls: { serverName }` is only asserted as an object property. If a runtime ever validates the certificate against the dialed IP rather than `serverName`, every HTTPS `safeFetch` breaks by default and no test would notice. One live-network smoke test closes this. **`safeFetch` re-attaches credentials on a→b→a.** Credentials return to the intended origin, but the path is attacker-chosen. Browsers do not re-add after leaving the origin. Track a `hasLeftOrigin` latch. **Gateway basic-auth: the username compare short-circuits.** `packages/dev-server/src/gateway.ts` — `&&` skips the password compare when the username misses, giving a measured 2.1x timing signal (39.8ms vs 83.6ms over 200k iterations). Username enumeration. The password compare itself is constant-time. Evaluate both, then combine. **`safeFetch` buffers the whole body before checking `maxResponseBytes`.** Pre-existing, not introduced by this branch: with no `content-length`, `await response.arrayBuffer()` buffers everything first. Verified 8MB buffered against a 1KB limit. **`packages/router` does not declare `@wrnexus/ui`.** Pre-existing. Passes every in-repo gate because bare `@wrnexus/*` specifiers resolve through the root tsconfig `paths` map, not `node_modules` — the same class of defect that would have shipped a broken published CLI. Worth auditing every package's declared-vs-imported dependencies once. **The generated `Permission` union has no consumer.** `can`, `guardPermission` and `decideFor` take bare `string`. The docstrings and design doc were corrected to stop promising compile-time checking; wiring a type parameter is a real option if wanted. **Catalog conflict origin tracking drifts.** A later re-declaration overwrites the recorded source file, so a conflict message can name the wrong original. The conflict is still detected; only the diagnostic is affected.