diff options
Diffstat (limited to 'docs/third-review.md')
| -rw-r--r-- | docs/third-review.md | 61 |
1 files changed, 36 insertions, 25 deletions
diff --git a/docs/third-review.md b/docs/third-review.md index 6839ef7..531f73e 100644 --- a/docs/third-review.md +++ b/docs/third-review.md @@ -75,7 +75,7 @@ and a few of the second-review fixes did not reach every path. running SPA. None of this breaks the architecture. The cryptographic core and the trust model -are unchanged and still sound. Every finding in this review (H1, H2, M1–M6) was +are unchanged and still sound. H1, H2 and M1–M5 were fixed on 2026-09-01; what is left is the L-list — opportunistic hardening, not a hole — plus verifying the SPA CSP (M5) against the live app. @@ -502,24 +502,32 @@ hashes to the `<script>` tags in `_HTML` (the content hash is already computed). --- -### M6 — `add_group_member` accepts node-scoped tokens +### M6 — `add_group_member` accepts node-scoped tokens — WITHDRAWN -> **Fixed 2026-09-01.** Dependency changed to `require_user_scope`. The -> regression test `test_node_auth.py::test_node_scope_blocks_add_member` already -> existed and was red on `main`; it now passes. - -**Location:** `api/groups.py:585-619` +> **Not a finding. Fixed then reverted 2026-09-01.** +> +> `add_group_member` accepting a node-scoped token is **deliberate** (commit +> `0443cf8`): the node calls `POST /v1/groups/{id}/members/{username}` after a +> CLI `member invite` so the group becomes visible in the invitee's SPA, and it +> authenticates with a node-scoped token. The `group.admin_id == caller` check +> is the real guard — a node can only touch its own operator's groups, adding an +> already-registered account. +> +> This review misread that as drift (a stale test, +> `test_node_scope_blocks_add_member`, asserted the opposite and had been left +> red on `main`). Tightening the dependency to `require_user_scope` **broke the +> CLI invite → hub-membership flow**: `ops.create_invite` swallows the resulting +> 403 with a `log.warning`, so an invited user silently never appears in +> `group_members` and the group is invisible to them. Found in live testing. +> The dependency is back on `get_current_user` and the stale test now asserts +> the intended behaviour (node token may add to its own group, 403 for a group +> it does not own). -Every mutating group endpoint depends on `require_user_scope` — except -`POST /v1/groups/{group_id}/members/{username}`, which depended on -`get_current_user`. `deps.py` states plainly: "Node-scoped tokens CANNOT -create/delete groups or manage membership." This one could: a node-scoped daemon -token (or a stolen one) whose subject owns the group could add any existing user -to it. Impact was bounded (own groups, existing accounts only), but it -contradicted NS7 ("operator manages groups from the browser only") and was the -same class as the second review's M9. The test asserting the intended behaviour -was already in the tree — the endpoint had simply drifted off the shared -dependency. +The original M6 concern — a stolen node token adding accounts to the operator's +own groups — is real but low: bounded to the operator's groups, existing +accounts only, and it is now a *required* capability. If it is ever worth +constraining, it needs a dedicated node→membership path, not a scope block on +this shared endpoint. --- @@ -636,9 +644,9 @@ Against the v6 §4 claims, updated for this review: **One-sentence version:** *the E2E story between browser and node is now genuinely mutual and covers every path, the second review's critical gaps are -closed, and every finding in this review (H1, H2, M1–M6) was fixed the day it was -written — leaving the L-list as opportunistic hardening and one thing to verify: -the SPA's new CSP against the live app.* +closed, and H1/H2/M1–M5 were fixed the day this was written (M6 was withdrawn as +a misread of a deliberate design) — leaving the L-list as opportunistic +hardening and one thing to verify: the SPA's new CSP against the live app.* --- @@ -653,7 +661,7 @@ the SPA's new CSP against the live app.* | M3 | Link-preview SSRF: no rate limit, ports open, rebinding | Medium | M | ✅ **fixed 2026-09-01** — rate limit + port allowlist + connect-address re-check + bomb guard | | M4 | Federation: peer over-trust, `aud` unchecked, revoke no-op | Medium | M | ✅ **fixed 2026-09-01** — source bound to signer, push capped, revoke prunes the peer's own entries, replay rejected | | M5 | No CSP / security headers on the SPA | Medium | S | ✅ **fixed 2026-09-01** — CSP + `nosniff` + `frame-ancestors` middleware; verify against the live SPA | -| M6 | `add_group_member` accepts node tokens | Medium | S | ✅ **fixed 2026-09-01** — dependency → `require_user_scope` | +| M6 | `add_group_member` accepts node tokens | — | — | ⛔ **withdrawn** — deliberate (CLI invite flow, commit 0443cf8); the "fix" broke it and was reverted | | L1 | Relay registration no PoP | Low | S | If/when the relay registry is used | | L2 | Orphaned `replication.py` / `revocation.py` | Low | S | Delete now | | L3–L11 | See §5 | Low | S | Opportunistic | @@ -695,8 +703,11 @@ federation peer, and a missing header policy. None of them required exotic capability, and none of them were architectural — they were the cost of adding six subsystems faster than the authorization model grew to cover them. -Every finding in this review (H1, H2, M1–M6) was fixed the day it was written. -What is left is the L-list — opportunistic hardening — and one verification: the -SPA's new CSP (M5) against the running app, since a mis-tuned CSP shows as a -blank page. On a build whose honest claims are now strong and largely +H1, H2 and M1–M5 were fixed the day this was written; M6 was withdrawn — it +misread the node registering a hub membership during the CLI invite flow +(deliberate, commit `0443cf8`) as authorization drift, and the "fix" broke that +flow in live testing. What is left is the L-list — opportunistic hardening — and +one verification: the SPA's new CSP (M5) against the running app, since a +mis-tuned CSP shows as a blank page. On a build whose honest claims are now +strong and largely defensible. |