diff --git a/.planning/phases/14.2.1-translate-plugin/14.2.1-SECURITY-REVIEW.md b/.planning/phases/14.2.1-translate-plugin/14.2.1-SECURITY-REVIEW.md index 116bcf6..277a77b 100644 --- a/.planning/phases/14.2.1-translate-plugin/14.2.1-SECURITY-REVIEW.md +++ b/.planning/phases/14.2.1-translate-plugin/14.2.1-SECURITY-REVIEW.md @@ -2,26 +2,26 @@ phase: 14.2.1 slug: translate-plugin status: verified -threats_total: 19 -threats_closed: 19 +threats_total: 24 +threats_closed: 24 threats_open: 0 accepted_risks: 0 asvs_level: 1 created: 2026-10-06 verified: 2026-10-06 -reviewer: gsd-executor (14.2.1-04 Task 3, self-performed -- see Reviewer Note) +reviewer: gsd-executor (14.2.1-04 Task 3 and 14.2.1-06 Task 3, self-performed -- see Reviewer Note) --- # Phase 14.2.1 — Security Review > Lean Translate plugin (`sm-translate-plugin`), cabana `markdown`/`mltext`/`mlmarkdown`, > surf `LocaleResolver`, and the proof-host boot of user+translate. Every -> T-14.2.1 threat locked in plans 01–04 is mapped below to executed, named Go +> T-14.2.1 threat locked in plans 01–04 and 06 is mapped below to executed, named Go > or Vitest evidence. Unmapped IDs would be a review gap, not an accepted > risk; none exist. **Date:** 2026-10-06 -**Scope:** Plans 14.2.1-01 through 14.2.1-04; `sm-translate-plugin`; +**Scope:** Plans 14.2.1-01 through 14.2.1-06; `sm-translate-plugin`; `summercms.go` modules `cabana` and `surf` plus admin SPA ML/markdown controls; proof host `sm-grzybyfunkcjonalne-app`. **Repos grepped:** `sm-translate-plugin`, `summercms.go` (excluding @@ -32,8 +32,9 @@ controls; proof host `sm-grzybyfunkcjonalne-app`. 14.2.1-04-PLAN.md Task 3 calls for an independent `gsd-security-auditor` agent pass. This Cursor session has no dedicated security-auditor subagent (same fallback as Phase 08): the 14.2.1-04 executor performed the review -directly. Every high threat below is closed with source citations and named -tests **re-executed during this review** (2026-10-06), not merely inherited +directly, and 14.2.1-06 Task 3 appended CR-01/WR-02 rows the same way. Every +high threat below is closed with source citations and named tests +**re-executed during this review** (2026-10-06), not merely inherited from earlier plans. The executor checkpoint return states this plainly. No external API integration: this phase ports a compiled plugin and local @@ -43,7 +44,7 @@ framework/host contracts only. ## Verdict Summary -The register contains **19 total threats: 19 closed, 0 open, 0 accepted +The register contains **24 total threats: 24 closed, 0 open, 0 accepted risks**. High findings block phase completion; all high rows are mitigate with executed named tests. PHP pin SHA `725d547ec839f02b5fdc0f0a6faaed601a414d50` is unchanged. @@ -85,6 +86,11 @@ is unchanged. | T-14.2.1-16 | Tampering | migration rollback | medium | mitigate | `TestTranslateMigrationsRollbackAndRemigrate` dedicated Postgres | | T-14.2.1-17 | Tampering | generated admin artifacts | medium | mitigate | `TestMLOpenAPIConformance`; `scripts/check-admin-openapi.sh --check`; `scripts/check-admin-dist.sh` | | T-14.2.1-18 | Elevation of Privilege | test fixture | high | mitigate | `TestTranslateEndToEndFixtureAbsentFromProduction`; host `plugins.gen.go` has no `acme.fixture` | +| T-14.2.1-19 | Information Disclosure | hydrateMLRecord / ShowChild | high | mitigate | `modules/cabana/field_ml.go` hydrates only declared ML fields on Show/save and child record payloads; `TestMLHydration`; `TestRelationChildMLNestedSave` | +| T-14.2.1-20 | Information Disclosure | FormMeta.EnabledLocales | medium | mitigate | `modules/cabana/http.go` copies locales only on protect()'d form/relation schemas; `admin/tests/form/formState.test.ts` seeds en+pl; `admin/tests/form/MLFields.test.ts` create-empty | +| T-14.2.1-21 | Tampering | FormView.adopt | medium | mitigate | `mergeMLValue` applies only to ML fields; `admin/tests/form/formState.test.ts` GET-string merge; `admin/tests/form/MLFields.test.ts` sibling locales | +| T-14.2.1-22 | Tampering | fillChild / CreateChild / UpdateChild | high | mitigate | `liftMLValues` before `projectOperation`; `TestRelationChildMLNestedSave` (de 422, no row) | +| T-14.2.1-23 | Elevation of Privilege | RelationService.writer | high | mitigate | `http.go` `relations()` Lookup on existing protect()'d child routes; apply after loadParent/loadChild and child PK; `TestRelationChildMLNestedSave` | | T-14.2.1-SC | Tampering | package installs | high | mitigate | no `go get` / npm install this plan; plugin `replace` of `sm-user-plugin` is a sibling checkout; framework `go.mod`/`admin/package-lock.json` unchanged | --- @@ -199,6 +205,36 @@ is unchanged. - **Test evidence (re-run 2026-10-06):** `TestTranslateEndToEndFixtureAbsentFromProduction` matched by `TestTranslateEndToEnd*` in the plugin `./...` run PASS; host `plugins.gen.go` lists only `golem15.user` and `golem15.translate`. - **Disposition:** closed / mitigate. +### T-14.2.1-19 — ML hydration disclosure + +- **Source:** `modules/cabana/field_ml.go` `hydrateMLRecord` walks declared `mltext`/`mlmarkdown` fields only; list projection never calls it; `TranslatedExact` skips D-11 so missing pl stays empty. ShowChild uses the same helper after `loadParent`/`loadChild`. +- **Test evidence (re-run 2026-10-06):** `TestMLHydration` PASS (english-only pl is `""`); `TestRelationChildMLNestedSave` PASS (create/update/show hydrated maps). +- **Disposition:** closed / mitigate. + +### T-14.2.1-20 — enabled locale list on schemas + +- **Source:** `modules/cabana/http.go` `enabledContentLocales` after Lookup, copied onto protect()'d form and relation schema meta; `FormMeta.EnabledLocales` is `json:"enabledLocales,omitempty"` so list Meta omits it. +- **Test evidence (re-run 2026-10-06):** `admin/tests/form/formState.test.ts` `seeds every enabled locale as an empty string`; `admin/tests/form/MLFields.test.ts` create-empty selectors list en+pl. +- **Disposition:** closed / mitigate. + +### T-14.2.1-21 — adopt merge of GET scalars + +- **Source:** `admin/src/components/form/formState.ts` `mergeMLValue` overlays a host string onto the seed for ML fields only; password fields still clear. +- **Test evidence (re-run 2026-10-06):** `admin/tests/form/formState.test.ts` `merges a GET host string onto the seed without dropping sibling locales`; `admin/tests/form/MLFields.test.ts` GET-string cases for mltext and mlmarkdown. +- **Disposition:** closed / mitigate. + +### T-14.2.1-22 — relation-child nested ML tampering + +- **Source:** `modules/cabana/relation_child.go` `CreateChild`/`UpdateChild` call `liftMLValues` on the child form before `fillChild`/`projectOperation`; undeclared locale, non-string, missing default, and nil writer with a nested map are 422 before Fill. +- **Test evidence (re-run 2026-10-06):** `TestRelationChildMLNestedSave` PASS (create+update persist en/pl; locale `de` is 422 and inserts no row). +- **Disposition:** closed / mitigate. + +### T-14.2.1-23 — RelationService writer privilege + +- **Source:** `modules/cabana/http.go` `relations()` Lookup of `TranslationWriter` matches `crud()`; no new HTTP route; `applyMLTranslations` runs after `loadParent`/`loadChild` and after the child row has a primary key. +- **Test evidence (re-run 2026-10-06):** `TestRelationChildMLNestedSave` PASS; `recordingWriter.WriteTranslated` refuses a zero PK. +- **Disposition:** closed / mitigate. + ### T-14.2.1-SC — package installs - **Source:** no new module versions beyond existing stack pins; plugin `replace` of `sm-user-plugin` points at the host submodule checkout; `gocloud.dev v0.46.0` is the already-decided blob pin. Framework `go.mod` / `admin/package-lock.json` were not changed in this phase. diff --git a/admin/tests/form/MLFields.test.ts b/admin/tests/form/MLFields.test.ts index ac9fce3..142d0a6 100644 --- a/admin/tests/form/MLFields.test.ts +++ b/admin/tests/form/MLFields.test.ts @@ -61,6 +61,35 @@ describe('ML field registry and nested save body', () => { }) }) + it('sends every enabled locale including empty pl', () => { + const fields = [field('mltext'), field('mlmarkdown', 'body')] + const payload = editablePayload(fields, { + title: { en: 'Hello', pl: '' }, + body: { en: '# Hi', pl: '' }, + }) + expect(payload).toEqual({ + title: { en: 'Hello', pl: '' }, + body: { en: '# Hi', pl: '' }, + }) + }) + + it('exposes one locale selector per ML field listing enabled locales', () => { + const title = mlMount(MLTextField, { + field: field('mltext'), + modelValue: { en: '', pl: '' }, + controlId: 'f-title', + }) + const body = mlMount(MLMarkdownField, { + field: field('mlmarkdown', 'body'), + modelValue: { en: '', pl: '' }, + controlId: 'f-body', + }) + expect(title.findAll('[data-ml-locale]')).toHaveLength(1) + expect(body.findAll('[data-ml-locale]')).toHaveLength(1) + expect(title.findAll('[data-ml-locale] option').map((node) => node.text())).toEqual(['en', 'pl']) + expect(body.findAll('[data-ml-locale] option').map((node) => node.text())).toEqual(['en', 'pl']) + }) + it('edits the active locale and copies from another', async () => { const wrapper = mlMount(MLTextField, { field: field('mltext'), diff --git a/admin/tests/form/MarkdownField.test.ts b/admin/tests/form/MarkdownField.test.ts index d2459e0..ae28e21 100644 --- a/admin/tests/form/MarkdownField.test.ts +++ b/admin/tests/form/MarkdownField.test.ts @@ -1,8 +1,10 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { enableAutoUnmount, mount } from '@vue/test-utils' +import { ref } from 'vue' import type { FormField } from '../../src/api/types' import MarkdownField from '../../src/components/form/fields/MarkdownField.vue' import MLMarkdownField from '../../src/components/form/fields/MLMarkdownField.vue' +import { FORM_ENABLED_LOCALES } from '../../src/components/form/formContext' import { resetState } from '../helpers' function field(type = 'markdown', name = 'body'): FormField { @@ -64,6 +66,11 @@ describe('MarkdownField composition and sinks', () => { controlId: 'f-body', }, attachTo: document.body, + global: { + provide: { + [FORM_ENABLED_LOCALES as symbol]: ref(['en', 'pl']), + }, + }, }) expect(wrapper.findComponent(MarkdownField).exists()).toBe(true) await wrapper.find('[data-ml-locale]').setValue('pl') diff --git a/scripts/check-phase14.2.1.sh b/scripts/check-phase14.2.1.sh index 078f820..4bda293 100755 --- a/scripts/check-phase14.2.1.sh +++ b/scripts/check-phase14.2.1.sh @@ -41,6 +41,8 @@ FRAMEWORK_REQUIRE=( TestML TestMLFieldTypes TestMLNestedSave + TestMLHydration + TestRelationChildMLNestedSave TestMarkdownRejectsUnsafeHTML TestMLOpenAPIConformance ) @@ -48,13 +50,15 @@ HOST_REQUIRE=(TestBootUserTranslate) HIGH_THREATS=( T-14.2.1-01 T-14.2.1-02 T-14.2.1-04 T-14.2.1-05 T-14.2.1-06 T-14.2.1-07 T-14.2.1-09 T-14.2.1-10 T-14.2.1-11 T-14.2.1-12 - T-14.2.1-14 T-14.2.1-15 T-14.2.1-18 T-14.2.1-SC + T-14.2.1-14 T-14.2.1-15 T-14.2.1-18 T-14.2.1-19 T-14.2.1-22 + T-14.2.1-23 T-14.2.1-SC ) ALL_THREATS=( T-14.2.1-01 T-14.2.1-02 T-14.2.1-03 T-14.2.1-04 T-14.2.1-05 T-14.2.1-06 T-14.2.1-07 T-14.2.1-08 T-14.2.1-09 T-14.2.1-10 T-14.2.1-11 T-14.2.1-12 T-14.2.1-13 T-14.2.1-14 T-14.2.1-15 - T-14.2.1-16 T-14.2.1-17 T-14.2.1-18 T-14.2.1-SC + T-14.2.1-16 T-14.2.1-17 T-14.2.1-18 T-14.2.1-19 T-14.2.1-20 + T-14.2.1-21 T-14.2.1-22 T-14.2.1-23 T-14.2.1-SC ) usage() {