diff --git a/.dev-loop/INGEST_REPORT.md b/.dev-loop/INGEST_REPORT.md index 55ccfd1..9ac3610 100644 --- a/.dev-loop/INGEST_REPORT.md +++ b/.dev-loop/INGEST_REPORT.md @@ -1,53 +1,207 @@ -# Knowledge consolidation — 15 open PRs (#17–#40) → one reconciled state +# Knowledge flush — 4 insight(s) -The 15 open `knowledge/*` PRs (created 2026-08-04 → 2026-08-05, before the -harvest processed-store dedupe fix in #41) contained 123 file-versions of ~75 -unique pages, with the same insight landing at up to 3 different paths across -up to 8 PRs. Per-PR review would re-import those duplicates, so — as with the -#6–#13 consolidation — this branch carries the reconciled end-state and the 15 -PRs are closed in its favor. +Queue drained: `~/.dev-loop/queue/` held 4 pending rows across 3 session files. +Result: 2 new pages, 1 merge into an existing page, 1 candidate corrected and +folded (its asserted directive did not survive verification). ## Verified best-practice -Every adopted page's sources were carried from its originating PR's flush, where -they were live-verified at flush time; no new URLs were introduced during -consolidation (checked mechanically: every `http(s)` URL in every merged page -appears in a source PR's diff; every added body line in amended pages traces to -a source PR hunk — orphan-line verification). Confidence fields were kept as the -originating flushes set them, except client-side-rate-limiting where the union -of provider-doc citations (Okta, Auth0, GitHub, OpenAI, RFC 6585) supports -`verified` for the load-bearing claims. One subagent's fabricated content (12 -files matching neither main nor any PR, with invented source URLs) was detected -by the same verification and replaced with true PR content. +### 1. A plugin-bundled MCP server missing from `/mcp` is a registration fault first + +**Claim.** When a Claude Code plugin's MCP server does not appear in `/mcp`, run +`/reload-plugins` before editing any configuration; MCP servers are connected at +the point the plugin layer loads, so a session that predates the install/edit +does not know the server. + +**Sources checked.** +- https://code.claude.com/docs/en/plugins-reference — "Changes you make to a + skill's `SKILL.md` take effect immediately in the current session. Changes to + the plugin's other components, such as `hooks/`, `.mcp.json`, `agents/`, and + `output-styles/`, do not. Run `/reload-plugins` or restart Claude Code to pick + those up." Also: "When a plugin updates mid-session, hook commands, monitors, + MCP servers, and LSP servers keep using the previous version's path." +- https://code.claude.com/docs/en/mcp — "At session startup, Claude Code connects + the servers for enabled plugins automatically"; "If you enable or disable a + plugin during a session, run `/reload-plugins` to connect or disconnect its MCP + servers." + +**How verified.** Both quotes read from the live official docs this session. The +session's own reproduction matches: `lnpl-mcp` 0.3.0 was installed, registered in +`enabledPlugins`, and answered `initialize` correctly when run by hand, yet was +absent from `/mcp` until `/reload-plugins`, which exposed its tools immediately. + +**Confidence: verified.** + +Sub-claim — **both `.mcp.json` shapes load** (a bare map of server names, and the +documented `{"mcpServers": {...}}` wrapper), so the shape is a poor first +suspect. The docs show only the wrapper; the bare map was verified by direct +observation rather than documentation: in this machine's plugin cache, +`context7` and `playwright` ship a bare map while `claude-mem`, `figma`, and +`atlassian` ship the wrapper, and servers of **both** shapes are live in this +session (their tools are callable as +`mcp__plugin_context7_context7__*` / `mcp__plugin_figma_figma__*`). +**Confidence: verified** (reproducible check, not doc-stated). + +### 2. `.mcp.json` `env` — the queued directive did NOT verify; corrected before ingest + +**Queued claim.** "Do not relay a variable the user already exports through +`env`; a stdio server is a child process, so it inherits the parent environment — +use `env` only to inject values the plugin alone knows." + +**What the sources say.** +- https://github.com/modelcontextprotocol/typescript-sdk/issues/216 — the + reference stdio client's `getDefaultEnvironment()` returns only + `DEFAULT_INHERITED_ENV_VARS` (`HOME`, `LOGNAME`, `PATH`, `SHELL`, `TERM`, + `USER` on POSIX) when `env` is absent, and a supplied `env` **replaces** rather + than extends that set. An arbitrary exported variable such as `LNPL_IMPL` is + not inherited by default in that implementation. +- https://code.claude.com/docs/en/mcp — states only that plugin servers get + "access to the same environment variables as manually configured servers"; it + does not document full parent-environment inheritance. + +**Why the session's evidence does not carry it.** The cited measurement +(`cwd=/` + `export LNPL_IMPL` → correct `serverInfo`) was a **manual shell run** +of the server. That demonstrates shell-to-child inheritance, not what the harness +passes when it spawns the server. The claim is therefore **not substantiated** +and was **not ingested as a directive**. + +**What was ingested instead — verified.** From +https://code.claude.com/docs/en/mcp: "If a referenced environment variable isn't +set and has no default value, the config still loads: Claude Code reports a +missing-variable warning for that server in `claude mcp list` output and uses the +unexpanded `${VAR}` text as-is." So the real hazard is the opposite of the +queued one: an unset `${VAR}` is delivered to the server as the literal string +`${VAR}` rather than failing loudly. The page's directive is to keep the `env` +entry with a `:-` default (or assert the value inside the server at startup). +**Confidence: verified.** + +### 3. A send wrapper's success word is not proof the prompt was submitted + +**Claim.** Treat a tmux send helper's exit 0 / "delivered" as "the keys reached +the pane"; confirm submission by reading the pane (empty input line + the +target's working indicator), and press Enter as its own key event when a +`[Pasted text #N]` placeholder is still in the buffer. + +**Sources checked.** +- https://man7.org/linux/man-pages/man3/termios.3.html and + https://man7.org/linux/man-pages/man1/tmux.1.html — already cited by the target + page for the underlying mechanism (the tty echoes independently of the + program's `read()`; `send-keys`/`capture-pane` report nothing about + consumption). +- https://en.wikipedia.org/wiki/Bracketed-paste — already cited by + `platforms-processes-non-interactive-cli-invocation` for why a pasted block's + embedded newline is not a submit. + +**How verified.** Reproduced this session across 3 tmux worker sessions: +`send-prompt.sh` returned 0/"delivered" for two workers whose panes both sat at +`❯ [Pasted text #3]`/`#4` unsubmitted, while the run that returned "queued" and +whose follow-up `wait` reported pick-up had genuinely submitted. `Enter` sent as +a separate key event started both stuck workers immediately. + +**Confidence: verified** (mechanism doc-backed; the harness-specific exit-code +semantics field-reproduced this session). + +### 4. After an auth cutover, a session key with readers and zero writers is the missed route + +**Claim.** Having replaced an authentication mechanism, grep the retired session +key across the codebase and compare read sites to write sites. A key that is +still read but no longer written marks the route the migration missed; the old +gate's "empty config → allow" fallback makes it pass in development and refuse +everyone in production. + +**Sources checked.** +- https://github.com/OWASP/ASVS/blob/master/4.0/en/0x12-V4-Access-Control.md — + V4.1.5: "Verify that access controls fail securely including when an exception + occurs." +- https://cwe.mitre.org/data/definitions/561.html — CWE-561 Dead Code: "The + product contains dead code, which can never be executed … The surrounding code + makes it impossible for a section of code to ever be executed." A gate reading + a key no writer sets can only take its own default branch. +- https://cwe.mitre.org/data/definitions/1188.html — "Initialization of a + Resource with an Insecure Default": "the default is not secure." +- https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html + — session claims are trustworthy only where the application still writes them. + +**How verified.** All four source texts fetched and quoted this session. The +field incident is reproducible in its own repo: in `chungyak-alimi`, +`/notice/{no}` gated on `session["authed"]` with no code setting it; with +`WEB_USER` unset the suite was green, and the reproduction test failed +(303 ≠ 200) only when parameterized with `WEB_USER=admin`. + +**Confidence: verified** (principle doc-backed by ASVS/CWE; the read/write census +technique field-tested, with the failing-then-passing test as evidence). ## Existing-layer check -- Merged-main near-dup scan before consolidation: pairwise Jaccard over - title + "When this applies" across all 141 merged pages → **0 flagged pairs**; - previously merged content carries no duplication. -- Cross-PR dedup during consolidation: 10 duplicate clusters collapsed to one - canonical page each (rate limiting 8→1, call-site enumeration 7→folded into - the canonical merged in #20, stderr/exit-0 diagnostics 4→1, sysroot 2→1, - env-off-switch 2→1, completion predicates 2→1, robots.txt 2→1, - harness-mediated results 2→1, leaked artifacts 2→1, orchestration category - naming unified). Three near-pairs kept distinct after trigger comparison, - with mutual `related:` links (differential setup vs interpretation; expansion - semantics vs off-switch design; import-time tactics vs level choice). -- 24 existing pages received union-merged amendments; additions already present - in main (from #16/#20) were skipped, and all non-canonical `related:` ids - were remapped to canonical page ids (post-merge broken-link scan: 0). +Routed via `INDEX.md` → the domain indexes for `platforms`, `infrastructure`, and +`security`, then opened every page whose "load when" overlapped. + +Pages read: platforms-processes-non-interactive-cli-invocation, infrastructure-agent-orchestration-pane-delivery-confirmation, security-authn-session-vs-token, platforms-tools-version-keyed-artifact-cache + +| Candidate | Overlap found | Outcome | +|-----------|---------------|---------| +| 1 — MCP server absent from `/mcp` | No page covers plugin MCP registration. `platforms-tools-version-keyed-artifact-cache` is the nearest neighbour (a plugin update that keeps running old code) but its trigger is a stale version-keyed cache directory, not a server that never registered. Grep for `mcp` across `wiki/` hit only `backend/common/llm/context-window-budget` and `infrastructure/ci-cd/secrets-handling`, neither about server config | **New page**, cross-linked to the cache page in both directions | +| 2 — `.mcp.json` `env` | Same trigger family as candidate 1 (a plugin MCP server that does not work) | **Folded into the candidate-1 page** as two edge-case rows + one Instead-of row, with the unverified inheritance directive replaced by the sourced expansion behaviour. No second page — one case per page | +| 3 — send helper "delivered" ≠ submitted | **Already covered.** `infrastructure-agent-orchestration-pane-delivery-confirmation` owns the case (tty echo ≠ consumption; the evidence-strength table; "send the body and the submit key as separate calls"), and `platforms-processes-non-interactive-cli-invocation` already carries the `[Pasted text #1]` bracketed-paste edge case and its sources | **Merged, not created** — the genuinely new increment is the *wrapper's own exit code/status word* as false evidence, plus the queued-then-picked-up pair as a positive confirmation. Added 1 Instead-of row, 2 edge-case rows, 1 field-observation source; `last_verified` bumped to 2026-08-12 | +| 4 — retired auth gate | `security-authn-session-vs-token` is a mechanism-**choice** page (session vs JWT, revocation cost) and says nothing about a cutover's leftovers; `security-authz-resource-level-checks` covers per-resource authorization, not gate retirement | **New page**, cross-linked to both (`session-vs-token` given the reverse link) | + +Conflicts flagged: none — nothing in the merged layer contradicts these +directives. The only correction is internal to this flush (candidate 2's queued +directive, handled above and recorded in `log.md`). + +Related-links added: `platforms-tools-plugin-mcp-server-registration` ↔ +`platforms-tools-version-keyed-artifact-cache` (both directions); +`security-authn-retiring-a-replaced-auth-gate` ↔ `security-authn-session-vs-token` +(both directions); one-way to `platforms-tools-harness-mediated-tool-results`, +`infrastructure-config-environment-config`, and +`security-authz-resource-level-checks`. + +## Open-PR check + +Listed with +`gh pr list --repo choiyounggi/dev-loop --state open --search "head:knowledge/"` — +21 open `knowledge/*` heads: #79, #78, #76, #74, #73, #72, #69, #68, #66, #64, +#62, #61, #58, #57, #56, #55, #52, #51, #50, #49, #47. + +Enumerated every `wiki/` path each of the 21 heads touches and filtered for the +three candidate areas (MCP/plugin, security/authn, tmux pane + `processes/`). +Overlapping paths, and what each in-flight change actually does: + +| Open head | Overlapping path | Their change | Effect on this flush | +|-----------|------------------|--------------|----------------------| +| #51 | `pane-delivery-confirmation.md` | Adds pane **binding** rows (binding a new unit to a pane whose worker just reported done; failed-bind stage taxonomy) | Different case — no content overlap with candidate 3. Both edit the file, so the owner will resolve a `last_verified` line conflict | +| #64 | `pane-delivery-confirmation.md` | `related:` line only | No overlap | +| #57, #66 | `non-interactive-cli-invocation.md` | `related:` line only | No overlap | +| #56, #62, #76 | other `platforms/processes/` pages | Unrelated triggers (stderr diagnostics, background services, cloud CLI bounds, CLI JSON parsing) | No overlap | +| #51 | `wiki/security/index.md`, `security/data/commit-identity-in-public-repos.md` | A different security page + its index row | No overlap with `security/authn/` | + +No open head touches plugin/MCP configuration or `security/authn/` at all. + +**Per-candidate verdict:** + +| Candidate | Verdict | +|-----------|---------| +| 1 — MCP server absent from `/mcp` | **new** — no open PR carries it | +| 2 — `.mcp.json` `env` | **new**, ingested as part of candidate 1's page (corrected form) | +| 3 — send helper "delivered" ≠ submitted | **new** relative to the open PRs (none carries this content), but merged into the existing merged-main page rather than given its own — the existing-layer check, not the open-PR check, is what bounded it | +| 4 — retired auth gate | **new** — no open PR touches `security/authn/` | + +No sibling duplicate PR opened; nothing folded onto another branch; nothing +dropped as a pending duplicate. ## Routing decision -- New categories: `infrastructure/agent-orchestration` (5 pages; unified the - competing `orchestration`/`agent-orchestration` names), `databases/data-survey` - (1), `qa/deliverables` (1). All other pages route into existing categories. -- Canonical-path decisions: rate limiting → `backend/common/reliability/` - (sits beside timeouts-and-retries; 6 of 8 variants chose it); stderr - diagnostics → `platforms/processes/` (concern spans beyond shells); leaked - artifacts → `testing/data/artifact-leakage-from-a-suite`; call-site - enumeration → the existing `backend/common/change-impact/` page. -- All 38 new pages listed in their domain indexes (nearest-index rule; backend - routes via its python sub-index for bytecode-cache-staleness); INDEX.md domain - summaries updated for infrastructure/qa/databases. Full-wiki lint: frontmatter, - ids, related-links, index coverage, size, qualifiers, staleness → 0 findings. +| Insight | Target | New category? | +|---------|--------|---------------| +| 1 + 2 (MCP server registration and `env` expansion) | `platforms` / `tools` / **new page** `plugin-mcp-server-registration.md` (`platforms-tools-plugin-mcp-server-registration`) | No. `platforms` owns "commands inspected before execution / toolchain and harness behaviour", and its `tools` category already holds the sibling harness cases (`version-keyed-artifact-cache`, `harness-mediated-tool-results`). Not `infrastructure/config` — the case is a harness's plugin-loading lifecycle, not per-environment application config | +| 3 (send helper's success word) | `infrastructure` / `agent-orchestration` / **merged into** `pane-delivery-confirmation.md` | No — merge-before-create; the page's trigger already names deciding "whether the input was consumed" | +| 4 (retired auth gate) | `security` / `authn` / **new page** `retiring-a-replaced-auth-gate.md` (`security-authn-retiring-a-replaced-auth-gate`) | No. `security/authn` already owns mechanism choice and password storage; gate retirement is the same category's cutover case. Not `security/authz` — the missed check is authentication state, not per-resource permission | + +The harvested `domain:` hints were followed for all four (`platforms`, +`platforms`, `infrastructure`, `security`). + +Plumbing updated: `wiki/platforms/index.md` and `wiki/security/index.md` each get +the new page with a "load when" line enumerating its distinct uses; `log.md` has +the dated `ingest` entry, including the candidate-2 correction. Both new pages +are within the 120-line body limit (61 and 67 lines); every `related:` id +resolves to an existing page. diff --git a/log.md b/log.md index c930fc2..8a0c965 100644 --- a/log.md +++ b/log.md @@ -43,3 +43,4 @@ Append-only. Format: `## [YYYY-MM-DD] ///` | ## environment diff --git a/wiki/platforms/tools/plugin-mcp-server-registration.md b/wiki/platforms/tools/plugin-mcp-server-registration.md new file mode 100644 index 0000000..e10b935 --- /dev/null +++ b/wiki/platforms/tools/plugin-mcp-server-registration.md @@ -0,0 +1,74 @@ +--- +id: platforms-tools-plugin-mcp-server-registration +domain: platforms +category: tools +applies_to: [claude-code] +confidence: verified +sources: + - https://code.claude.com/docs/en/plugins-reference + - https://code.claude.com/docs/en/mcp + - https://github.com/modelcontextprotocol/typescript-sdk/issues/216 +last_verified: 2026-08-12 +related: [platforms-tools-version-keyed-artifact-cache, platforms-tools-harness-mediated-tool-results, infrastructure-config-environment-config] +--- + +# A Plugin-Bundled MCP Server That Does Not Appear in the Harness + +## When this applies + +You installed or edited a Claude Code plugin that bundles an MCP server and the +server is absent from `/mcp`, or its tools are not callable — while the plugin's +skills and commands work. Also when you are about to change that plugin's +`.mcp.json` shape, `command`/`args`, or `env` to make it appear. + +## Do this + +1. **Reload the plugin layer before editing any config.** Run `/reload-plugins`, + then re-open `/mcp`. A plugin's MCP servers are connected at session startup + for plugins that were already enabled; a plugin enabled, installed, or updated + mid-session keeps the session's previous plugin state until you reload. +2. **When it is still absent after a reload, read the harness's own report before + changing the file**: `claude mcp list` names the per-server failure detail and + any missing-variable warning, and `claude --debug` prints server + initialization errors. +3. **Run the installed copy by hand as the third step**, from the same working + directory the session uses, and feed it one `initialize` request on stdin. A + valid `serverInfo` response separates "the server is broken" from "the harness + never started it". +4. **Compare against a working sibling's file, not against your memory of the + schema**: `find ~/.claude/plugins/cache -maxdepth 4 -name .mcp.json`. + +Order the hypotheses by what each one explains: + +| Observation | Conclusion | +|-------------|------------| +| Server absent from `/mcp`, plugin's skills present, session predates the install/edit | The session holds the pre-change plugin state — reload | +| Server listed in `/mcp` with a failure status | The spawn or handshake failed — take the detail from `claude mcp list` / `claude --debug` | +| Server absent after a reload and a manual run answers `initialize` correctly | The config is not being read as you think — compare shape and placeholder expansion against a working sibling | +| Tools present but named unexpectedly | Plugin server tools are `mcp__plugin____`; search by that form before concluding they are missing | + +## Edge cases + +| Case | Then | +|------|------| +| The file is a bare map of server names, with no `"mcpServers"` wrapper | Both shapes load. The documented form wraps entries in `"mcpServers"`, and installed plugins ship both — treat the shape as an unlikely cause and keep looking | +| You relay a value the user already exports, as `"env": {"VAR": "${VAR}"}` | Give it a default (`${VAR:-}`) or assert on the value inside the server at startup: with no default and the variable unset, the config still loads and the server receives the literal text `${VAR}`, warning only in `claude mcp list` | +| You are tempted to drop `env` and let the child inherit the variable from your shell | Keep the entry (with a default) unless you have confirmed the inheritance for your harness version — the MCP reference stdio client passes only a fixed allowlist (`HOME`, `PATH`, `SHELL`, `TERM`, `USER`, `LOGNAME`) to the child when `env` is absent | +| The plugin was updated mid-session | Hooks, MCP servers, and LSP servers keep the previous version's `${CLAUDE_PLUGIN_ROOT}` path until `/reload-plugins`; monitors need a session restart ([platforms-tools-version-keyed-artifact-cache] owns the stale-cache case) | +| Only the plugin's slash commands are missing after a reload | The reload connects MCP servers but has not always rebuilt the command index — restart the session for commands specifically | +| The server writes logs to stdout | The harness reads stdout as protocol frames and disconnects the server, counting it as a crash — send logs to stderr | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Rewrite `.mcp.json` into the other shape because the server is missing | Run `/reload-plugins` first, then `claude mcp list` | Both shapes are accepted, so a shape edit that "fixes" it was really the reload; the true cause survives to the next install | +| Conclude the server is broken because `/mcp` does not list it | Run the installed copy by hand and feed it one `initialize` | A server that answers `initialize` was never started by the harness — that is a registration fault, not a server fault | +| Add `env` entries for variables your shell already exports, to be safe | Add them with `:-` defaults, or read the value inside the server and fail loudly when it is absent | An unexpanded `${VAR}` is delivered as literal text rather than rejected, so the server starts with a wrong value instead of not starting | + +## Sources + +- https://code.claude.com/docs/en/plugins-reference — "Changes you make to a skill's `SKILL.md` take effect immediately in the current session. Changes to the plugin's other components, such as `hooks/`, `.mcp.json`, `agents/`, and `output-styles/`, do not. Run `/reload-plugins` or restart Claude Code to pick those up"; "When a plugin updates mid-session, hook commands, monitors, MCP servers, and LSP servers keep using the previous version's path. Run `/reload-plugins` to switch hooks, MCP servers, and LSP servers to the new path; monitors require a session restart"; troubleshooting for a server whose tools do not appear ("Check the MCP server logs: `claude --debug` shows initialization errors", "Test the server manually outside of Claude Code"); stdout is read as protocol messages and non-protocol output disconnects the server +- https://code.claude.com/docs/en/mcp — "At session startup, Claude Code connects the servers for enabled plugins automatically"; "If you enable or disable a plugin during a session, run `/reload-plugins` to connect or disconnect its MCP servers"; expansion of `${VAR}` / `${VAR:-default}` in `command`, `args`, `env`, `url`, `headers`; "If a referenced environment variable isn't set and has no default value, the config still loads: Claude Code reports a missing-variable warning for that server in `claude mcp list` output and uses the unexpanded `${VAR}` text as-is"; plugin tool naming `mcp__plugin____`; `claude mcp get ` shows an `Issue:` line for a failed server +- https://github.com/modelcontextprotocol/typescript-sdk/issues/216 — the reference stdio client's `getDefaultEnvironment()` returns only `DEFAULT_INHERITED_ENV_VARS` (`HOME`, `LOGNAME`, `PATH`, `SHELL`, `TERM`, `USER` on POSIX) and a supplied `env` replaces rather than extends it; an arbitrary exported variable is not inherited by default +- Field reproduction 2026-08-12 (`lnpl-mcp` 0.3.0, macOS): install, `enabledPlugins` registration, and a manual run of the installed server (correct `serverInfo`) were all healthy while `/mcp` did not list the server; `/reload-plugins` exposed its two tools immediately. Control: in the same cache, `context7` and `playwright` ship a bare map and `claude-mem`, `figma`, and `atlassian` ship the `"mcpServers"` wrapper, and servers of both shapes were live in that session diff --git a/wiki/platforms/tools/version-keyed-artifact-cache.md b/wiki/platforms/tools/version-keyed-artifact-cache.md index 5121a1f..442d283 100644 --- a/wiki/platforms/tools/version-keyed-artifact-cache.md +++ b/wiki/platforms/tools/version-keyed-artifact-cache.md @@ -10,7 +10,7 @@ sources: - https://github.com/anthropics/claude-code/issues/17361 - https://github.com/anthropics/claude-code/issues/61954 last_verified: 2026-08-04 -related: [platforms-toolchains-version-management] +related: [platforms-toolchains-version-management, platforms-tools-plugin-mcp-server-registration] --- # Shipping New Code Through a Version-Keyed Artifact Cache diff --git a/wiki/security/authn/retiring-a-replaced-auth-gate.md b/wiki/security/authn/retiring-a-replaced-auth-gate.md new file mode 100644 index 0000000..b015e8d --- /dev/null +++ b/wiki/security/authn/retiring-a-replaced-auth-gate.md @@ -0,0 +1,81 @@ +--- +id: security-authn-retiring-a-replaced-auth-gate +domain: security +category: authn +applies_to: [general] +confidence: verified +sources: + - https://github.com/OWASP/ASVS/blob/master/4.0/en/0x12-V4-Access-Control.md + - https://cwe.mitre.org/data/definitions/561.html + - https://cwe.mitre.org/data/definitions/1188.html + - https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html +last_verified: 2026-08-12 +related: [security-authn-session-vs-token, security-authz-resource-level-checks, infrastructure-config-environment-config] +--- + +# Retiring the Old Gate After an Authentication Mechanism Changes + +## When this applies + +You replaced one authentication mechanism with another — basic-auth to member +accounts, session key A to session key B, a shared password to per-user login — +and authentication now fails or passes on only some routes or some users. Also +when you are reviewing such a cutover before it ships, or a route behaves +differently in production than in local development. + +## Do this + +1. **Census the old gate's state key across the whole codebase** before trusting + the cutover — search for every read and every write of the retired key + (`session["authed"]`, a cookie name, a request attribute): + + ```sh + grep -rn 'session\["authed"\]' --include='*.py' . # readers and writers together + ``` + +2. **Classify each key by its read/write counts.** The ratio names the defect: + +| Readers | Writers | Meaning | +|---------|---------|---------| +| ≥1 | ≥1 | Still live — decide whether this route is in scope for the cutover | +| ≥1 | 0 | The migration missed this route: it gates on a value nothing sets any more | +| 0 | ≥1 | The new gate reads something else — the write is dead and can go with the old gate | +| 0 | 0 | Fully retired | + +3. **Delete the old gate and its key in the same change that adds the new one**, + so no route is left reading a value with no writer — CWE-561 dead code + survives precisely because it looks like a working check. +4. **Give the new gate a deny default.** Missing configuration, an absent + session value, and an exception all resolve to "not authenticated" — ASVS + 4.1.5 requires access controls to fail securely including when an exception + occurs. +5. **Run the regression test in the configuration production uses.** Parameterize + the test over the environment variable that decides the old gate's fallback + and assert the authenticated outcome in both arms. + +## Edge cases + +| Case | Then | +|------|------| +| The old gate reads "config empty → allow" | This is why the bug is invisible locally: development leaves the setting unset and every request passes, while production sets it and every request is refused. Cover both arms in tests before removing the fallback (CWE-1188, an insecure default) | +| The key is written by a template, middleware, or a framework hook rather than a route | Widen the census to templates and config before declaring zero writers; `grep` the bare key name, not only the code expression | +| The old and new mechanism must run side by side during a rollout | Give each its own key and make the composite decision explicit in one place; leaving one key readable by both gates makes the retirement untestable | +| The retired mechanism gated a route you did not know existed | The zero-writer readers are the inventory — treat each hit as a route to re-gate, not as noise to clean up | +| Sessions issued under the old mechanism are still in the store | Invalidate them at cutover; a session minted under the old gate carries the old claim shape | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Declare the cutover done because login and the routes you touched work | Run the read/write census on the old key and re-gate every zero-writer reader | The routes you did not touch are exactly the ones that kept the old gate, and they pass locally | +| Leave the old key's check in place "in case something still sets it" | Remove the check with the mechanism, or point it at the new key | A check on a value with no writer is not a fallback — it is a gate whose outcome is decided entirely by its own default | +| Keep an "unset config means allow" fallback so local development stays convenient | Default to deny and supply a development credential through the same configuration path | The convenience default is the production behavior when the config load fails, which is when you need the gate most | +| Test only the environment your machine is in | Parameterize the test on the setting that changes the fallback, asserting both arms | A single-arm test proves the arm production does not run | + +## Sources + +- https://github.com/OWASP/ASVS/blob/master/4.0/en/0x12-V4-Access-Control.md — V4.1.5: "Verify that access controls fail securely including when an exception occurs"; V4.1.3 states the least-privilege requirement the retired gate stops enforcing +- https://cwe.mitre.org/data/definitions/561.html — CWE-561 Dead Code: "The product contains dead code, which can never be executed … The surrounding code makes it impossible for a section of code to ever be executed"; a gate whose key no writer sets can only take its default branch +- https://cwe.mitre.org/data/definitions/1188.html — CWE-1188, insecure default initialization: a default chosen for convenience becomes the security decision when the intended value is never supplied +- https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html — session state is server-authoritative; a claim in the session is trustworthy only where the application still writes it +- Field incident 2026-08-12 (`chungyak-alimi`, FastAPI/Starlette sessions): after a basic-auth → member-account cutover, `/notice/{no}` still gated on `session["authed"]` while no code set it any more. With `WEB_USER` unset (local) the old gate's empty-config fallback allowed every request and the suite was green; with `WEB_USER` set (deployed) logged-in members were redirected 303 instead of served 200. The reproduction test failed only when parameterized with `WEB_USER=admin` diff --git a/wiki/security/authn/session-vs-token.md b/wiki/security/authn/session-vs-token.md index a0642c9..fabd241 100644 --- a/wiki/security/authn/session-vs-token.md +++ b/wiki/security/authn/session-vs-token.md @@ -8,7 +8,7 @@ sources: - https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html - https://www.rfc-editor.org/rfc/rfc8725 last_verified: 2026-07-10 -related: [security-authz-resource-level-checks] +related: [security-authz-resource-level-checks, security-authn-retiring-a-replaced-auth-gate] --- # Choosing How Users Stay Authenticated: Server Session vs Token diff --git a/wiki/security/index.md b/wiki/security/index.md index 9c6ec2c..1fd914f 100644 --- a/wiki/security/index.md +++ b/wiki/security/index.md @@ -24,6 +24,7 @@ Match your situation to a "load when" line; load only matching pages. | Page | Load when | |------|-----------| | [session-vs-token](authn/session-vs-token.md) | Choosing how users stay authenticated (server session cookie vs JWT/stateless tokens) for a new app or API; reviewing an auth design; setting access-token lifetime against a revocation requirement (implementation → wiki/backend/common/auth/, wiki/frontend/auth/) | +| [retiring-a-replaced-auth-gate](authn/retiring-a-replaced-auth-gate.md) | Replacing one authentication mechanism with another (basic-auth → member accounts, session key A → key B) and auth now fails or passes on only some routes or users; reviewing such a cutover before it ships; a route authenticates locally but refuses or admits everyone in production; auditing a session/cookie key that code reads but nothing writes; deciding the deny default and the test arms for a new gate | | [password-storage](authn/password-storage.md) | Implementing or reviewing password hashing (argon2id/bcrypt parameters, migrating off SHA-256/MD5); tuning hash cost vs login latency; login endpoint as a hashing-DoS target; bcrypt length limits | ## authz