--- phase: "13" reviewed: "2026-10-03" reviewer: "gsd-executor, plan 13-06 (self-performed code-and-test review of plans 13-01 to 13-06; no reviewer agent was spawned, per the 08-10 precedent)" threats_open: 0 gate: "scripts/check-phase13.sh --all" removal_harness: "scripts/check-phase13.sh --removal" --- # Phase 13 Security Review This is a code-and-test review of every threat in the registers of Plans 13-01 to 13-06: every id of the form T-13- (T-13-01 to T-13-36) and T-13-SC. Severity and disposition are copied from the originating plan. T-13-SC is declared by every plan and is listed once with the strictest entry: 13-04, medium, mitigate (the direct `golang.org/x/text` requirement). The executor performed the review itself, as plan 08-10 did, because no separate reviewer agent was spawned. A high threat counts as mitigated only when its named test fails with the protection removed. `scripts/check-phase13.sh --removal` does this for every high mitigated threat, and for several medium ones: - it refuses a file with uncommitted changes; - it applies an anchor-exact mutation that removes the protection; - it runs the named test and requires it to fail on an assertion (a build failure does not count); - it restores the file byte for byte, checked with `cmp`. The results are under "Removal checks". The two accepted threats keep their rationale from their originating plans. The review found one defect in Phase 13 code, fixed in plan 13-06 with a failing-when-broken test (see "Fixes made during the review"): the CSV export dropped a database error that struck after its headers were sent, with no log line. It also fixed one stale test: the Phase 9 admin route inventory, which predated the Phase 12.2 cabana routes, all of which carry the backend guard. Commands run from `summercms.go`; `../fonoteka.go` tests run inside that repository. Gate stages are modes of `scripts/check-phase13.sh`. `TestPhase13Threats/T-13-NN` is the subtest of `../fonoteka.go/plugins/golem15/fonoteka/phase13_security_test.go` for that threat. | Threat | Category | Component | Severity | Disposition | Production mitigation | Test or gate stage | Observed result | Residual risk | |--------|----------|-----------|----------|-------------|-----------------------|--------------------|-----------------|---------------| | T-13-01 | Information Disclosure | token guessing / enumeration | high | mitigate | `classes/public_share.go` `PubfailCounter.Begin`: 10 failed resolutions per client address per 60 s, shared by all six public routes of both kinds, checked before any query, the slot reserved so concurrent failures cannot overshoot; the 16-symbol shape check before any lookup; inline `throttle:10,1` on the resolves and the per-token and per-IP buckets on the albums routes | `TestPhase13Threats/T-13-01` (ten failures through the albums routes lock the address; a valid token from it is then 429, another address 200), `TestPubfailCounter`, `TestPublicBucketsPerRoute`, `TestRouteTablePhase13`, `TestFonotekaNuxtFlows` (public-pubfail) | pass; removal checks RC-08 (Begin never throttles) and RC-09 (TooMany always false) fail | The counter is per process (13-05 decision): N instances behind a balancer allow N x 10 guesses per minute; a 16-symbol token has about 95 bits | | T-13-02 | Information Disclosure | public serializer and facets | high | mitigate | `classes/serialize_public_album.go` `PublicAlbumDTO` is its own twelve-key type; the public search drops rating and price sorts, refuses the rating filter with 422, searches only the public text fields and re-gates engine ids to the shared collection; zero-count facets are dropped; no reservation is loaded | `TestPhase13Threats/T-13-02` (exactly the twelve keys in PHP order, none of shelf, notes, barcode, Discogs id, price, condition, reservation or rating; rating filter 422), `TestPublicAlbumFieldSet`, `TestPublicAlbumsIndex`, `TestPublicAlbumsEngine` | pass; removal check RC-20 (the DTO gains `shelf`) fails | None known | | T-13-03 | Spoofing | disabled or regenerated share | high | mitigate | `classes/share_service.go` `ResolvePublic` requires `public_enabled`, the route's kind and the exact current token | `TestPhase13Threats/T-13-03` (a disabled share and a regenerated share's old token answer the public 404; the new token 200), `TestPublicResolve`, `TestFonotekaNuxtFlows` (public-anonymous) | pass; removal check RC-06 (`public_enabled` dropped from the lookup) fails | None known | | T-13-04 | Elevation of Privilege | reserve, cancel, reveal | high | mitigate | `classes/reservations.go` `ReserveAlbum` refuses the owner (422) and a disabled wishlist (409) and locks the album row; `CancelReservation` deletes only the caller's own row; reveal looks the album up in the caller's own wishlist | `TestPhase13Threats/T-13-04` (a peer reserves; the owner cannot; another peer cannot cancel; the reserver cannot reveal; an outsider cannot reach the album; the reservation is unchanged), `TestReserveConcurrent`, `TestRevealIdempotent` | pass; removal check RC-23 (cancel without the user condition) fails | None known | | T-13-05 | Information Disclosure | reservation mask in every serializer path | high | mitigate | `ReservationStateForViewer` answers the owner `reserved`, `is_mine:false`, `revealed:false` and no `reserved_by` until the reveal, on show, index and peer routes | `TestPhase13Threats/T-13-05` (owner show and index carry neither `reserved_by` nor the reserver's name; the reserver sees herself), `TestReservationMask`, `TestPhase13ReservationMask`, `TestWishlistOwnListAndShow` | pass; removal check RC-05 fails | None known | | T-13-06 | Information Disclosure / Tampering | peer wishlists, subscriptions, wishlist album ids | high | mitigate | `ReservableWishlistIDs`, `WishlistsVisibleTo` and `ActiveWishlist` scope every lookup; a foreign and a missing id answer the same Winter 404 page | `TestPhase13Threats/T-13-06` (an outsider's peer albums, peer album, collection subscribe and own-wishlist album lookups are identical to a missing id; no subscription is written), `TestWishlistSubscriptions` | pass; removal check RC-24 (the visible-wishlist scope dropped from collection subscriptions) fails | None known | | T-13-07 | Elevation of Privilege | token group | high | mitigate | `routes.go` mounts on `/api/v1/fonoteka` only the four wishlist CRUD routes and the export, each with exactly one `inv.scope` | `TestRouteTablePhase13` (58 routes on routes.php's groups with their lines, one scope per token route, Phase 14 routes absent), `TestPhase13Threats/T-13-07` (social, credential, notification, import and public paths unmounted for a token; a read-only token cannot write) | pass; removal check RC-22 (a second `inv.scope:read` on the token wishlist list) fails | None known | | T-13-08 | Information Disclosure | credential responses, logs, marshal | high | mitigate | `lagoon.Encrypted` columns with `json:"-"` models; show selects provider, model and base_url only; nothing logs a secret | `TestPhase13Threats/T-13-08` (store and show of AI, organisation AI and Discogs credentials plus me/context: no secret in any body, in the captured logs or in the stored columns), `TestCredentialSecretsNeverSerialized` | pass; removal check RC-21 (show echoes the key) fails | Whoever holds the app key can decrypt stored secrets | | T-13-09 | Elevation of Privilege | org credential store/destroy, Discogs shared | high | mitigate | `controllers/api/credentials_controller.go` `mayManageOrg` (after provisioning an org-less caller) refuses with 403 `{"error":"Forbidden"}` or `shared_forbidden`, writing nothing | `TestPhase13Threats/T-13-09` (a plain member's store, delete and shared Discogs store are 403 and leave the owner's key and no Discogs mirror), `TestCredentialsCRUD`, `TestDiscogsSharedMirror` | pass; removal check RC-25 (store without the manage check) fails | None known | | T-13-10 | Tampering | credential owner FK mass assignment | high | mitigate | `classes/credential_write_service.go` fills only `CredentialFillFields` (provider, model, base_url); the owner keys come from the session on create and update | `FuzzWriteEndpoints` (the 29 Phase 13 write routes beside the 41 of Phase 12, every server-owned key hostile, Postgres snapshots), `TestPhase13Threats/T-13-10` (create and update with a hostile `user_id` and `organisation_id`) | pass; removal checks RC-15 (FuzzWriteEndpoints) and RC-16 (T-13-10) fail with `user_id` in the fill list | None known | | T-13-11 | Tampering | base_url as SSRF target | medium | accept | Phase 13 only stores `base_url` (`nullable|url`); no outbound call exists until Phase 14, which owns the guarded client (INTG-02). | none (accepted) | accepted | Phase 14 must route the AI client through fetchguard | | T-13-12 | Elevation of Privilege | import routes by id | high | mitigate | `classes/csv_import_service.go` `CsvImportFor` requires `user_id` = caller and an accessible collection | `TestPhase13Threats/T-13-12` (an own import on an unreachable collection and another user's import answer the missing-id body; cancel too), `TestCsvImportScope` | pass; removal check RC-13 (the accessible-collection check dropped) fails | None known | | T-13-13 | Information Disclosure | CSV upload storage | high | mitigate | `controllers/api/csv_import_controller.go` writes to the private bucket from `golem15.fonoteka.csv.bucket_url` under server keys `fonoteka-csv//.csv`, never the public uploads bucket | `TestPhase13Threats/T-13-13` (a `../../evil.csv` upload lands under the user's prefix in the private directory and not in the public bucket), `TestCsvStoreAndShow` | pass; removal check RC-26 (the upload bucket used instead) fails | None known | | T-13-14 | Tampering | exported cells | medium | mitigate | `classes/csv` `FormulaSafe` prefixes an apostrophe to cells PHP's `FORMULA_PATTERN` flags; since 13-06 a failure after the headers is logged | `TestPhase13Threats/T-13-14`, `TestCsvExport`, `TestPHPFputcsv`, `TestPhase13HandlersFailClosed` (export-stream-failure) | pass | A failure mid-stream can only cut the file short, as in PHP | | T-13-15 | Denial of Service | CSV parse | medium | mitigate | 5 MiB cap, 5000 rows, NUL rejection, `throttle:10,1` on store | `TestPhase13Threats/T-13-15` (NUL and 5001 rows refused, no import left), `TestPhase13Boundaries` (5242880 vs 5242881 bytes, 5000 vs 5001 rows), `TestCsvParserTruthTable` | pass | None known | | T-13-16 | Tampering | row edit Discogs pick | high | mitigate | Candidate allow-list; the Phase 13 `ReleaseFetcher` always fails, so a pick answers `discogs_unavailable` and writes nothing | `TestPhase13Threats/T-13-16` (with Discogs allowed for the caller, a candidate pick is 422 and the row unchanged), `TestCsvRowPickSeam` | pass; removal check RC-18 (the fetcher returns data) fails | Phase 14 installs the real fetcher | | T-13-17 | Tampering | double commit / double writer | high | mitigate | `CommitCsvImport` is one `UPDATE ... WHERE status = 'preview'`; only the winner dispatches; a replay answers the existing job | `TestPhase13Threats/T-13-17` (two commits answer one job id; one import job exists), `TestCsvCommitCAS` | pass; removal check RC-14 (the status condition dropped) fails | None known | | T-13-18 | Elevation of Privilege | onboarding bootstrap | high | mitigate | `classes/onboarding.go` `BootstrapOwner`: 409 before validation when any live user exists, then an advisory transaction lock and a recount under it; users.email unique | `TestPhase13Threats/T-13-18` (a bootstrap waiting on the held lock answers 409 and writes nothing once a user appears), `TestBootstrapConcurrent` | pass; removal check RC-12 (the in-transaction recount dropped) fails | None known | | T-13-19 | Spoofing | register listener invitation match | high | mitigate | `HandleRegisterEvent` holds a registrant only for a pending, unexpired, unrevoked, unaccepted invitation whose trimmed lowercased e-mail matches theirs | `TestPhase13Threats/T-13-19` (pending held; expired, revoked, accepted and another address not), `TestRegisterInvitationListener` | pass; removal check RC-27 (the e-mail condition made always true) fails | None known | | T-13-20 | Information Disclosure | RegisterEvent.Payload | high | mitigate | sm-user-plugin `controllers/registration.go` `FireRegisterEvent` deletes `password` and `password_confirmation` from its copy of the input | `TestRegisterEventPayload`, `TestRegisterUserExports`, `TestPhase13Threats/T-13-20` (the bootstrap's register event carries the e-mail and no password key or value) | pass; removal checks RC-10 (TestRegisterEventPayload) and RC-11 (TestPhase13Threats/T-13-20) fail without the confirmation delete | None known | | T-13-21 | Information Disclosure / Tampering | notifications read and mark-read | medium | mitigate | `classes/notifications.go` scopes every query by `user_id`; zero updated rows is the Winter 404 page | `TestPhase13Threats/T-13-21` (another user's mark-read is 404 and leaves the row unread; read-all and the list never touch it), `TestNotificationsRoutes`, `TestPhase13Boundaries` (the 50-row cap) | pass; removal check RC-17 (mark-read without the user condition) fails | None known | | T-13-22 | Denial of Service / Repudiation | conga unregistered kinds | high | mitigate | `modules/conga/conga.go` `clientFor` sends unregistered kinds through the insert-only client and, while a worker runs, refuses a queue it serves | `TestUnregisteredKindWithWorker`, `TestUnregisteredKindRefusalAndDelay`, `TestJobContractDispatchWhileWorkerRuns` | pass; removal check RC-04 (unregistered kinds through the worker client) fails | None known | | T-13-23 | Elevation of Privilege | surf overlap dispatch | high | mitigate | `modules/surf/overlap.go` tries family members in registration order on their literals and constraints; each member runs its own wrapped chain; no match is 404 | `TestOverlappingConstrainedRoutes`, `TestOverlapConstraintFallsThrough`, `TestRouteTablePhase13` (the four routes.php pairs through the real handlers, 404 and 405), `TestPhase13Threats/T-13-23` (a constraint miss is the router's 404 before any member's guard) | pass; removal checks RC-01 (constraints skipped), RC-02 and RC-03 (literals skipped) fail | None known | | T-13-24 | Repudiation | tide date and publication masks | medium | mitigate | Each mask checks the masked value's shape and leaves every other path visible | `TestNormalizeContentDispositionDate`, `TestNormalizeNotificationPublication`, `TestNormalizePhase13Edges` (quoted and RFC 5987 names, three dates, a changed date count, a captured id beside the masked one) | pass | None known | | T-13-25 | Tampering | lagoon prohibited rule | medium | mitigate | Laravel 9 semantics (`!validateRequired`), not implicit | `TestValidateRequestProhibited`, `TestValidateRequestProhibitedNested` (wildcards, dotted paths, after bail), `FuzzWriteEndpoints` (wishlist writes never persist condition or shelf) | pass | None known | | T-13-26 | Information Disclosure | php_parity.sh rows and share capture | medium | mitigate | `rows` accepts one read-only SELECT under sqlite3 `-readonly -safe`; share tokens are captured as `{{share:wishlist}}` | `TestCheckCorpusPortedCaseStatus`; stage `check-phase13.sh --parity` (`check_corpus --require-recorded --check-secrets`) | pass | None known | | T-13-27 | Information Disclosure | invitation inspection | medium | mitigate | `InspectInvitation` reveals the collection name only for the sha256 of a pending invitation's exact token | `TestPhase13Threats/T-13-27` (expired, upper-cased, truncated and hashed tokens answer unavailable), `TestInspectInvitation` | pass | Shares the anonymous `throttle:10,1` budget (T-13-32) | | T-13-28 | Tampering / Repudiation | item-added and purchase side effects | medium | mitigate | Bell rows, the digest upsert (`RETURNING xmax = 0`, dispatch on insert only) and mail enqueues run on the write transaction; publications after commit | `TestPhase13Threats/T-13-28` (three items, one digest row at 3 and one digest job per subscriber), `TestDigestCoalescing`, `TestPurchaseMailAfterCommit` | pass; removal check RC-19 (dispatch on every upsert) fails | None known | | T-13-29 | Information Disclosure | subscribe by token | medium | mitigate | `ResolvePublic` with kind wishlist and a constant-time exact token compare after the case-insensitive lookup | `TestPhase13Threats/T-13-29` (a case-flipped, a disabled and a regenerated token are 404 and write nothing; the current token 201), `TestWishlistSubscriptions` | pass; removal check RC-07 (the compare made case-insensitive) fails | None known | | T-13-30 | Information Disclosure | purchase mail job args and logs | medium | mitigate | `WishlistPurchasedMailArgs` holds the subscriber id and two names; the worker logs the id only | `TestPhase13Threats/T-13-30` (args keys exactly album_name, subscriber_id, wishlist_name; no address), `TestPurchaseSideEffects` | pass | None known | | T-13-31 | Repudiation | queued jobs without workers | medium | mitigate | The 13-01 contract's unserved queues; cancel stops summer_jobs rows and River jobs | `TestPhase13Threats/T-13-31` (match and import jobs available on their queues, unattempted; both cancelled), `TestCsvJobRows`, `TestCsvCancel` | pass | Phase 14 registers the workers | | T-13-32 | Denial of Service | shared anonymous inline budget | low | accept | PHP shares one `throttle:10,1` guest key across onboarding, inspection and public resolves; Go mirrors it (`inline:domainless|ClientIP`) for parity; documented in parity/README.md. | none (accepted) | accepted | One address's onboarding traffic counts against its public resolves | | T-13-33 | Elevation of Privilege | public route kind confusion | medium | mitigate | Every public handler passes its route's kind to `ResolvePublic` | `TestPhase13Threats/T-13-33` (a wishlist token on `public/` and a collection token on `public-wishlist/` answer 404; each on its own route 200), `TestPublicResolve`, `TestPublicAlbumsIndex` | pass | None known | | T-13-34 | Tampering | gate --removal leaving mutated source | medium | mitigate | `check-phase13.sh --removal` refuses a dirty target file, mutates by exact anchor, restores in a `finally` and on SIGINT/SIGTERM, and checks the restore with `cmp`; it is not part of `--all` | `check-phase13.sh --self-test` (dirty file, non-unique anchor, build failure, surviving mutation, byte-identical restore) | pass; removal check RC-31 (the dirty-file refusal disabled) fails the self-test | None known | | T-13-35 | Repudiation | security review claims without evidence | medium | mitigate | `check-phase13.sh --evidence` refuses a threat without one review row copying its strictest severity and disposition, a mitigated threat naming a test the `--named` stage does not run, and a high mitigated threat without a removal row | `check-phase13.sh --self-test` (missing row, wrong disposition, missing removal row, unrun test, pending row, missing Wave 0 flag, unnamed validation test) | pass; removal check RC-30 (the removal-row requirement disabled) fails the self-test | None known | | T-13-36 | Information Disclosure | fuzz seed corpus | low | mitigate | The 70 seeds hold synthetic values only; the gate scans `testdata/fuzz` for 64-hex values, `inv_` tokens, JWTs and bearer headers | `check-phase13.sh --parity` (corpus scan), `check-phase13.sh --self-test` (each planted shape refused) | pass; removal check RC-29 (the scan never fails) fails the self-test | None known | | T-13-SC | Tampering | package installs (golang.org/x/text v0.42.0) | medium | mitigate | `golang.org/x/text` is a direct requirement of the plugin module at v0.42.0, already in the graph through go-i18n, pinned by go.sum; plans 13-01 to 13-03, 13-05 and 13-06 add no dependency | `check-phase13.sh --go` (module pin), `check-phase13.sh --self-test` | pass; removal check RC-28 (the pin lookup replaced by a fixed line) fails the self-test | None known | ## Removal checks Each row is one anchor-exact mutation from `scripts/check-phase13.sh --removal`. The anchor occurs exactly once in the file. The test must fail on an assertion, not a build failure. The file is restored byte for byte and checked with `cmp`. The script rows mutate a copy of the gate and run its `--self-test`. The run of 2026-10-03 passed every row (31 of 31). | Check | Threat | File | Anchor removed or changed | Replacement | Test run | Observed | |-------|--------|------|---------------------------|-------------|----------|----------| | RC-01 | T-13-23 | `modules/surf/overlap.go` | the constraint check in `familyDispatch.ServeHTTP` | removed | `go test ./modules/surf -run '^TestOverlapConstraintFallsThrough$'` | fails: TestOverlapConstraintFallsThrough; restored, cmp ok | | RC-02 | T-13-23 | `modules/surf/overlap.go` | the literal check in `familyMember.pathMatches` | `if false && ...` | `go test ./modules/surf -run '^TestOverlappingConstrainedRoutes$'` | fails: TestOverlappingConstrainedRoutes, TestOverlappingConstrainedRoutes/dispatch, TestOverlappingConstrainedRoutes/constraint-404 (+more); restored, cmp ok | | RC-03 | T-13-23 | `modules/surf/overlap.go` | the literal check in `familyMember.pathMatches` | `if false && ...` | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestRouteTablePhase13$'` | fails: TestRouteTablePhase13, TestRouteTablePhase13/numeric-path-parameters, TestRouteTablePhase13/phase14-routes-answer-404 (+more); restored, cmp ok | | RC-04 | T-13-22 | `modules/conga/conga.go` | the insert-only client for unregistered kinds in `clientFor` | `return m.insertClient()` (the worker client) | `go test ./modules/conga -run '^TestUnregisteredKindWithWorker$'` | fails: TestUnregisteredKindWithWorker, TestUnregisteredKindWithWorker/dispatch-unregistered, TestUnregisteredKindWithWorker/enqueue-delayed; restored, cmp ok | | RC-05 | T-13-05 | `../fonoteka.go/plugins/golem15/fonoteka/classes/reservations.go` | `if rc.IsOwner && !revealed {` in `ReservationStateForViewer` | `if false && ...` | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-05$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-05; restored, cmp ok | | RC-06 | T-13-03 | `../fonoteka.go/plugins/golem15/fonoteka/classes/share_service.go` | `public_enabled = ?` in `ResolvePublic` | the bound `true` alone | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-03$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-03; restored, cmp ok | | RC-07 | T-13-29 | `../fonoteka.go/plugins/golem15/fonoteka/classes/share_service.go` | the exact constant-time compare in `ResolvePublic` | a compare of the lower-cased tokens | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-29$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-29; restored, cmp ok | | RC-08 | T-13-01 | `../fonoteka.go/plugins/golem15/fonoteka/classes/public_share.go` | the limit check in `PubfailCounter.Begin` | `if false && ...` | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-01$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-01; restored, cmp ok | | RC-09 | T-13-01 | `../fonoteka.go/plugins/golem15/fonoteka/classes/public_share.go` | `too := w.hits+w.inflight >= c.limit` in `TooMany` | `false && ...` | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPubfailCounter$'` | fails: TestPubfailCounter, TestPubfailCounter/boundary, TestPubfailCounter/window (+more); restored, cmp ok | | RC-10 | T-13-20 | `../fonoteka.go/plugins/golem15/user/controllers/registration.go` | `delete(payload, "password_confirmation")` in `FireRegisterEvent` | removed | `go -C ../fonoteka.go test ./plugins/golem15/user -run '^TestRegisterEventPayload$'` | fails: TestRegisterEventPayload; restored, cmp ok | | RC-11 | T-13-20 | `../fonoteka.go/plugins/golem15/user/controllers/registration.go` | `delete(payload, "password_confirmation")` in `FireRegisterEvent` | removed | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-20$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-20; restored, cmp ok | | RC-12 | T-13-18 | `../fonoteka.go/plugins/golem15/fonoteka/classes/onboarding.go` | the recount under the advisory lock in `BootstrapOwner` | removed | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-18$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-18; restored, cmp ok | | RC-13 | T-13-12 | `../fonoteka.go/plugins/golem15/fonoteka/classes/csv_import_service.go` | the accessible-collection check in `CsvImportFor` | removed | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-12$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-12; restored, cmp ok | | RC-14 | T-13-17 | `../fonoteka.go/plugins/golem15/fonoteka/classes/csv_import_service.go` | `WHERE id = ? AND status = ?` in the commit compare-and-swap | the status condition dropped | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-17$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-17; restored, cmp ok | | RC-15 | T-13-10 | `../fonoteka.go/plugins/golem15/fonoteka/classes/credential_write_service.go` | `CredentialFillFields` | plus `"user_id"` | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^FuzzWriteEndpoints$'` | fails: FuzzWriteEndpoints, FuzzWriteEndpoints/seed#22; restored, cmp ok | | RC-16 | T-13-10 | `../fonoteka.go/plugins/golem15/fonoteka/classes/credential_write_service.go` | `CredentialFillFields` | plus `"user_id"` | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-10$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-10; restored, cmp ok | | RC-17 | T-13-21 | `../fonoteka.go/plugins/golem15/fonoteka/classes/notifications.go` | `WHERE user_id = ? AND id = ?` in `MarkNotificationRead` | the user condition dropped | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-21$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-21; restored, cmp ok | | RC-18 | T-13-16 | `../fonoteka.go/plugins/golem15/fonoteka/classes/csv_import_service.go` | `return nil, ErrDiscogsUnavailable` in the Phase 13 `ReleaseFetcher` | returns a draft | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-16$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-16; restored, cmp ok | | RC-19 | T-13-28 | `../fonoteka.go/plugins/golem15/fonoteka/classes/wishlist_notifications.go` | `if len(inserted) != 1 \|\| !inserted[0] {` in `EnqueueWishlistDigest` | dispatch on every upsert | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-28$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-28; restored, cmp ok | | RC-20 | T-13-02 | `../fonoteka.go/plugins/golem15/fonoteka/classes/serialize_public_album.go` | the end of `PublicAlbumDTO` | plus a `shelf` field | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-02$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-02; restored, cmp ok | | RC-21 | T-13-08 | `../fonoteka.go/plugins/golem15/fonoteka/controllers/api/credentials_controller.go` | the column select and status body of `AiCredentialShow` | a body echoing the decrypted key | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-08$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-08; restored, cmp ok | | RC-22 | T-13-07 | `../fonoteka.go/plugins/golem15/fonoteka/routes.go` | `g.Get("/wishlist/albums", wishlistIndex, "inv.scope:read")` on the token group | a second `inv.scope:read` | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestRouteTablePhase13$'` | fails: TestRouteTablePhase13, TestRouteTablePhase13/group-middleware-and-scopes; restored, cmp ok | | RC-23 | T-13-04 | `../fonoteka.go/plugins/golem15/fonoteka/classes/reservations.go` | `WHERE album_id = ? AND user_id = ?` in `CancelReservation` | the user condition dropped | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-04$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-04; restored, cmp ok | | RC-24 | T-13-06 | `../fonoteka.go/plugins/golem15/fonoteka/controllers/api/wishlist_subscriptions_controller.go` | `Scopes(classes.WishlistsVisibleTo(user.ID))` in `visibleWishlist` | any wishlist | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-06$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-06; restored, cmp ok | | RC-25 | T-13-09 | `../fonoteka.go/plugins/golem15/fonoteka/controllers/api/credentials_controller.go` | the `mayManageOrg` check in `OrgAiCredentialStore` | removed | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-09$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-09; restored, cmp ok | | RC-26 | T-13-13 | `../fonoteka.go/plugins/golem15/fonoteka/controllers/api/csv_import_controller.go` | `csvBucket(app)` in `CsvImportStore` | the public uploads bucket | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-13$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-13; restored, cmp ok | | RC-27 | T-13-19 | `../fonoteka.go/plugins/golem15/fonoteka/classes/onboarding.go` | `LOWER(email) = ?` in `HandleRegisterEvent` | `(LOWER(email) = ? OR TRUE)` | `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^TestPhase13Threats$/^T-13-19$'` | fails: TestPhase13Threats, TestPhase13Threats/T-13-19; restored, cmp ok | | RC-28 | T-13-SC | `scripts/check-phase13.sh` (mutated copy) | the `golang.org/x/text` lookup in `module_pin` | a fixed audited line | `bash --self-test` | fails: "refuse: self-test module_pin accepted golang.org/x/text v0.41.0"; original untouched | | RC-29 | T-13-36 | `scripts/check-phase13.sh` (mutated copy) | `sys.exit(1)` after a corpus secret is found | `pass` | `bash --self-test` | fails: "refuse: self-test corpus_scan accepted a planted secret aaaaaaaaaaaa"; original untouched | | RC-30 | T-13-35 | `scripts/check-phase13.sh` (mutated copy) | the removal-row requirement of high threats in `evidence_check` | `if False:` | `bash --self-test` | fails: "refuse: self-test evidence_check accepted the removal plant"; original untouched | | RC-31 | T-13-34 | `scripts/check-phase13.sh` (mutated copy) | the dirty-file refusal of `removal_harness` | `if False:` | `bash --self-test` | fails: "refuse: self-test removal harness mutated a dirty file"; original untouched | Not every protection is removable on its own. Skipping the constraint check in surf's family dispatcher does not change any routes.php pair: each member's own handler is wrapped with its `Where` constraints too, and the routes.php pairs already differ in their literal segments. RC-01 is therefore caught by `TestOverlapConstraintFallsThrough`, a family where an earlier member matches the literals but not its constraint. The application-level checks (RC-02, RC-03) skip the literal match instead. Disabling only `PubfailCounter.TooMany` leaves the handlers locked, because they use `Begin`; RC-08 removes the check in `Begin` and RC-09 pins `TooMany` through its unit test. ## Fixes made during the review | Defect | Threat | Fix | Failing-when-broken test | Commit | |--------|--------|-----|--------------------------|--------| | The CSV export, which streams like PHP's download, dropped a database error that struck after the BOM and the header row: the client got a short file with 200 and no log line, where Laravel reports the exception | T-13-14 (export component) | `CsvExport` logs the error with the user id; the status cannot change once the headers are sent, as in PHP | `TestPhase13HandlersFailClosed` (export-stream-failure: RED with no log line) | fonoteka.go `4dec779` | | The Phase 9 admin route inventory predated the 19 Phase 12.2 cabana relation child, pivot and file routes, so `TestPhase09SecurityRoutes` failed on the current framework; every one carries the backend guard | — (test inventory, no production change) | The expected set lists them; the test still fails on any unguarded or unlisted admin route | `TestPhase09SecurityRoutes` | fonoteka.go `549840d` |