diff --git a/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-REVIEW.md b/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-REVIEW.md new file mode 100644 index 0000000..7a15425 --- /dev/null +++ b/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-REVIEW.md @@ -0,0 +1,168 @@ +--- +phase: 16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f +reviewed: 2026-10-09T21:20:45Z +depth: standard +files_reviewed: 22 +files_reviewed_list: + - admin/src/state/listState.ts + - admin/src/state/useAuth.ts + - admin/src/state/useSidebar.ts + - admin/src/views/ListView.vue + - admin/tests/form/FormView.test.ts + - admin/tests/list/ListView.test.ts + - admin/tests/smoke/list.smoke.test.ts + - admin/tests/state/listState.test.ts + - admin/tests/state/useAuth.test.ts + - docs/backend/forms.md + - docs/backend/lists-and-filters.md + - modules/cabana/README.md + - modules/cabana/http.go + - modules/cabana/locale_order_test.go + - modules/cabana/query.go + - modules/cabana/query_relation_test.go + - scripts/check-phase10.sh + - ../sm-journal-plugin/README.md + - ../sm-journal-plugin/classes/format_html.go + - ../sm-journal-plugin/classes/format_html_test.go + - ../sm-journal-plugin/go.mod + - ../sm-journal-plugin/posts_admin_smoke_test.go +findings: + critical: 1 + warning: 5 + info: 3 + total: 9 +status: issues_found +--- + +# Phase 16.1: Code Review Report + +**Reviewed:** 2026-10-09T21:20:45Z +**Depth:** standard +**Files Reviewed:** 22 +**Status:** issues_found + +## Narrative Findings (AI reviewer) + +## Summary + +I reviewed the phase diffs: summercms.go `1ef21b0^..HEAD` and sm-journal-plugin `3d533d4..HEAD`. I checked the relation subquery SQL in `modules/cabana/query.go` against GORM's relationship model. Table, pivot and column names come only from the parsed schema, every identifier goes through `quotedIdent`/`qualifiedColumn`, and the search term is a bound parameter. I found no SQL injection path. The EXISTS correlation is correct for belongsTo, hasOne, hasMany and belongsToMany, including self-referencing relations. The `enabledContentLocales` reorder is correct, and so is the list-state key and validation logic. + +The blocking problem is in the journal plugin. Turning on `html.WithUnsafe()` makes the three-regex `rejectUnsafe` blocklist the only defence against stored XSS. That blocklist can be bypassed with ordinary HTML-parser behaviour, and I confirmed this by running the same goldmark config and regexes. The public Nuxt layer (`vue-journal-layer/app/utils/htmlGate.ts`) uses the same three regexes word for word, so it adds no defence in depth. Before this phase, goldmark replaced these payloads with ``. They now reach `content_html` and the public `v-html` sinks. + +## Critical Issues + +### CR-01: The rejectUnsafe gate can be bypassed now that raw HTML passes, so stored XSS reaches content_html (T-16.1-02 not met) + +**File:** `../sm-journal-plugin/classes/format_html.go:22,25-27,54-66` +**Issue:** With `goldmark.WithRendererOptions(html.WithUnsafe())`, goldmark copies raw HTML blocks verbatim. Inside a type-6 HTML block, which is any block starting with `
`, the content does not even have to be valid CommonMark tag syntax. With unsafe mode on, goldmark also stops applying its own `IsDangerousURL` filter. The remaining gate is a substring blocklist, and these payloads get past it. I verified each one against goldmark v1.8.6 with the exact engine options and regexes; every one returns `err == nil`: + +| Source passed to `FormatHTML` | Output (unchanged) | Why the gate misses it | +|---|---|---| +| `