- 12-SECURITY-REVIEW.md maps T-12-01..T-12-34 and T-12-SC to a named test and 25 removal checks, all seen failing with the protection removed - 12-VALIDATION.md validated with the final per-task map, nyquist_compliant - REQUIREMENTS.md: API-01 and API-02 complete
29 KiB
phase, reviewed, reviewer, threats_open, gate, removal_harness
| phase | reviewed | reviewer | threats_open | gate | removal_harness |
|---|---|---|---|---|---|
| 12 | 2026-10-02 | gsd-executor, plan 12-05 (self-performed code-and-test review of plans 12-01 to 12-05; no reviewer agent was spawned, per the 08-10 precedent) | 0 | scripts/check-phase12.sh --all | scripts/check-phase12.sh --removal |
Phase 12 Security Review
This is a code-and-test review of every threat in the registers of Plans 12-01 to 12-05 (T-12-01 to T-12-34 and T-12-SC). Severity and disposition are copied from the originating plan; T-12-SC is declared by every plan and is listed once with the strictest entry (12-01: high, mitigate, the golang.org/x/image bump), the later plans adding no dependency. 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 or gate stage fails with the protection removed. scripts/check-phase12.sh --removal does this for every high mitigated threat: it refuses a file with uncommitted changes, applies an anchor-exact mutation that removes the protection, runs the named test, requires it to fail on an assertion (a build failure does not count), and restores the file byte for byte, checked with cmp. The results are under "Removal checks". The one accepted threat keeps its rationale from its originating plan.
The review found four defects in Phase 12 code and two in the 12-01 framework port, all fixed in plan 12-05 with a failing-when-broken test (see "Fixes made during the review"). The most serious: a 100-byte PNG declaring a 30000x30000 canvas, uploaded by any household member, made every later listing of the collection and the album answer 500 (T-12-16).
Commands run from summercms.go; ../fonoteka.go tests run inside that repository. Gate stages are modes of scripts/check-phase12.sh.
| Threat | Category | Component | Severity | Disposition | Production mitigation | Test or gate stage | Observed result | Residual risk |
|---|---|---|---|---|---|---|---|---|
| T-12-01 | Information Disclosure | collections/{id}, photos/{fileId}, image | high | mitigate | controllers/api/collections_controller.go findAccessibleCollection scopes every lookup with classes.AccessibleBy(user, token); photo and image lookups are scoped to the collection's own system_files rows; foreign and missing ids share one 404 body |
TestPhase12Threats/T-12-01 (GET/PUT/DELETE, photo, image and a foreign photo id: identical 404s, the foreign photo stays attached), TestCollectionsIndexBothGroups, parity 404 fixtures; stage --named |
pass; removal check RC-01 fails TestPhase12Threats/T-12-01 | None known |
| T-12-02 | Information Disclosure | albums/search items and meta.total | high | mitigate | classes/album_search.go re-gates every engine id with ScopedAlbums (AlbumsAccessibleBy plus the active collection) for the page and for the total; engine errors fall back to SQL |
TestSearchLeak (stale-moved, mis-scoped, soft-deleted, removed-editor and its resolve race, token-pin, on both groups, page ids and meta.total compared exactly) |
pass; removal checks RC-02 and RC-04 fail TestSearchLeak | A stale index document shortens a page, as Scout does |
| T-12-03 | Elevation of Privilege | personal token pin | high | mitigate | classes/access.go AccessibleBy narrows to the pin (plus the owner's wishlist) as a separate grouped AND; Resolve needs exactly one accessible pinned id; SwitchTo refuses tokens |
TestPhase12Threats/T-12-03 (token collections list holds only the pin; the owner's other collection is a 404 like a missing one; SwitchTo refuses), TestResolvePinnedToken, TestCollectionsIndexBothGroups |
pass; removal check RC-05 fails TestPhase12Threats/T-12-03 | None known |
| T-12-04 | Elevation of Privilege | owner-only interactive routes on the token group | high | mitigate | routes.go mounts switch, collection/share, me/context, realtime/channels, sync and missing on the JWT group only; the share handler and SwitchTo also refuse a token |
TestRouteTablePhase12 (no missing or unexpected route, every JWT-only path absent from the token group), TestPhase12Threats/T-12-04 (each path unmounted for a token; the share token unchanged) |
pass; removal check RC-07 fails TestRouteTablePhase12 | None known |
| T-12-05 | Elevation of Privilege | editor acting as owner | high | mitigate | Explicit owner_id == user checks in collection destroy, image upload and delete, and the share surface; editors get the 404 |
TestPhase12Threats/T-12-05 (editor delete, image upload/delete and share show/update/regenerate refused; the row, its image and share token unchanged), TestShareOwnerOnly |
pass; removal check RC-10 fails TestPhase12Threats/T-12-05 | None known |
| T-12-06 | Spoofing | invitation accept | high | mitigate | classes/invitation_service.go AcceptInvitation looks the invitation up by sha256 FOR UPDATE, requires it pending, unexpired, unrevoked and addressed to the caller's normalized e-mail, else 410; resend rotates the hash |
TestPhase12Threats/T-12-06 (another user's, unknown, reused, expired, revoked and rotated tokens all 410), TestInvitationAcceptAddsEditor, TestConcurrentAcceptSingleEditor |
pass; removal checks RC-12 (e-mail check) and RC-13 (sha256 lookup) fail TestPhase12Threats/T-12-06 | A leaked invitation link is usable by its addressee until accepted or expired, as in PHP |
| T-12-07 | Information Disclosure | raw token in river_job.args, summer_jobs and logs | high | mitigate | enqueueInvitationMail stores the token in the job args only as lagoon.Encrypted ciphertext under the app key, through Enqueue (never summer_jobs); nothing logs args, token or link |
TestPhase12Threats/T-12-07 (no 64-hex value in river_job.args, the decrypted token matches the stored hash and appears neither in args nor in the captured logs nor in summer_jobs), TestInvitationMailEnqueuedInTx |
pass; removal check RC-14 fails TestPhase12Threats/T-12-07 | Whoever holds the app key can decrypt queued tokens |
| T-12-08 | Spoofing | share token | high | mitigate | classes/share_service.go draws 16 symbols of a 62-symbol alphabet from crypto/rand with rejection at 248, retries on a unique clash, mutates under FOR UPDATE, never logs |
TestPhase12Threats/T-12-08 (300 unique tokens of the alphabet, rejection sampling over scripted bytes, an exhausted source fails), TestShareTokenAlphabet |
pass; removal check RC-15 fails TestPhase12Threats/T-12-08 | About 95 bits per token |
| T-12-09 | Tampering / Information Disclosure | cover_urls and cover_url fetches (SSRF) | high | mitigate | Manual cover URLs go through fetchguard PublicOnly (https only, dial-time private-address refusal, no redirects, byte cap, timeout); cover imports through fetchguard AllowHosts (.discogs.com), after the write commits |
TestPhase12Threats/T-12-09 (http, ftp, loopback, RFC 1918, link-local metadata, IPv6 loopback and ULA refused with PHP's reasons; importer refuses non-Discogs, non-https and private hosts and attaches nothing), TestManualCoverReasons, TestCoverImportAfterCommit, fetchguard's own tests |
pass; removal check RC-16 (fetch without fetchguard) fails TestPhase12Threats/T-12-09 | DNS rebinding is refused at dial time by fetchguard |
| T-12-10 | Tampering / Denial of Service | photo upload | high | mitigate | Router body cap, the max:10240 file rule, classes.IsAllowedImage (sniff plus DecodeConfig), throttle:20,1 on the JWT photo route |
TestPhase12Threats/T-12-10 (a PNG-signature polyglot and an HTML file refused before any row is written, an over-cap body 413, the 21st upload in a minute 429), TestAlbumPhotoUpload |
pass; removal check RC-17 fails TestPhase12Threats/T-12-10 | The token group's photo route carries only the token bucket, as in PHP |
| T-12-11 | Tampering | mass assignment on albums and bulk | high | mitigate | AlbumFillFields plus a per-field setter (setAlbumField has no server-owned column) and a server-set collection_id; market_price_source is only ever cleared |
FuzzWriteEndpoints (all 41 Phase 12 write routes from the route table; every server-owned key with a hostile value plus fuzzed keys; Postgres snapshots allow only each route's own columns; 82 seeds in plain go test, 60 s fuzzing clean) |
pass; removal check RC-18 (collection_id taken from the input) fails FuzzWriteEndpoints | None known |
| T-12-12 | Tampering | concurrent resolve provisioning | medium | mitigate | users row FOR UPDATE before the context row, the unique user_id context key |
TestPhase12Threats/T-12-12 (six concurrent first resolves provision one collection), TestResolveProvisionsOnce |
pass | None known |
| T-12-13 | Elevation of Privilege | realtime/channels raw id | low | accept | Matches PHP; the id is the caller's own resolved collection, never accepted as a tenant selector on any write, and the collection authorizer re-validates membership on every subscribe. | none (accepted) | accepted | The channel name reveals the caller's own collection id |
| T-12-14 | Tampering | lagoon exists: and regex rules |
high | mitigate | modules/lagoon/validate_rules.go ParseRules checks table and column names against identName at registration (panics on a bad one); the value is bound, compared as text; regexes come only from code and must compile in RE2 |
TestValidateRequestParseRules, TestValidateRequestExistsRule (injection-shaped values are plain misses, an unsafe inferred column and a missing table are errors), TestPhase12Threats/T-12-14 |
pass; removal check RC-20 fails TestValidateRequestParseRules | None known |
| T-12-15 | Denial of Service | wildcard expansion and size rules | medium | mitigate | Expansion is bounded by the decoded body, which surf caps; size counts are linear; RE2 has no backtracking | TestPhase12Threats/T-12-15 (20000 wildcard rows validate in about a second; an over-cap JSON body is 413), TestValidateRulesMatchLaravel |
pass | None known |
| T-12-16 | Denial of Service | webp/png/jpeg decode in Thumb and DecodeConfig | medium | mitigate | attach.File.Thumb reads the size from the header and never decodes more than 4096x4096 pixels; since 12-05 an unusable original gets WinterCMS's broken-image picture instead of an error (fixed: the error made every later listing 500) |
TestThumbBrokenSourceServesPlaceholder, TestPhase12Threats/T-12-16 (a 30000x30000 header-only PNG uploads at once and the listings still answer 200), TestThumbModesAndFormats |
pass | None known |
| T-12-17 | Information Disclosure | tide multipart part files | medium | mitigate | Part bytes are committed test images pinned by sha256; substitution never touches file bytes; check_corpus --check-secrets scans every YAML |
TestPhase12Threats/T-12-17 (each part file is an image or the one short text fixture, no 64-hex value), TestMultipartTamperedPartFileFailsLoad, TestMultipartPartEdges (symlink out of the fixture directory refused) |
pass | None known |
| T-12-18 | Elevation of Privilege | user groups table | medium | mitigate | No route writes users_groups; User.Groups is json:"-"; the site-admin predicate reads group codes server-side |
TestPhase12Threats/T-12-18 (no write route mentions groups; a user with groups marshals and answers me/context without them), TestUserGroupCodesAndHasGroupCode, TestMeContextFlags |
pass | None known |
| T-12-19 | Information Disclosure | Winter error pages | low | mitigate | The embedded pages are PHP's APP_DEBUG=false bytes with only the stylesheet origin templated from app.url |
TestPhase12Threats/T-12-19 (no stack trace, path, line number or exception class in any page or a live 404), TestWinterErrorWriters |
pass | None known |
| T-12-20 | Information Disclosure | committed flow fixtures | high | mitigate | Raw invitation tokens appear only as {{secret:invite}}; parity/check_corpus.go --check-secrets fails on any 64-hex value except a multipart sha256 pin |
TestCheckCorpusInvitationToken, TestPhase12Threats/T-12-20 (every fixture scanned), check-phase12.sh --parity (runs check_corpus.go --require-recorded --check-secrets) |
pass; removal check RC-21 fails TestCheckCorpusInvitationToken | None known |
| T-12-21 | Denial of Service | invite and resend mail flooding | medium | mitigate | throttle:10,1 on store and resend; one pending row per (collection, e-mail) |
TestPhase12Threats/T-12-21 (a repeated e-mail reuses its row; the store is throttled within ten requests), TestRouteTablePhase12 (inline throttles on exactly their routes) |
pass | None known |
| T-12-22 | Tampering | accept joining the inviter's organisation | medium | mitigate | Only an org-less invitee joins, as member, inside the accept transaction | TestPhase12Threats/T-12-22 (an invitee's own organisation stays; an org-less invitee joins as member) |
pass | None known |
| T-12-23 | Information Disclosure | album_added notifications and broadcasts | medium | mitigate | Recipients are the collection's owner and editors minus the actor; broadcasts only on the kind=collection channel, after commit | TestPhase12Threats/T-12-23 (an editor's album notifies only the owner), TestAlbumAddedNotifiesHousehold, TestBroadcastGoldens |
pass | None known |
| T-12-24 | Denial of Service | search recount and bulk size | medium | mitigate | Recount capped at 1000 ids in pages of 250; bulk bounded by the body cap and validation; cover imports capped at max_covers |
TestSearchLeak/cap, TestPhase12Threats/T-12-24 (six cover_urls 422, an over-cap bulk 413, the importer stops at max_covers) |
pass | None known |
| T-12-25 | Tampering | gate --removal leaving mutated source | medium | mitigate | check-phase12.sh --removal refuses a file with uncommitted changes, mutates by exact anchor, restores in a finally and compares with cmp |
check-phase12.sh --self-test (removal harness on a scratch module: a guarded mutation fails, a surviving one is reported, a non-unique anchor and a dirty file are refused, the file is restored) |
pass; both repositories clean after the 25 removal runs | An interrupted run (SIGKILL) could leave a mutation; git status shows it |
| T-12-26 | Repudiation | security review claims without evidence | medium | mitigate | check-phase12.sh --evidence refuses a threat without one review row, a high mitigated threat without a removal row, a mitigated threat naming no test, and a validation row that is not green or names a test the gate does not run |
check-phase12.sh --evidence, check-phase12.sh --self-test (five plants refused) |
pass | The review text itself is written by the executor |
| T-12-27 | Information Disclosure | fuzz seed corpus | low | mitigate | Seeds hold synthetic values only; the gate scans testdata/fuzz for 64-hex values, inv_ tokens, JWTs and bearer headers |
check-phase12.sh --parity (corpus scan), check-phase12.sh --self-test (four planted secrets and an empty corpus refused) |
pass; removal check RC-25 fails --self-test |
None known |
| T-12-28 | Information Disclosure | beachcomber SearchPage found count | high | mitigate | found is used only as the size of the SQL recount (D-19); the total exposed is the re-gated count of at most 1000 engine ids |
TestSearchLeak (short-page, recount, cap: no poisoned id counted, the recount pages of 250, never beyond 1000), TestSearchPageUsesPageSearcher |
pass; removal check RC-03 fails TestSearchLeak | None known |
| T-12-29 | Denial of Service | collection photo/image upload | medium | mitigate | Body cap, max:10240, image and mimes sniff before the blob write |
TestPhase12Threats/T-12-29 (a non-image refused on photos and image with no row written), TestCollectionPhotoUpload |
pass | None known |
| T-12-30 | Tampering | mass assignment on collections | high | mitigate | CollectionFillFields holds only name and description; owner_id, kind and the share columns are server-set |
FuzzWriteEndpoints (collection create, update, share and delete routes with every server-owned key hostile) |
pass; removal check RC-19 (owner_id fillable) fails FuzzWriteEndpoints | None known |
| T-12-31 | Elevation of Privilege | household routes under a personal token | high | mitigate | Household and invitation routes are on the JWT group only; assertInvitationOwner and the handlers' ownerCollection also refuse a token |
TestRouteTablePhase12, TestPhase12Threats/T-12-31 (every household path unmounted for a token; SendInvitation and RemoveEditor refuse a token) |
pass; removal checks RC-08 (route) fails TestRouteTablePhase12 and RC-09 (service lock) fails TestPhase12Threats/T-12-31 | None known |
| T-12-32 | Elevation of Privilege | editor managing members or invitations | high | mitigate | ownerCollection checks the resolved collection's owner before every household action; editors get the 404 page |
TestPhase12Threats/T-12-32 (every household action of an editor is the 404 page and changes nothing), TestHouseholdInvitationsIndexRoute |
pass; removal check RC-11 fails TestPhase12Threats/T-12-32 | None known |
| T-12-33 | Information Disclosure | albums/{id}, photos/{fileId}, rating | high | mitigate | FindAccessibleAlbum is ScopedAlbums plus the id; photo lookups match the album's own attachments; one 404 body for foreign, out-of-context and missing ids |
TestPhase12Threats/T-12-33 (every id route on both groups, a foreign and an out-of-context album, another album's photo id) |
pass; removal checks RC-22 (photo scoping) and RC-23 (album scoping) fail TestPhase12Threats/T-12-33 | None known |
| T-12-34 | Elevation of Privilege | token pin on albums, stats, value, search, lookups | high | mitigate | Resolve answers a token with its pin, never the owner's stored context; AlbumsAccessibleBy narrows; sync and missing are JWT only |
TestPhase12Threats/T-12-34 (a pinned token sees only its collection on albums, stats, value, search and artists while the owner's context is elsewhere), TestSearchLeak/token-pin, TestRouteTablePhase12 |
pass; removal check RC-06 fails TestPhase12Threats/T-12-34 | None known |
| T-12-SC | Tampering | Go module installs (golang.org/x/image v0.46.0) | high | mitigate | golang.org/x/image pinned at v0.46.0 with go.sum checksums (D-24); no other dependency was added in Phase 12 |
check-phase12.sh --go (module pin over go list -m all of both repositories), --self-test (plants) |
pass; removal check RC-24 (pin check disabled) fails --self-test |
None known |
Removal checks
Each row is one anchor-exact mutation from scripts/check-phase12.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 and compared with cmp against the copy saved before the mutation. All twenty-five were run on 2026-10-02 and all failed as required; git status was clean in both repositories afterwards. RC-12 and RC-22 first failed to build (an unused variable and an unused import) and were rewritten to compile before being run again.
| Check | Threat | File | Anchor removed or changed | Replacement | Test run | Observed |
|---|---|---|---|---|---|---|
| RC-01 | T-12-01 | ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/collections_controller.go |
Scopes(classes.AccessibleBy(userID, token)). in findAccessibleCollection |
removed | go test ./plugins/golem15/fonoteka -run '^TestPhase12Threats$/^T-12-01$' |
fails: TestPhase12Threats/T-12-01; restored, cmp ok |
| RC-02 | T-12-02 | ../fonoteka.go/plugins/golem15/fonoteka/classes/album_search.go |
ScopedAlbums(...) on the page re-gate |
an unscoped album query | go test ./plugins/golem15/fonoteka -run '^TestSearchLeak$' |
fails: stale-moved, mis-scoped, token-pin, short-page, recount, removed-editor (both groups); restored, cmp ok |
| RC-03 | T-12-28 | ../fonoteka.go/plugins/golem15/fonoteka/classes/album_search.go |
ScopedAlbums(...) on the total recount |
an unscoped album query | go test ./plugins/golem15/fonoteka -run '^TestSearchLeak$' |
fails: every total assertion, including cap; restored, cmp ok |
| RC-04 | T-12-02 | ../fonoteka.go/plugins/golem15/fonoteka/classes/album_queries.go |
Scopes(AlbumsAccessibleBy(userID, token)). in ScopedAlbums |
removed | go test ./plugins/golem15/fonoteka -run '^TestSearchLeak$' |
fails: token-pin, removed-editor/race; restored, cmp ok |
| RC-05 | T-12-03 | ../fonoteka.go/plugins/golem15/fonoteka/classes/access.go |
if pin := tokenCollectionPin(token); len(pin) > 0 { |
... false && len(pin) > 0 { |
go test ./plugins/golem15/fonoteka -run '^TestPhase12Threats$/^T-12-03$' |
fails: TestPhase12Threats/T-12-03; restored, cmp ok |
| RC-06 | T-12-34 | ../fonoteka.go/plugins/golem15/fonoteka/classes/active_collection.go |
if token != nil { before resolvePinnedTokenCollection |
if false { |
go test ./plugins/golem15/fonoteka -run '^TestPhase12Threats$/^T-12-34$' |
fails: TestPhase12Threats/T-12-34; restored, cmp ok |
| RC-07 | T-12-04 | ../fonoteka.go/plugins/golem15/fonoteka/routes.go |
the token group's g.Get("/me", ...) |
plus a token GET /collection/share |
go test ./plugins/golem15/fonoteka -run '^TestRouteTablePhase12$' |
fails: router-matches-table, jwt-only-routes-absent-from-token-group; restored, cmp ok |
| RC-08 | T-12-31 | ../fonoteka.go/plugins/golem15/fonoteka/routes.go |
the token group's g.Get("/me", ...) |
plus a token GET /household/members |
go test ./plugins/golem15/fonoteka -run '^TestRouteTablePhase12$' |
fails: router-matches-table, jwt-only-routes-absent-from-token-group; restored, cmp ok |
| RC-09 | T-12-31 | ../fonoteka.go/plugins/golem15/fonoteka/classes/invitation_service.go |
if token != nil || actor == nil || ... in assertInvitationOwner |
without token != nil |
go test ./plugins/golem15/fonoteka -run '^TestPhase12Threats$/^T-12-31$' |
fails: TestPhase12Threats/T-12-31; restored, cmp ok |
| RC-10 | T-12-05 | ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/collections_controller.go |
if c == nil || c.OwnerID != user.ID { in collection destroy |
if c == nil { |
go test ./plugins/golem15/fonoteka -run '^TestPhase12Threats$/^T-12-05$' |
fails: TestPhase12Threats/T-12-05; restored, cmp ok |
| RC-11 | T-12-32 | ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/invitations_controller.go |
if c.OwnerID != user.ID { in ownerCollection |
if false { |
go test ./plugins/golem15/fonoteka -run '^TestPhase12Threats$/^T-12-32$' |
fails: TestPhase12Threats/T-12-32; restored, cmp ok |
| RC-12 | T-12-06 | ../fonoteka.go/plugins/golem15/fonoteka/classes/invitation_service.go |
if inv.Email != email { in AcceptInvitation |
if false && inv.Email != email { |
go test ./plugins/golem15/fonoteka -run '^TestPhase12Threats$/^T-12-06$' |
fails: TestPhase12Threats/T-12-06; restored, cmp ok |
| RC-13 | T-12-06 | ../fonoteka.go/plugins/golem15/fonoteka/classes/invitation_service.go |
invitationTokenHash(rawToken) in the accept lookup |
rawToken |
go test ./plugins/golem15/fonoteka -run '^TestPhase12Threats$/^T-12-06$' |
fails: TestPhase12Threats/T-12-06; restored, cmp ok |
| RC-14 | T-12-07 | ../fonoteka.go/plugins/golem15/fonoteka/classes/invitation_service.go |
InvitationMailArgs{InvitationID: invitationID, Token: s} (ciphertext) |
Token: raw |
go test ./plugins/golem15/fonoteka -run '^TestPhase12Threats$/^T-12-07$' |
fails: TestPhase12Threats/T-12-07; restored, cmp ok |
| RC-15 | T-12-08 | ../fonoteka.go/plugins/golem15/fonoteka/classes/share_service.go |
shareRejectAt = 248 |
= 255 |
go test ./plugins/golem15/fonoteka -run '^TestPhase12Threats$/^T-12-08$' |
fails: TestPhase12Threats/T-12-08; restored, cmp ok |
| RC-16 | T-12-09 | ../fonoteka.go/plugins/golem15/fonoteka/classes/manual_cover_fetcher.go |
return fetchguard.Fetch(ctx, rawURL, policy, nil) |
a fetch that skips fetchguard | go test ./plugins/golem15/fonoteka -run '^TestPhase12Threats$/^T-12-09$' |
fails: TestPhase12Threats/T-12-09; restored, cmp ok |
| RC-17 | T-12-10 | ../fonoteka.go/plugins/golem15/fonoteka/classes/image_guard.go |
the body of IsAllowedImage |
return true first |
go test ./plugins/golem15/fonoteka -run '^TestPhase12Threats$/^T-12-10$' |
fails: TestPhase12Threats/T-12-10; restored, cmp ok |
| RC-18 | T-12-11 | ../fonoteka.go/plugins/golem15/fonoteka/classes/album_write_service.go |
a.CollectionID = collectionID in CreateAlbum |
collection_id taken from the input |
go test ./plugins/golem15/fonoteka -run '^FuzzWriteEndpoints$' |
fails: FuzzWriteEndpoints (every album-create seed); restored, cmp ok |
| RC-19 | T-12-30 | ../fonoteka.go/plugins/golem15/fonoteka/classes/collection_write_service.go |
CollectionFillFields = []string{"name", "description"} |
plus "owner_id" |
go test ./plugins/golem15/fonoteka -run '^FuzzWriteEndpoints$' |
fails: FuzzWriteEndpoints (the collection update seeds on both groups); restored, cmp ok |
| RC-20 | T-12-14 | modules/lagoon/validate_rules.go |
if !identName.MatchString(table) { in exists parsing |
if false { |
go test ./modules/lagoon -run '^TestValidateRequestParseRules$' |
fails: TestValidateRequestParseRules; restored, cmp ok |
| RC-21 | T-12-20 | ../fonoteka.go/parity/check_corpus.go |
if hex64Re.MatchString(line) { |
if false && ... |
go test ./parity -run '^TestCheckCorpusInvitationToken$' |
fails: planted_path, planted_body, planted_bare; restored, cmp ok |
| RC-22 | T-12-33 | ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/album_photos_controller.go |
the photo lookup's attachment type, id and field conditions | the file id alone | go test ./plugins/golem15/fonoteka -run '^TestPhase12Threats$/^T-12-33$' |
fails: TestPhase12Threats/T-12-33; restored, cmp ok |
| RC-23 | T-12-33 | ../fonoteka.go/plugins/golem15/fonoteka/classes/album_queries.go |
ScopedAlbums(...) in FindAccessibleAlbum |
an unscoped album query | go test ./plugins/golem15/fonoteka -run '^TestPhase12Threats$/^T-12-33$' |
fails: TestPhase12Threats/T-12-33; restored, cmp ok |
| RC-24 | T-12-SC | scripts/check-phase12.sh (mutated copy) |
the golang.org/x/image lookup in module_pin |
a fixed audited line | bash <copy> --self-test |
fails: "refuse: self-test module_pin accepted golang.org/x/image v0.45.0"; original untouched |
| RC-25 | T-12-27 | scripts/check-phase12.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"; original untouched |
Not every protection is removable on its own. Dropping AlbumsAccessibleBy from ScopedAlbums (RC-04) leaves the stale-moved, mis-scoped and soft-deleted cases passing, because the active-collection filter and GORM's own deleted_at filter still exclude them; only the token pin and the resolve/removal race depend on the access scope, and those cases fail. Making collection_id fillable in AlbumFillFields alone does not survive to the database either (setAlbumField has no setter for it and CreateAlbum sets the column), so RC-18 removes the server-set assignment instead.
Fixes made during the review
| Defect | Threat | Fix | Failing-when-broken test | Commit |
|---|---|---|---|---|
A stored photo whose original is missing, undecodable or declares more than 4096x4096 pixels made attach.File.Thumb return an error, so every listing that shows it (GET collections, the collection, the album) answered 500 from then on: a 100-byte PNG from any household member broke the household's listings |
T-12-16 | Thumb follows WinterCMS's File::makeThumb catch branch: log the reason, store WinterCMS's broken-image picture (attach.BrokenImagePNG) as the thumbnail and return its URL |
TestThumbBrokenSourceServesPlaceholder (RED: three errors), TestPhase12Threats/T-12-16 |
summercms.go 1307060 |
in compared array elements loosely and not_in failed an array when any element was listed or when the array rule was absent; Laravel uses array_diff (exact strings) and validateNotIn is !validateIn |
T-12-14 | validateIn/validateNotIn follow Laravel |
TestValidateRulesMatchLaravel (162 cases recorded from WinterCMS's validator; RED: three divergences) |
summercms.go 36983bc |
JSON floats were cast to strings with up to 17 digits; PHP's (string) uses 14 (%.14G) |
T-12-14 | phpFloatString formats like zend_gcvt |
TestPHPFloatStringMatchesPHPCast (RED: nine values) |
summercms.go 3ac1d64 |
(int) of request values read digits only and overflowed ("1e2" was 1, "9999999999999999999" was PHP_INT_MIN, out-of-range floats undefined); is_numeric("1e400") was false; float strings used Go's %G |
— (API-02 parity) | phpIntCast follows zval_get_long (numeric prefix, saturating) and zend_dval_to_lval (modular), phpNumericValue accepts overflowing exponents, phpStringOf uses %.14G |
TestPHPValueCasts (RED: nineteen values) |
fonoteka.go 64f5496 |
| The Laravel e-mail rule's domain part refused literals with spaces, allowed 63-byte labels after the first (egulias counts the dot), had no 253-byte cap and refused comments at the start of the domain | — (API-01 parity, not observable: every invitation e-mail failure answers the same 500 page) | laravelDomainOK follows egulias |
TestValidLaravelEmailRFC (RED: three addresses) |
fonoteka.go 817867d |
albums/sync?per_page=1e2 paged one album where PHP pages 100 (phpInt read digits only); $request->boolean() did not trim and refused 1.0 |
— (API-02 parity) | phpInt uses classes.PHPIntCast; laravelBoolean follows FILTER_VALIDATE_BOOLEAN |
TestRequestCasts (RED: six values) |
fonoteka.go 2869747 |