From b519c9c320e2727fd738a6f3320b6c27f49fd58f Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Mon, 12 Oct 2026 16:28:58 +0200 Subject: [PATCH] docs(phase-16.3): add security threat verification --- .../16.3-SECURITY.md | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 .planning/phases/16.3-backend-admin-personal-api-tokens-with-scopes/16.3-SECURITY.md diff --git a/.planning/phases/16.3-backend-admin-personal-api-tokens-with-scopes/16.3-SECURITY.md b/.planning/phases/16.3-backend-admin-personal-api-tokens-with-scopes/16.3-SECURITY.md new file mode 100644 index 0000000..1748d17 --- /dev/null +++ b/.planning/phases/16.3-backend-admin-personal-api-tokens-with-scopes/16.3-SECURITY.md @@ -0,0 +1,100 @@ +--- +phase: "16.3" +slug: "backend-admin-personal-api-tokens-with-scopes" +status: verified +# threats_open = count of OPEN threats at or above workflow.security_block_on severity (the blocking gate) +threats_open: 0 +asvs_level: 1 +created: "2026-10-12" +--- + +# Phase 16.3 — Security + +> Per-phase security contract: threat register, accepted risks, and audit trail. + +--- + +## Trust Boundaries + +| Boundary | Description | Data Crossing | +|----------|-------------|---------------| +| client → admin API | Authorization header (JWT or g15_ token) and cookie; untrusted | credentials | +| client → plugin routes via ResolveBackend | Bearer value only; plugin routes have no CSRF check | credentials | +| plugin code → cabana boot | plugins declare token scopes and permissions compiled at boot | scope definitions | +| operator → CLI | runs with database access; may mint for any user | plaintext token (stdout once) | +| database → process | token rows; only hashes stored | sha256 hash, 4-char hint | +| server → SPA | the 201 create response carries the plaintext once | plaintext token | +| SPA → browser storage, clipboard | plaintext stays in component memory; clipboard is user-initiated | plaintext token | +| server data → DOM | token names and plugin scope labels rendered as text | user/plugin strings | +| agent → Journal API | Bearer JWT or g15_ token; untrusted | credentials | +| executor → git origin | tags and branches read by other repositories | release refs | +| tests → databases, network | throwaway containers only; no external servers | test data | + +--- + +## Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation | Status | +|-----------|----------|-----------|----------|-------------|------------|--------| +| T-16.3-01 | Spoofing | compositeGuard token branch | high | mitigate | `wellFormedAPIToken` (cabana/api_tokens.go:59) before DB; crypto/rand mint; `subtle.ConstantTimeCompare` on hash (token_guard.go:93); owner reloaded per request | closed | +| T-16.3-02 | Information Disclosure | backend_api_tokens | medium | mitigate | `TokenHash` is `json:"-"`, only `token_hint` exposed (api_tokens.go:29-30) | closed | +| T-16.3-03 | Information Disclosure | create response, logs | high | mitigate | `logTokenAuth` logs outcome, method, path, remote, admin_id, token_id only — no Authorization value (token_guard.go:226-247) | closed | +| T-16.3-04 | Elevation of Privilege | cabana.Allows | high | mitigate | Scoped branch runs before the IsSuperuser/empty shortcut (contracts.go:201-206); `allowsUnderScope` + `ownerHas` | closed | +| T-16.3-05 | Elevation of Privilege | account, token, refresh, logout routes | high | mitigate | `interactiveOnly` 403 `interactive_only` (token_guard.go:151), wrapped outside `requireAjax` (http.go:254) | closed | +| T-16.3-06 | Spoofing / Tampering (CSRF) | token transport, ResolveBackend | high | mitigate | Token branch reads only `Authorization: Bearer` (token_guard.go:141); `ResolveBackend` requires a Bearer and uses the cookie-less resolver (token_guard.go:196) | closed | +| T-16.3-07 | Elevation of Privilege | owner loading | medium | mitigate | `g.users.FindByID` per request via `lazyBackendUsers` (token_guard.go:101, http.go:231) | closed | +| T-16.3-08 | Elevation of Privilege | issueAPIToken (API and CLI) | medium | mitigate | `issueAPIToken` runs `validateTokenRequest` against the registry before insert (api_tokens.go:169) | closed | +| T-16.3-09 | Denial of Service | last_used_at writes | low | mitigate | `stampThrottle` + conditional UPDATE (api_tokens.go:267-273) | closed | +| T-16.3-10 | Denial of Service | invalid token floods | low | accept | See Accepted Risks AR-01 | closed | +| T-16.3-11 | Tampering | scope registration | medium | mitigate | Boot refuses reserved, invalid-method and malformed scopes (token_scopes.go:114, :138) | closed | +| T-16.3-12 | Repudiation | revoke | low | mitigate | Soft revoke stores `revoked_at`/`revoked_by` (api_tokens.go:220; CLI nil at token_commands.go:193); `admin.auth` events | closed | +| T-16.3-13 | Elevation of Privilege | routes outside Allows | high | mitigate | `tokenRouteGate` on field types and markdown preview (field_type.go:870, field_markdown.go:77); route classification test token_routes_test.go | closed | +| T-16.3-14 | Information Disclosure | ApiTokenCreateDialog plaintext | high | mitigate | Dialog-local `plaintext` ref cleared on close (line 87) and `onBeforeUnmount` (line 345); no storage/console/v-html/router use in components/account | closed | +| T-16.3-15 | Information Disclosure | show-once step | low | accept | See Accepted Risks AR-02 | closed | +| T-16.3-16 | Tampering (XSS) | token names, scope labels | medium | mitigate | No `v-html` in components/account; text interpolation only | closed | +| T-16.3-17 | Elevation of Privilege | scope picker | low | mitigate | SPA reads `GET /account/tokens/scopes`; server re-validates on create (T-16.3-08) | closed | +| T-16.3-18 | Elevation of Privilege | Journal ownership branches | high | mitigate | sm-journal-plugin controllers/api/auth.go: every principal from `cabana.ResolveBackend(app, r, "golem15.journal.*")`; journal_api_tokens_test.go matrix | closed | +| T-16.3-19 | Elevation of Privilege | journal:read on write routes | high | mitigate | `journal:read` declares GET/HEAD only (journal token_scopes.go:23); guard `methodFilterScopes` on `r.Method` (token_guard.go:110, token_scopes.go:378) | closed | +| T-16.3-20 | Spoofing / Tampering (CSRF) | Journal write routes | high | mitigate | Journal uses Bearer-only `ResolveBackend` (see T-16.3-06) | closed | +| T-16.3-21 | Information Disclosure | Journal error bodies | low | mitigate | `resolveError` writes only the PHP constants (`Insufficient permissions` / auth required) | closed | +| T-16.3-22 | Tampering / Repudiation | tag and pushes | high | mitigate | Human release checkpoint in 16.3-03; v0.1.21 tagged after v0.1.20, no moved tags | closed | +| T-16.3-23 | Elevation of Privilege | code-declared role grants on Journal API | low | accept | See Accepted Risks AR-03 | closed | +| T-16.3-24 | Repudiation (false green) | phase gate | medium | mitigate | scripts/check-phase16.3.sh fail-closed detectors, `--self-test`, NOT RUN postgres reporting | closed | +| T-16.3-25 | Tampering (security regression) | Allows, guard, route gates, plaintext | high | mitigate | token_scopes/guard/routes/account/commands tests, SPA backstops, Journal token matrix in the gate | closed | +| T-16.3-26 | Tampering | test isolation | low | mitigate | Stubs, httptest, happy-dom; testcontainers for Postgres | closed | +| T-16.3-SC | Tampering | npm/pip/cargo installs | high | mitigate | No packages installed in any plan; committed lockfiles and go.sum; swag pinned v1.16.6 | closed | + +*Status: open · closed · open — below high threshold (non-blocking)* +*Severity: critical > high > medium > low — only open threats at or above workflow.security_block_on count toward threats_open* +*Disposition: mitigate (implementation required) · accept (documented risk) · transfer (third-party)* + +--- + +## Accepted Risks Log + +| Risk ID | Threat Ref | Rationale | Accepted By | Date | +|---------|------------|-----------|-------------|------| +| AR-01 | T-16.3-10 | Format check precedes an indexed unique lookup; 160-bit secrets are not guessable; the admin API has no throttle bucket beyond login, so no new one is added | Plan 16.3-01 threat model (user-approved plan) | 2026-10-12 | +| AR-02 | T-16.3-15 | Full token shown once by design (UI-SPEC §5) so the user can verify it; overlay clicks cannot close the step, closing before copying asks first | Plan 16.3-02 threat model (user-approved plan) | 2026-10-12 | +| AR-03 | T-16.3-23 | Honouring code-declared role grants on the Journal API is the intended fix (the admin API already honours them); documented in the Journal README as a behaviour change | Plan 16.3-03 threat model (user-approved plan) | 2026-10-12 | + +*Accepted risks do not resurface in future audit runs.* + +--- + +## Security Audit Trail + +| Audit Date | Threats Total | Closed | Open | Run By | +|------------|---------------|--------|------|--------| +| 2026-10-12 | 27 | 27 | 0 | msd-secure-phase (orchestrator, ASVS L1 grep-depth; auditor skipped per short-circuit rule) | + +--- + +## Sign-Off + +- [x] All threats have a disposition (mitigate / accept / transfer) +- [x] Accepted risks documented in Accepted Risks Log +- [x] `threats_open: 0` confirmed +- [x] `status: verified` set in frontmatter + +**Approval:** verified 2026-10-12