Files
summercms/.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-SECURITY-REVIEW.md
Jakub Zych 931c02db31 docs(13-06): sign off the Phase 13 security review and validation
- 13-SECURITY-REVIEW.md: every T-13 threat with its strictest severity and
  disposition, protecting code, named tests and the 31 removal checks,
  all failing as required; the CSV export logging fix and the Phase 9
  route inventory update
- 13-VALIDATION.md: per-task map 13-01-T1 to 13-06-T3 all green, the gate
  command, coverage per package, Wave 0 ticked, status validated
- REQUIREMENTS.md: API-03, API-04, API-06 and API-07 complete
- deferred-items.md: 13-06 findings (process-wide job dispatcher, scalar
  mapping body, tmpfs quota)
2026-10-03 11:43:39 +02:00

32 KiB

phase, reviewed, reviewer, threats_open, gate, removal_harness
phase reviewed reviewer threats_open gate removal_harness
13 2026-10-03 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) 0 scripts/check-phase13.sh --all 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
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/<uid>/<uuid>.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
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 <copy> --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 <copy> --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 <copy> --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 <copy> --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