docs(12.2-05): security review and validation map for phase 12.2
- 12.2-SECURITY-REVIEW.md maps T-12.2-01 to T-12.2-36 and the supply chain rows to the controls as built and their passing tests, with the parent-predicate removal check and the residual risks - 12.2-VALIDATION.md: per-task map with real task ids, all green, nyquist_compliant and wave_0_complete set - deferred-items.md: out-of-scope findings for follow-up
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
phase: "12.2"
|
||||||
|
reviewed: "2026-10-02"
|
||||||
|
threats_open: 0
|
||||||
|
reviewer: "gsd-executor (plan 12.2-05; no separate security agent was available, as in Phase 08)"
|
||||||
|
gate: "scripts/check-phase12.2.sh --all"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 12.2 Security Review
|
||||||
|
|
||||||
|
This review covers the Phase 12.2 admin surface after plan 12.2-05:
|
||||||
|
|
||||||
|
- datepicker and fileupload fields
|
||||||
|
- the seven record file routes and the seven child file routes
|
||||||
|
- relation child CRUD and pivot editing
|
||||||
|
- deferred binding with commit and purge
|
||||||
|
- the SPA controls that drive them
|
||||||
|
|
||||||
|
Each row names the control as built and the test that proves it. The executor wrote this review, because no separate security agent was available (the same arrangement as Phase 08). Every named test was re-run in this plan, either through `bash scripts/check-phase12.2.sh --all` (all nine stages PASS on 2026-10-02) or by its own `go test -run` line.
|
||||||
|
|
||||||
|
A high threat counts as mitigated only when a named test fails once its control is removed. For D-15 this was shown directly. With the parent predicate in `loadChild` (modules/cabana/relation_child.go) dropped, `go test ./modules/cabana -run '^TestRelationChildScope' -count=1` fails with:
|
||||||
|
|
||||||
|
```
|
||||||
|
relation_child_scope_test.go:97: GET records/{child} through G1: status=200 want 404 body={"data":{"due_on":null,"id":2,"label":"p2"},"meta":{"labels":{}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
The same run also failed `TestRelationChildScopeSessionKey/foreign_admin` (admin B read admin A's pending part). The file was restored with `git checkout`, and `git diff --quiet` confirmed it.
|
||||||
|
|
||||||
|
Paths below are relative to the summercms.go root. `cabana/`, `lagoon/`, `attach/`, `conga/` and `pact/` mean `modules/cabana/`, `modules/lagoon/`, `modules/lagoon/attach/`, `modules/conga/` and `modules/pact/`.
|
||||||
|
|
||||||
|
## Threat register
|
||||||
|
|
||||||
|
| Threat | Severity | Disposition | Mitigation as built (file, function) | Executable evidence (file, test) | Result |
|
||||||
|
|--------|----------|-------------|---------------------------------------|----------------------------------|--------|
|
||||||
|
| T-12.2-01 | high | mitigated | `attach/store.go` `Store`, `writeBlob`: 1 MiB bounded peek refuses an over-limit head before any write; `LimitReader(MaxBytes+1)` while streaming; the key is deleted on overflow | `attach/store_test.go` `TestStore/size` (exact, +1, past the read-ahead, 0 = no limit), `TestStoreSmokeRefusals` | pass |
|
||||||
|
| T-12.2-02 | high | mitigated | `attach/guard.go` `IsAllowedImage` (sniff, `image.DecodeConfig`, positive size, 4096x4096 ceiling, fail closed); `attach/store.go` `DefaultFileExtensions` without svg, js, map, css, less, scss, swf, xml | `attach/guard_test.go` `TestIsAllowedImageRefuses` (SVG, HTML, GIF and PNG signature polyglots, truncated, empty, over the ceiling, zero width, BMP), `TestIsAllowedImageAccepts`; `attach/store_test.go` `TestStore/default_lists`; `cabana/fileupload_test.go` `TestFileuploadUpload` (polyglot 422 on the field) | pass; residual noted below |
|
||||||
|
| T-12.2-03 | high | mitigated | `attach/store.go` `newDiskName` (22 hex + validated extension), `clientBaseName` (base name only in file_name) | `attach/store_test.go` `TestStore/extensions` (a `..\..\Uploads/PHOTO.PNG` name: disk name shape, no client path in the key) | pass |
|
||||||
|
| T-12.2-04 | high | mitigated | `lagoon/purge.go` `PurgeDeferred`, `purgeBinding`, `deleteCreatedChild` (created envelope only, unattached files only, SKIP LOCKED, unresolvable types skipped) | `lagoon/purge_test.go` `TestPurgeDeferredRules`, `TestPurgeDeferredSkipsLocked`, `TestPurgeDeferredCutoff` | pass |
|
||||||
|
| T-12.2-05 | medium | mitigated | `lagoon/purge.go` (keys collected, `AfterCommit` + `attach.DeleteKeys`); `cabana/field_file.go` `deleteBlobsAfterCommit`; `cabana/deferred.go` `deleteFile` | `lagoon/purge_test.go` `TestPurgeDeferredBlobsAfterCommit` (blob kept inside the transaction and after a rollback); `cabana/fileupload_test.go` `TestFileuploadRemove`, `TestFileuploadAttachOneReplace` | pass |
|
||||||
|
| T-12.2-06 | high | mitigated | `lagoon/deferred.go` `DeferredKey.validate`; `lagoon/deferred_migrations.go` `backend_user_id INTEGER NOT NULL` | `lagoon/deferred_test.go` `TestDeferredStore/refusals`, `TestDeferredMigrations` (NOT NULL insert refused) | pass |
|
||||||
|
| T-12.2-07 | medium | mitigated | `conga/scheduler.go` `scheduleEntries` (framework entry in the compiled table); `runScheduled` matches exact entries | `conga/schedule_test.go` `TestFrameworkScheduleForgedArgs` (other args, other command, other index skipped; the exact entry runs), `TestFrameworkScheduleEntries` | pass |
|
||||||
|
| T-12.2-08 | low | accepted | Text parsing runs only on string sources already bounded by the request body cap; the parsers are linear stdlib parsers | `lagoon/fill_test.go` `TestFillTextDateTypes`, `TestFillTextKeepsEarlierConversions` (behaviour, not DoS) | accepted |
|
||||||
|
| T-12.2-09 | high | mitigated | `cabana/deferred.go` `sessionKeyFrom`, `commitDeferred` (key, bouncer admin id, morph type); `cabana/field_file.go` `parentFileScope`, `findFile`; `cabana/relation_child.go` `loadParent` | `cabana/relation_child_scope_test.go` `TestRelationChildScopeSessionKey/foreign_admin`; `cabana/protected_file_test.go` `TestProtectedFileScope/pending_file_of_another_admin`; `cabana/deferred_commit_test.go` `TestDeferredCommitAppliedOnly` (another admin's binding with the same key stays) | pass |
|
||||||
|
| T-12.2-10 | high | mitigated | `cabana/deferred.go` `commitDeferred` (controller morph type, declared fields and relations allowed in the operation context) | `cabana/deferred_commit_test.go` `TestDeferredCommitAppliedOnly` (undeclared field, foreign morph type and update-only field bindings ignored, slaves untouched) | pass |
|
||||||
|
| T-12.2-11 | high | mitigated | `cabana/field_file.go` `fileScope.findFile` (one parent-scoped query), `parentFileScope` (owner through `loadRecord`/FormExtendQuery) | `cabana/protected_file_test.go` `TestProtectedFileScope` (download, thumb, caption, remove through another gadget: 404, no change; reorder: the same 422 as an unknown id) | pass |
|
||||||
|
| T-12.2-12 | high | mitigated | `cabana/field_file.go` `serveProtectedFileOn` (inline only for `attach.AllowedImageMIMEs`, else octet-stream attachment; nosniff, `private, no-store`, `default-src 'none'; sandbox`) | `cabana/protected_file_test.go` `TestProtectedFileHeaders` (SVG, HTML, text as attachments; PNG, GIF, JPEG, WebP inline; headers on every response and thumb) | pass |
|
||||||
|
| T-12.2-13 | medium | mitigated | `cabana/field_file.go` `fileItem` (no `url`/`thumb_url` for a protected relation); `attach/static.go` `StaticHandlerPublic` | `cabana/protected_file_test.go` `TestProtectedFileListHasNoURLs`, `TestProtectedFileScope/public_rows`; `attach/static_test.go` `TestStaticHandlerPublicGate` | pass |
|
||||||
|
| T-12.2-14 | high | mitigated | `cabana/field_file.go` `uploadCap` (min of upload_bytes and maxFilesize + 64 KiB), `writeFileError` (413); one `file_data` part | `cabana/fileupload_test.go` `TestFileuploadUpload` (413 past the cap, 422 between maxFilesize and the cap, state unchanged) | pass |
|
||||||
|
| T-12.2-15 | high | mitigated | `cabana/csrf.go` `requireAjax` on upload, reorder, caption, remove and every child write route | `cabana/phase10_csrf_test.go` `TestPhase10CSRF`; `cabana/security_coverage_test.go` `TestPhase09PermissionMatrix`; `cabana/phase10_coverage_test.go` `TestPhase10Coverage` | pass |
|
||||||
|
| T-12.2-16 | medium | mitigated | `lagoon/deferred.go` `DeferredBindings` (FOR UPDATE); `cabana/deferred.go` `commitDeferred` (applied rows forgotten in the same transaction) | `cabana/deferred_commit_test.go` `TestDeferredCommitConcurrent` (two concurrent saves with one key: file, part and pivot applied once, to one gadget); `lagoon/deferred_test.go` `TestDeferredConcurrentFirstBind` (documents the duplicate first-bind gap, below) | pass |
|
||||||
|
| T-12.2-17 | medium | mitigated | `cabana/field_date.go` `dateBoundDetails` (in the save and the child save) | `cabana/datepicker_test.go` `TestDatepickerBounds` (inclusive edges, UTC date of a datetime, wall-clock date with ignoreTimezone, create and update); `cabana/deferred_commit_test.go` `TestDeferredCommitRollback/datepicker_bound` | pass |
|
||||||
|
| T-12.2-18 | medium | mitigated | `cabana/field_file.go` `decodeStrictBody` (default_bytes cap, DisallowUnknownFields, trailing data); `cabana/relation_child.go` `decodeCappedObject` | `cabana/fileupload_test.go` `TestFileuploadCaption` (unknown key 422), `TestFileuploadReorder`; `cabana/relation_child_test.go` `TestRelationChildCRUD/body_cap` (413 past default_bytes on a child body) | pass |
|
||||||
|
| T-12.2-19 | high | mitigated | `cabana/relation_child.go` `loadChild` (FK, pivot EXISTS or the admin's bound slaves in the same query), `childFileScope` | `cabana/relation_child_scope_test.go` `TestRelationChildScope` (all 12 child routes, the seven child file routes among them, through another parent: 404 `not_found`, database, files and blobs unchanged); the removal check above | pass |
|
||||||
|
| T-12.2-20 | high | mitigated | `cabana/relation_form.go` `compileRelationForm` (pivot keys, timestamps and HookPivotColumns refused at boot); `cabana/relation.go` `fillPivot` (unknown keys 422), `insertPivot` (RelationBeforeLink stamps) | `cabana/relation_child_scope_test.go` `TestRelationChildScopePivotWhitelist` (gadget_id, member_id, id, created_at, role: 422, row unchanged); `cabana/relation_child_test.go` `TestRelationChildForms` (pivot foreign key and hook column stop boot), `TestRelationChildCRUD/belongsToMany` (role stamped) | pass |
|
||||||
|
| T-12.2-21 | high | mitigated | `cabana/relation_child.go` `relationAllowed`/`relationButton`, `pivotAllowed`; `cabana/http.go` `relationMutation` (toolbar button before any SQL) | `cabana/relation_child_scope_test.go` `TestRelationChildScopeToolbar` (11 routes 403 on a parent id that does not exist, nothing changed; declared buttons reach the 404) | pass |
|
||||||
|
| T-12.2-22 | medium | mitigated | `cabana/relation.go` `excludePendingCreated`, hasMany candidates with a NULL key | `cabana/relation_child_test.go` `TestRelationChildCRUD/hasMany` (owned part not a candidate, link refused), `TestRelationChildDeferred` (pending part excluded); `cabana/relation_child_smoke_test.go` `TestRelationChildSmokeDeferredCreate` | pass |
|
||||||
|
| T-12.2-23 | high | mitigated | `cabana/relation_child.go` `loadParent` through `loadRecord` (FormExtendQuery) | `cabana/relation_child_scope_test.go` `TestRelationChildScope/hidden_parent` (every child route 404, unchanged) | pass |
|
||||||
|
| T-12.2-24 | high | mitigated | `cabana/relation_child.go` `loadParent` (id 0 needs key, deferrable relation, create, create context, backend admin), `relationParent.boundSlaves` | `cabana/relation_child_scope_test.go` `TestRelationChildScopeSessionKey` (no key 404 on 13 routes; malformed 422 `session_key`; admin B: empty lists, 404 on show, update, delete, pivot and every child file route; B's save adopts nothing) | pass |
|
||||||
|
| T-12.2-25 | medium | mitigated | `cabana/deferred.go` `applyRelationBinding` (bind of an existing record re-runs `linkRelated` with the saved parent) | `cabana/deferred_commit_test.go` `TestDeferredCommitRollback/ineligible_link` (422 on members, state unchanged); `cabana/relation_child_smoke_test.go` `TestRelationChildSmokeDeferredRollback` | pass |
|
||||||
|
| T-12.2-26 | medium | mitigated | `cabana/relation_form.go` `compileRelationForm` via `assetPath` (`$/` only inside the plugin) | `cabana/relation_child_test.go` `TestRelationChildForms/cross-plugin_path` | pass |
|
||||||
|
| T-12.2-27 | medium | mitigated | `cabana/relation_form.go` `relationFormRefusedTypes` | `cabana/relation_child_test.go` `TestRelationChildForms` (relation, relation-manager, widget and partial fields stop boot) | pass |
|
||||||
|
| T-12.2-28 | high | mitigated | `cabana/relation_child.go` `CreateChild` (`setModelColumn` from the scoped parent), `relation_form.go` (a field named like the ForeignKey stops boot), `ProjectWritableFields` | `cabana/relation_child_test.go` `TestRelationChildCRUD/hasMany` (body `gadget_id` of another gadget ignored), `TestRelationChildForms/foreign_key_field`; `cabana/relation_child_smoke_test.go` `TestRelationChildSmokeScope` | pass |
|
||||||
|
| T-12.2-29 | medium | mitigated | `admin/src/app/sessionKey.ts` `newSessionKey` (32 bytes, `crypto.getRandomValues`, base64url) | `admin/tests/app/sessionKey.test.ts` (43 chars, alphabet, crypto source used, 200 unique keys); `RelationChildModal.test.ts` (new child key per open) | pass |
|
||||||
|
| T-12.2-30 | high | mitigated | Text interpolation only in the new components; no raw-HTML sink in `admin/src` | `scripts/check-phase12.2.sh --hygiene` (raw-HTML sink grep); `admin/tests/list/CellValue.test.ts` (never renders markup) | pass |
|
||||||
|
| T-12.2-31 | low | mitigated | `FileuploadField.vue` `loadThumb`, `forgetThumb`, `onBeforeUnmount` (object URLs revoked) | `admin/tests/form/FileuploadField.test.ts` "protected thumbnails (backstop 3)", "file rows of a protected field" (download object URL revoked on unmount) | pass |
|
||||||
|
| T-12.2-32 | high | mitigated | `admin/src/api/files.ts` `uploadWithProgress` (X-Requested-With on every upload) | `admin/tests/smoke/deferred.smoke.test.ts` "uploads to record 0 with the form key" (header asserted); server side `TestPhase10CSRF` | pass |
|
||||||
|
| T-12.2-33 | low | mitigated | Keys travel in `X-Session-Key` and `X-Child-Session-Key` headers only | `scripts/check-phase12.2.sh --hygiene` (no key query parameter in admin/src or cabana); `FileuploadField.test.ts` (no query string on the thumb request); `deferred.smoke.test.ts` | pass |
|
||||||
|
| T-12.2-34 | low | mitigated | Fixture plugin `acme.deferred` only in `cabana/phase122_fixture_test.go` and `cabana/testdata/deferred` | `scripts/check-phase12.2.sh --hygiene` (no `acme.deferred`/`dfPlugin` in non-test Go files) | pass |
|
||||||
|
| T-12.2-35 | high | mitigated | Named security tests run by the gate's `--security` stage, which refuses a missing, renamed or skipped test | `scripts/check-phase12.2.sh --security` and `--self-test` (detector refuses skip, zero tests, no tests to run, a subtest-only match and a missing named prefix); planted missing name refused with exit 1; the removal check above | pass |
|
||||||
|
| T-12.2-36 | medium | mitigated | The executor does not tag or push. The user tags at the blocking-human checkpoint of plan 05 Task 4 | `git tag --list v0.1.1` is empty when the checkpoint is presented | pending user (checkpoint) |
|
||||||
|
| T-12.2-SC (plans 01, 02, 03, 05) | low | accepted | No Go module and no npm package added in these plans; go.mod and go.sum unchanged | `git diff 79e2a43..HEAD -- go.mod go.sum` shows no change in this phase | accepted |
|
||||||
|
| T-12.2-SC (plan 04) | high | mitigated | `@internationalized/date` 3.12.4 added only after the user approved it at a blocking-human decision checkpoint (STATE.md decision); exact pin; integrity hash in the committed package-lock.json | `admin/package.json` pin `3.12.4`; `scripts/check-admin-dist.sh` (rebuild from the committed lockfile) | pass |
|
||||||
|
|
||||||
|
## Residual risk
|
||||||
|
|
||||||
|
- **Header-only image guard (T-12.2-02).** `IsAllowedImage` checks the type sniff and the decoded header only. A GIF with a valid 1x1 header followed by HTML is accepted and stored as `image/gif`. This was probed in this plan and is not pinned by a test. The protected route serves it as `image/gif` with nosniff and a sandbox CSP, and browsers do not sniff image types into HTML, so no script runs. The public `StaticHandler`/`StaticHandlerPublic` sends the stored content type without `X-Content-Type-Options: nosniff`. That handler predates this phase. It is recorded in `deferred-items.md` as a hardening follow-up and was not changed here.
|
||||||
|
- **Concurrent first binds (T-12.2-16).** Two transactions that bind the same slave for the first time can both insert a binding, because no unique index exists. WinterCMS has the same gap. `TestDeferredConcurrentFirstBind` documents this. The duplicates are harmless: a save reads both, applies them idempotently and forgets both. A unique index needs a decision and a migration, so it is left to the user (see the 12.2-05 summary).
|
||||||
|
- **Browser checks.** Calendar keyboard use and visual fit, drag reorder and progress feel, and the modal flow are manual checks for `/gsd-verify-work 12.2`. This review does not claim them.
|
||||||
@@ -3,9 +3,9 @@ phase: "12.2"
|
|||||||
slug: "admin-form-fields-date-file-upload-relation-editing-with-def"
|
slug: "admin-form-fields-date-file-upload-relation-editing-with-def"
|
||||||
# status lifecycle: draft (seeded by plan-phase) → validated (set by validate-phase §6)
|
# status lifecycle: draft (seeded by plan-phase) → validated (set by validate-phase §6)
|
||||||
# audit-milestone §5.5 distinguishes NOT-VALIDATED (draft) from PARTIAL (validated + nyquist_compliant: false) (#2117)
|
# audit-milestone §5.5 distinguishes NOT-VALIDATED (draft) from PARTIAL (validated + nyquist_compliant: false) (#2117)
|
||||||
status: draft
|
status: validated
|
||||||
nyquist_compliant: false
|
nyquist_compliant: true
|
||||||
wave_0_complete: false
|
wave_0_complete: true
|
||||||
created: "2026-10-02"
|
created: "2026-10-02"
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -38,24 +38,46 @@ created: "2026-10-02"
|
|||||||
|
|
||||||
## Per-Task Verification Map
|
## Per-Task Verification Map
|
||||||
|
|
||||||
Task IDs are filled in once PLAN.md files exist. The behaviour-to-command map from RESEARCH.md is the contract:
|
Each row maps one task to the commands that verify it. Every command was run green in plan 12.2-05 (2026-10-02), and the whole map is run by `bash scripts/check-phase12.2.sh --all` (all nine stages PASS). Run Go tests with FORCE_COLOR unset.
|
||||||
|
|
||||||
|
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||||
|
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||||
|
| 12.2-01-T1 | 01 | 1 | SC-2, SC-4 | T-12.2-01..06 | bindings owned per admin; guarded store; purge deletes blobs after commit | unit + PG | `go test ./modules/lagoon ./modules/lagoon/attach -run '^(TestDeferred.*|TestPurgeDeferred.*|TestStore.*|TestIsAllowedImage.*)$' -count=1` | ✅ | ✅ green |
|
||||||
|
| 12.2-01-T2 | 01 | 1 | SC-1 | T-12.2-08 | N/A | unit + PG | `go test ./modules/lagoon -run '^(TestDate.*|TestTimeOfDay.*|TestFillText.*|TestValidateRequiredZero.*)$' -count=1` | ✅ | ✅ green |
|
||||||
|
| 12.2-01-T3 | 01 | 1 | SC-4 | T-12.2-07 | forged scheduled jobs skipped | unit + PG | `go test ./modules/conga ./modules/pact ./modules/lagoon -run '^(TestFrameworkSchedule.*|TestRelationHookInterfaces|TestPurgeDeferredDays|TestPurgeDeferredCommand)$' -count=1` | ✅ | ✅ green |
|
||||||
|
| 12.2-02-T1 | 02 | 2 | SC-2, SC-4 | T-12.2-09, -14, -15 | foreign key replay finds nothing; 413 past the cap | integration | `go test ./modules/cabana -run '^(TestFileuploadUpload|TestFileuploadCompile|TestFileuploadSmoke.*)$' -count=1` | ✅ | ✅ green |
|
||||||
|
| 12.2-02-T2 | 02 | 2 | SC-2, SC-4 | T-12.2-10..13, -16, -18 | parent-scoped file ids; protected download headers; commit once | integration + security | `go test ./modules/cabana -run '^(TestProtectedFile.*|TestFileupload(Remove|Caption|Reorder|AttachOneReplace|MIME)|TestDeferredCommit.*)$' -count=1` | ✅ | ✅ green |
|
||||||
|
| 12.2-02-T3 | 02 | 2 | SC-1 | T-12.2-17 | min/max enforced on the server (422) | integration | `go test ./modules/cabana -run '^TestDatepicker.*$' -count=1` | ✅ | ✅ green |
|
||||||
|
| 12.2-03-T1 | 03 | 3 | SC-3 | T-12.2-26..28 | `$/` inside the plugin; D-23 refusals; FK set by the server | integration | `go test ./modules/cabana -run '^(TestRelationChild(Contract|ZeroKind|Forms|Schema)|TestRelationChildSmokeCreate)$' -count=1` | ✅ | ✅ green |
|
||||||
|
| 12.2-03-T2 | 03 | 3 | SC-3 | T-12.2-19..23 | child of another parent or a hidden parent 404; toolbar 403 before SQL; pivot whitelist | security | `go test ./modules/cabana -run '^(TestRelationChildScope.*|TestRelationChildCRUD)$' -count=1` | ✅ | ✅ green |
|
||||||
|
| 12.2-03-T3 | 03 | 3 | SC-4 | T-12.2-24, -25 | another admin's pending children 404; ineligible deferred link 422 keeps bindings | integration + security | `go test ./modules/cabana -run '^(TestRelationChildDeferred|TestRelationChildPurgeModels|TestRelationChildScopeSessionKey|TestDeferredCommitRollback)$' -count=1` | ✅ | ✅ green |
|
||||||
|
| 12.2-04-T1 | 04 | 4 | SC-2 | T-12.2-29, -31..33 | session key in headers only; protected thumbs from object URLs | SPA unit | `npm --prefix admin test -- tests/app/sessionKey.test.ts tests/form/FileuploadField.test.ts tests/smoke/deferred.smoke.test.ts` | ✅ | ✅ green |
|
||||||
|
| 12.2-04-T2 | 04 | 4 | — | T-12.2-SC | exact pin after user approval | checkpoint (decision) | `scripts/check-admin-dist.sh` (rebuild from the committed lockfile) | ✅ | ✅ green (user approved) |
|
||||||
|
| 12.2-04-T3 | 04 | 4 | SC-1 | — | N/A | SPA unit | `npm --prefix admin test -- tests/app/dateFormat.test.ts tests/form/DatepickerField.test.ts tests/list/CellValue.test.ts` | ✅ | ✅ green |
|
||||||
|
| 12.2-04-T4 | 04 | 4 | SC-3, SC-4 | T-12.2-30 | no raw-HTML sinks; child modal sends both keys | SPA unit | `npm --prefix admin test -- tests/relation` and `bash scripts/check-phase12.2.sh --hygiene` | ✅ | ✅ green |
|
||||||
|
| 12.2-05-T1 | 05 | 5 | SC-3 | T-12.2-19, -23, -24, -35 | D-15 suite through the router; fails with the parent predicate removed | security | `go test ./modules/cabana -run '^(TestRelationChildScope.*|TestProtectedFile.*)$' -count=1 -v` | ✅ | ✅ green |
|
||||||
|
| 12.2-05-T2 | 05 | 5 | SC-1..4 | all Go rows | every Go behaviour tested | unit + integration | `go vet ./... && go test ./... -count=1` | ✅ | ✅ green |
|
||||||
|
| 12.2-05-T3 | 05 | 5 | SC-5 | T-12.2-34, -35 | named security tests refused when missing or skipped; fixture test-only | gate + docs | `bash scripts/check-phase12.2.sh --all` (includes `go test ./cmd/summer -run TestDocsTree` and `go run ./cmd/summer docs:build --check`) | ✅ | ✅ green |
|
||||||
|
| 12.2-05-T4 | 05 | 5 | — | T-12.2-36 | the user tags v0.1.1; the executor never tags | checkpoint (human-action) | `git tag --list v0.1.1` (empty until the user tags) | ✅ | ⬜ awaiting user |
|
||||||
|
|
||||||
|
Earlier behaviour-to-command contract (RESEARCH.md), all rows now green:
|
||||||
|
|
||||||
| Area | Success criterion | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
| Area | Success criterion | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||||
|------|-------------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
|------|-------------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||||
| datepicker compile | SC-1 | — | unknown key / mode-type mismatch fails boot | unit | `go test -short ./modules/cabana -run TestDatepicker` | ❌ W0 | ⬜ pending |
|
| datepicker compile | SC-1 | — | unknown key / mode-type mismatch fails boot | unit | `go test ./modules/cabana -run TestDatepicker` | ✅ | ✅ green |
|
||||||
| Date / TimeOfDay / Fill | SC-1 | — | N/A | unit + PG | `go test ./modules/lagoon -run 'TestDate\|TestTimeOfDay\|TestFillText'` | ❌ W0 | ⬜ pending |
|
| Date / TimeOfDay / Fill | SC-1 | — | N/A | unit + PG | `go test ./modules/lagoon -run 'TestDate\|TestTimeOfDay\|TestFillText'` | ✅ | ✅ green |
|
||||||
| date bounds | SC-1 | — | min/max enforced server-side (422) | integration | `go test ./modules/cabana -run TestDatepickerBounds` | ❌ W0 | ⬜ pending |
|
| date bounds | SC-1 | — | min/max enforced server-side (422) | integration | `go test ./modules/cabana -run TestDatepickerBounds` | ✅ | ✅ green |
|
||||||
| attach.Store | SC-2 | upload | size/MIME/image guard, polyglot refused | unit + PG | `go test ./modules/lagoon/attach -run TestStore` | ❌ W0 | ⬜ pending |
|
| attach.Store | SC-2 | upload | size/MIME/image guard, polyglot refused | unit + PG | `go test ./modules/lagoon/attach -run 'TestStore\|TestIsAllowedImage'` | ✅ | ✅ green |
|
||||||
| fileupload routes | SC-2 | upload | MaxBytesReader 413, maxFiles, deferred bind | integration | `go test ./modules/cabana -run TestFileupload` | ❌ W0 | ⬜ pending |
|
| fileupload routes | SC-2 | upload | MaxBytesReader 413, maxFiles, deferred bind | integration | `go test ./modules/cabana -run TestFileupload` | ✅ | ✅ green |
|
||||||
| protected file download | SC-2 | IDOR | 404 for other parent / other admin's pending file; nosniff | security | `go test ./modules/cabana -run TestProtectedFile` | ❌ W0 | ⬜ pending |
|
| protected file download | SC-2 | IDOR | 404 for other parent / other admin's pending file; nosniff | security | `go test ./modules/cabana -run TestProtectedFile` | ✅ | ✅ green |
|
||||||
| relation child CRUD | SC-3 | mass assign | toolbarButtons gate, pivot whitelist | integration | `go test ./modules/cabana -run TestRelationChild` | ❌ W0 | ⬜ pending |
|
| relation child CRUD | SC-3 | mass assign | toolbarButtons gate, pivot whitelist | integration | `go test ./modules/cabana -run TestRelationChild` | ✅ | ✅ green |
|
||||||
| parent scoping (D-15) | SC-3 | IDOR | child of another parent → 404 | security | `go test ./modules/cabana -run TestRelationChildScope` | ❌ W0 | ⬜ pending |
|
| parent scoping (D-15) | SC-3 | IDOR | child of another parent → 404 | security | `go test ./modules/cabana -run TestRelationChildScope` | ✅ | ✅ green |
|
||||||
| deferred commit | SC-4 | session replay | foreign admin key ignored; rollback keeps bindings; double submit serialized | integration | `go test ./modules/cabana -run TestDeferredCommit` | ❌ W0 | ⬜ pending |
|
| deferred commit | SC-4 | session replay | foreign admin key ignored; rollback keeps bindings; double submit serialized | integration | `go test ./modules/cabana -run TestDeferredCommit` | ✅ | ✅ green |
|
||||||
| purge | SC-4 | storage DoS | expired bindings + orphans removed, blobs after commit | integration | `go test ./modules/lagoon -run TestPurgeDeferred` | ❌ W0 | ⬜ pending |
|
| purge | SC-4 | storage DoS | expired bindings + orphans removed, blobs after commit | integration | `go test ./modules/lagoon -run TestPurgeDeferred` | ✅ | ✅ green |
|
||||||
| framework schedule | SC-4 | — | N/A | unit | `go test -short ./modules/conga -run TestFrameworkSchedule` | ❌ W0 | ⬜ pending |
|
| framework schedule | SC-4 | — | N/A | unit | `go test ./modules/conga -run TestFrameworkSchedule` | ✅ | ✅ green |
|
||||||
| route inventory + OpenAPI | SC-3/4 | — | every route permission-checked | contract | `go test -short ./modules/cabana -run 'TestPhase09PermissionMatrix\|TestPhase09ContractInventory\|TestOpenAPI'` | ✅ | ⬜ pending |
|
| route inventory + OpenAPI | SC-3/4 | — | every route permission-checked | contract | `go test ./modules/cabana -run 'TestPhase09PermissionMatrix\|TestPhase09ContractInventory\|TestPhase10OpenAPIConformance'` | ✅ | ✅ green |
|
||||||
| SPA fields + modals | SC-1/2/3 | — | session key header sent | SPA unit | `npm --prefix admin test -- tests/form tests/relation` | ❌ W0 | ⬜ pending |
|
| SPA fields + modals | SC-1/2/3 | — | session key header sent | SPA unit | `npm --prefix admin test -- tests/form tests/relation tests/app tests/list` | ✅ | ✅ green |
|
||||||
| docs checker | SC-5 | — | N/A | docs | `go test ./cmd/summer -run TestDocsTree && go run ./cmd/summer docs:build --check` | ✅ | ⬜ pending |
|
| docs checker | SC-5 | — | N/A | docs | `go test ./cmd/summer -run TestDocsTree && go run ./cmd/summer docs:build --check` | ✅ | ✅ green |
|
||||||
|
|
||||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||||
|
|
||||||
@@ -63,9 +85,9 @@ Task IDs are filled in once PLAN.md files exist. The behaviour-to-command map fr
|
|||||||
|
|
||||||
## Wave 0 Requirements
|
## Wave 0 Requirements
|
||||||
|
|
||||||
- [ ] `modules/cabana/testdata/` fixture plugin: parent model (attach owner + attach relations), hasMany child with nullable FK, belongsToMany with pivot form, `config_relation.yaml` using `manage.form` / `pivot.form`
|
- [x] `modules/cabana/testdata/deferred` fixture plugin `acme.deferred` (`phase122_fixture_test.go`): parent model (attach owner and attach relations), hasMany child with a nullable FK, belongsToMany with a pivot form, `config_relation.yaml` using `manage.form` and `pivot.form` (12.2-05 Task 1)
|
||||||
- [ ] `phase09Routes` in `modules/cabana/security_coverage_test.go` extended in the same commit as each new route
|
- [x] `phase09Routes` in `modules/cabana/security_coverage_test.go` extended in the same commit as each new route (plans 02 and 03)
|
||||||
- [ ] `admin/tests/fixtures/` entries for the new schema fields and file list
|
- [x] `admin/tests/fixtures/` entries for the new schema fields and file list: `deferred.form-schema.json` (plan 04), `deferred.relation-schema.json` and `deferred.files.json` (12.2-05)
|
||||||
|
|
||||||
*No framework install needed.*
|
*No framework install needed.*
|
||||||
|
|
||||||
@@ -77,16 +99,17 @@ Task IDs are filled in once PLAN.md files exist. The behaviour-to-command map fr
|
|||||||
|----------|-------------|------------|-------------------|
|
|----------|-------------|------------|-------------------|
|
||||||
| Datepicker keyboard / a11y and visual fit with Direction C | SC-1 | Visual and interaction quality | Open a form with each mode in the admin SPA; navigate the calendar by keyboard; check against UI-SPEC |
|
| Datepicker keyboard / a11y and visual fit with Direction C | SC-1 | Visual and interaction quality | Open a form with each mode in the admin SPA; navigate the calendar by keyboard; check against UI-SPEC |
|
||||||
| Drag reorder and upload progress feel | SC-2 | Pointer interaction | Upload several images to an attachMany field on an unsaved record, reorder, save, reload |
|
| Drag reorder and upload progress feel | SC-2 | Pointer interaction | Upload several images to an attachMany field on an unsaved record, reorder, save, reload |
|
||||||
|
| Child and pivot modal flow | SC-3, SC-4 | Visual flow across modals | On the create screen add children with an uploaded image and a link with pivot details, save; then edit a child, edit pivot details and delete a child on the saved record |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Validation Sign-Off
|
## Validation Sign-Off
|
||||||
|
|
||||||
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
|
- [x] All tasks have `<automated>` verify or Wave 0 dependencies
|
||||||
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
|
- [x] Sampling continuity: no 3 consecutive tasks without automated verify
|
||||||
- [ ] Wave 0 covers all MISSING references
|
- [x] Wave 0 covers all MISSING references
|
||||||
- [ ] No watch-mode flags
|
- [x] No watch-mode flags
|
||||||
- [ ] Feedback latency < 240s
|
- [x] Feedback latency < 240s (`check-phase12.2.sh --all` about 200 s with Docker up)
|
||||||
- [ ] `nyquist_compliant: true` set in frontmatter
|
- [x] `nyquist_compliant` set to true in the frontmatter
|
||||||
|
|
||||||
**Approval:** pending
|
**Approval:** automated map green in plan 12.2-05 (2026-10-02). The manual-only rows wait for `/gsd-verify-work 12.2`.
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Phase 12.2 deferred items
|
||||||
|
|
||||||
|
Things found during plan 12.2-05 that are outside this phase's scope. None of them was changed here.
|
||||||
|
|
||||||
|
| Item | Where | Why deferred | Suggested follow-up |
|
||||||
|
|------|-------|--------------|---------------------|
|
||||||
|
| The public static handler sends no `X-Content-Type-Options: nosniff` | `modules/lagoon/attach/static.go` `servePublicBlobs` (Phase 5/12 code) | It predates this phase and is not in its threat register. The stored content type comes from the sniff, and the protected admin route already sends nosniff | Add `X-Content-Type-Options: nosniff` to `StaticHandler`/`StaticHandlerPublic` responses (one header, plus a test) |
|
||||||
|
| `IsAllowedImage` checks the header only: a valid GIF header followed by HTML passes | `modules/lagoon/attach/guard.go` | Header-only by design (it gets the 1 MiB read-ahead). The content is served as `image/gif`, and browsers do not sniff images into HTML | Consider a full decode for small images, or re-encoding image uploads, if polyglots matter beyond content-type safety |
|
||||||
|
| No unique index on a first bind | `deferred_bindings` (12.2-01) | Two concurrent first binds of the same slave can both insert. WinterCMS has the same gap. The duplicates are harmless (`TestDeferredConcurrentFirstBind`) | A user decision: keep it, or add a partial unique index on (session_key, backend_user_id, master_type, master_field, slave_type, slave_id, is_bind) in a new migration |
|
||||||
|
| GORM reads a bare `type:time` tag as its own time type | Test models only | `AutoMigrate` with `gorm:"type:time"` creates `timestamptz`. Framework migrations are hand-written SQL, so production is unaffected | A docs note in `docs/database/casts-and-validation.md` that an AutoMigrate'd `lagoon.TimeOfDay` column needs `type:time without time zone` |
|
||||||
|
| `lagoon.Date` accepts a timestamp string, `*lagoon.Date` does not | `modules/lagoon/fill.go` (Scan fallback only for non-pointer fields) | A plain Date falls back to `Date.Scan`, which accepts the driver's `YYYY-MM-DDT...` form. The SPA always sends `YYYY-MM-DD` | Make the two variants consistent, either way, if an API client ever sends timestamps to date fields |
|
||||||
Reference in New Issue
Block a user