Map T-03-01 through T-03-SC to passing tests, record the check-phase3.sh gate, and mark nyquist_compliant after that gate exited 0. Co-authored-by: Cursor <cursoragent@cursor.com>
9.2 KiB
9.2 KiB
phase, slug, status, threats_open, asvs_level, created, verified
| phase | slug | status | threats_open | asvs_level | created | verified |
|---|---|---|---|---|---|---|
| 03 | first-vertical-slice-genres-end-to-end | verified | 0 | 1 | 2026-09-17 | 2026-09-17 |
Phase 3 — Security Review
Token verification, cross-plugin guard lookup, migration isolation, query tenant boundaries, and secret handling for
GET /_fonoteka/api/v1/genres.
Trust Boundaries
| Boundary | Description | Data Crossing |
|---|---|---|
| Config and Go module resolution → process | DSN, JWT secret, and dependency metadata enter boot | SUMMER_DATABASE__DSN, SUMMER_GOLEM15__USER__JWT__SECRET, go.mod |
| Bearer token → user lookup → handler | Untrusted JWT claims become authenticated context | Authorization header, users.id, MustChangePassword |
| Plugin declarations → ServeMux | Named middleware and group routes compile to stdlib mux | jwt.auth, inv.must-change-password, path params |
| Authenticated user and context → SQL | Active collection and album counts must stay tenant-scoped | owner_id, editor rows, collection_id, genre_id |
| CLI plugin argument → migration history | Rollback must not select another plugin's gormigrate table | --plugin, history table name |
| Recorded fixture → test request | JWT and colliding IDs come from trusted seed state | jwt:alice, id:genre / id:token / id:wishlist-album |
Threat Register
| Threat ID | Category | Severity | Component | Disposition | Mitigation | Status |
|---|---|---|---|---|---|---|
| T-03-01 | Tampering | high | migrations / locale / rollback | mitigate | Explicit DDL, per-plugin history tables, ICU pl-PL check before migrate, isolated RollbackLast |
closed |
| T-03-02 | Elevation of privilege | high | named middleware | mitigate | Missing names fail boot with plugin+name; unauthenticated requests never reach the handler; seven-stage order | closed |
| T-03-03 | Spoofing | high | JWT guard | mitigate | HS256 pinned, exp+sub required, persisted user lookup, empty secret fails boot, 401 bodies omit secrets |
closed |
| T-03-04 | Information disclosure | high | aggregate query | mitigate | Grouped owner OR editor, AND active collection; foreign albums stay at count 0 | closed |
| T-03-05 | Tampering | medium | lagoon.OrderBy / non_empty |
mitigate | Allow-listed identifiers/direction, no COLLATE, non_empty in 0|1 else 422 |
closed |
| T-03-06 | Tampering | high | parity acceptance | mitigate | Unmodified fixture, passing increments only after replay, mutated body fails, 153 pending never count as pass | closed |
| T-03-07 | Information disclosure | medium | typed route IDs | mitigate | Malformed and unknown IDs both 404; constraints compiled at registration | closed |
| T-03-SC | Tampering | medium | Go module resolution | mitigate | Official module paths already in go.mod; testcontainers added only as the named STACK test dependency |
closed |
Status: open · closed Disposition: mitigate (implementation required) · accept (documented risk) · transfer (third-party)
Findings by Threat
T-03-01 — schema drift / locale / rollback isolation
- Source:
lagoon/connection.go(Open/Use/CheckLocale),lagoon/migrations.go(HistoryTableName,Migrate,RollbackLast), Fonoteka pluginupdates(explicitCREATE TABLE, noAutoMigrate). - Test evidence:
TestWrongICULocaleFailsOpen,TestTwoPluginMigrationSetsIsolated,TestNoAutoMigrate, appTestMigrateSeedsCanonicalGenres,TestRollbackLastIsolatesFonoteka,TestPluginMigrationsDoNotUseAutoMigrate,TestGenreQueryFailsOnWrongLocale. - Finding: ICU
pl-PLis required before GORM is used. Two plugins keep separatesummer_migrations_*tables; rolling backdemo.beta(framework) orgolem15.fonoteka(app) leaves the other plugin's history and rows intact. Production code and plugin sources contain noAutoMigrate. - Disposition: closed / mitigate.
T-03-02 — missing or bypassed named guard
- Source:
surf/router.go(wrapfails on unknown names;compilewraps recover → CORS around the mux; named auth runs after locale and before org/rate/handler),plugins/golem15/fonoteka/plugin.go(Use("jwt.auth", "inv.must-change-password")). - Test evidence:
TestAssembleMissingMiddlewareFailsBoot,TestUnauthenticatedNamedGuardDoesNotReachHandler,TestPipelineOrderRecoverCORSLocaleAuthPasswordOrgRateHandler,TestCORSPreflightBypassesNamedAuth, appTestGenreSecurityBoundaries/unauthenticatedand/cors-preflight. - Finding: A missing
jwt.authname fails Assemble with plugin ID and name. OPTIONS preflight returns 204 without running named auth or the genre handler (Cache-Controlstays unset). GET without a token returns 401Token not providedand does not set the handler'sCache-Control: no-cache, private. - Disposition: closed / mitigate.
T-03-03 — JWT forgery and secret handling
- Source:
bouncer/jwt.go(WithValidMethods([]string{"HS256"}),WithExpirationRequired(), nonemptysub,UserProvider.FindByID),plugins/golem15/user/user.go(jwtSecretfails closed). - Test evidence:
TestVerifyRejectsBadTokens,TestVerifyRejectsAlgNoneEmptySecretAndAbsentExp,TestVerifyAndMiddlewareOmitTokenAndSecret,TestMiddlewareStatusBodies, appTestGenreSecurityBoundaries(malformed / expired / wrong HMAC / alg:none / bad signature / absent exp / absent sub / unknown user),TestEmptyJWTSecretFailsBoot,TestGenreListBehindJWT. - Finding:
alg:none, HS384, bad signatures, missing/expiredexp, missingsub, and unknown subjects never reach the handler. Emptygolem15.user.jwt.secretfailsBoot. 401 JSON bodies are PHP-shaped and do not echo the token or secret. - Disposition: closed / mitigate.
T-03-04 — cross-tenant album counts
- Source:
plugins/golem15/fonoteka/active_collection.go(AccessibleByMembershipgrouped OR, then ANDcollection_id),genre_handler.goleft-join count. - Test evidence:
TestGenreCountsScopedToActiveCollection,TestGenreSecurityBoundaries/alice-counts-ignore-foreign,/bob-counts-ignore-alice. - Finding: Alice's rock albums do not appear in Bob's jazz counts and vice versa. Invalid stored context falls back to the lowest-ID accessible real collection. Wishlist and foreign collections are not counted.
- Disposition: closed / mitigate.
T-03-05 — ORDER BY / non_empty injection
- Source:
lagoon/order.goallow-list,genre_handler.goparseNonEmpty. - Test evidence:
TestOrderClauseAllowList, integrationnon_empty=0|1|invalidand duplicate-key last-wins, 422 envelope. - Finding: Unknown columns and directions are rejected. No COLLATE is emitted. Invalid
non_emptyreturns the PHP 422 envelope. - Disposition: closed / mitigate.
T-03-06 — false-green parity
- Source:
parity/parity_test.go(runCorpusRouteincrements passing only after ported replay),parity/manifest.yaml(oneportedroute withseed_hook: genres), unmodifiedfixtures/routes/get_genres_jwt.yaml. - Test evidence:
TestParityCorpuscoverage subtest (154 recorded, 1 passing, 153 pending, 0 failing, 0 unrecorded),TestParityContract/honest-counts,ported-mismatch,ported-mutated-response. - Finding: Pending routes are not sent to the Go handler. A mutated
album_countfailsReplayFlow. The recorded PHP fixture still containsBearer {{jwt:alice}}and"album_count":0. - Disposition: closed / mitigate.
T-03-07 — typed ID oracle
- Source:
surf/params.go(IntParam,constrain),examples/hello/plugins/greeter/plugin.go. - Test evidence:
TestTypedIDRouteReturns404,TestWhereInRejectsOutsideEnum,TestTypedItemRoute. - Finding:
/items/nopeand/items/99both 404;/kinds/other404. Request text never builds a regex or SQL fragment. - Disposition: closed / mitigate.
T-03-SC — module path legitimacy
- Source: framework
go.mod(GORM, pgx, gormigrate, golang-jwt, testcontainers at STACK versions). - Test evidence:
go vet ./.../go test ./...in both modules; slopcheck false positives on recent versions documented in 03-RESEARCH.md. - Finding: No unofficial module path was added. testcontainers is the STACK-named test dependency used by
lagoonisolation tests and the existing appTestMain. - Disposition: closed / mitigate.
Accepted Risks Log
No accepted risks.
High-severity JWT, missing-guard, cross-tenant, migration-isolation, and false-green parity issues are mitigated with failing-when-broken tests. Token issuing remains test-only (Phase 7). Full CORS/locale/rate-limit depth remains Phase 6. Accented/punctuation Polish collation vs MariaDB is documented as a later fixture risk (T-03-05 residual, medium, not open).
Security Audit Trail
| Audit Date | Threats Total | Closed | Open | Run By |
|---|---|---|---|---|
| 2026-09-17 | 8 | 8 | 0 | gsd-executor (03-04) |
Sign-Off
- All threats have a disposition (mitigate / accept / transfer)
- Accepted risks documented in Accepted Risks Log
threats_open: 0confirmedstatus: verifiedset in frontmatter- No open high-severity JWT or cross-tenant issue remains
Approval: verified 2026-09-17