From 7c4fca92ecbfa4c0bde7ca8846b319d839b04c99 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Tue, 6 Oct 2026 19:28:21 +0200 Subject: [PATCH] docs(15): add code review report --- .../phases/15-journal-plugin/15-REVIEW.md | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 .planning/phases/15-journal-plugin/15-REVIEW.md diff --git a/.planning/phases/15-journal-plugin/15-REVIEW.md b/.planning/phases/15-journal-plugin/15-REVIEW.md new file mode 100644 index 0000000..b2c1dab --- /dev/null +++ b/.planning/phases/15-journal-plugin/15-REVIEW.md @@ -0,0 +1,216 @@ +--- +phase: 15-journal-plugin +reviewed: 2026-10-06T17:27:26Z +depth: standard +files_reviewed: 62 +files_reviewed_list: + - ../sm-grzybyfunkcjonalne-app/.gitmodules + - ../sm-grzybyfunkcjonalne-app/boot_test.go + - ../sm-grzybyfunkcjonalne-app/config/http.yaml + - ../sm-grzybyfunkcjonalne-app/go.mod + - ../sm-grzybyfunkcjonalne-app/go.work + - ../sm-grzybyfunkcjonalne-app/plugins.gen.go + - ../sm-grzybyfunkcjonalne-app/summer.yaml + - ../sm-journal-plugin/README.md + - ../sm-journal-plugin/admin.go + - ../sm-journal-plugin/admin_harness_test.go + - ../sm-journal-plugin/admin_navigation.go + - ../sm-journal-plugin/admin_permissions.go + - ../sm-journal-plugin/assets/images/journal-icon.svg + - ../sm-journal-plugin/classes/format_html.go + - ../sm-journal-plugin/classes/format_html_test.go + - ../sm-journal-plugin/console/columns.go + - ../sm-journal-plugin/console/export_posts.go + - ../sm-journal-plugin/console/import_posts.go + - ../sm-journal-plugin/controllers/api/auth.go + - ../sm-journal-plugin/controllers/api/media.go + - ../sm-journal-plugin/controllers/api/media_test.go + - ../sm-journal-plugin/controllers/api/posts.go + - ../sm-journal-plugin/controllers/api/posts_helpers.go + - ../sm-journal-plugin/controllers/api/posts_search.go + - ../sm-journal-plugin/controllers/api/posts_test.go + - ../sm-journal-plugin/controllers/api/rss.go + - ../sm-journal-plugin/controllers/categories.go + - ../sm-journal-plugin/controllers/posts.go + - ../sm-journal-plugin/controllers/tags.go + - ../sm-journal-plugin/go.mod + - ../sm-journal-plugin/integration_test.go + - ../sm-journal-plugin/journal_api_matrix_test.go + - ../sm-journal-plugin/journal_api_task3_test.go + - ../sm-journal-plugin/journal_api_writes_test.go + - ../sm-journal-plugin/journal_public_list_smoke_test.go + - ../sm-journal-plugin/lang/en/lang.yaml + - ../sm-journal-plugin/lang/pl/lang.yaml + - ../sm-journal-plugin/models/category.go + - ../sm-journal-plugin/models/fillable_test.go + - ../sm-journal-plugin/models/post.go + - ../sm-journal-plugin/models/post/fields.yaml + - ../sm-journal-plugin/models/post_search.go + - ../sm-journal-plugin/models/post_translatable_smoke_test.go + - ../sm-journal-plugin/models/search_gate.go + - ../sm-journal-plugin/models/settings.go + - ../sm-journal-plugin/models/tag.go + - ../sm-journal-plugin/models/translatable_test.go + - ../sm-journal-plugin/plugin.go + - ../sm-journal-plugin/plugin_test.go + - ../sm-journal-plugin/posts_admin_smoke_test.go + - ../sm-journal-plugin/routes.go + - ../sm-journal-plugin/search.go + - ../sm-journal-plugin/search_test.go + - ../sm-journal-plugin/updates/202610060001_create_golem15_journal_posts.go + - ../sm-journal-plugin/updates/202610060002_create_golem15_journal_categories.go + - ../sm-journal-plugin/updates/202610060003_create_golem15_journal_tags.go + - ../sm-journal-plugin/updates/202610060004_create_golem15_journal_posts_categories.go + - ../sm-journal-plugin/updates/202610060005_create_golem15_journal_posts_tags.go + - ../sm-journal-plugin/updates/202610060006_create_golem15_journal_settings.go + - ../sm-journal-plugin/updates/202610060007_add_author_slug_to_backend_users.go + - ../sm-journal-plugin/updates/migrations_test.go + - ../sm-journal-plugin/updates/postgres_test.go + - scripts/check-phase15.sh +findings: + critical: 2 + warning: 6 + info: 4 + total: 12 +status: issues_found +--- + +# Phase 15: Code Review Report + +**Reviewed:** 2026-10-06T17:27:26Z +**Depth:** standard +**Files Reviewed:** 62 +**Status:** issues_found + +## Summary + +Standard-depth review of Phase 15 Journal plugin sources from 15-01 through 15-04 SUMMARY key-files. Production code lives in `sm-journal-plugin` (HEAD `d09edde`) and the `sm-grzybyfunkcjonalne-app` proof host, plus `scripts/check-phase15.sh`. Planning artifacts (`15-SECURITY-REVIEW.md`, `15-VALIDATION.md`) were excluded. + +Public GET auth, JOURNAL-005 draft 404, backend JWT writes, fillable allow-lists, parameterized search `ILIKE`, and media folder confinement under `/journal/` are sound. Two ship-blocking issues remain: the Posts toolbar import/export actions write and read arbitrary filesystem paths from client JSON, and `translations.content_html` bypasses `FormatHTML`, defeating the XSS gate that main `content` goes through. + +## Narrative Findings (AI reviewer) + +## Critical Issues + +### CR-01: Admin toolbar import/export accepts unsandboxed filesystem paths + +**File:** `../sm-journal-plugin/console/import_posts.go:340-357` +**Issue:** `IOPayloadPath` copies the client JSON `path` field unchanged. Posts admin actions `runExportPosts` / `runImportPosts` pass that string into `ExportToDir` / `ImportFromDir`, which call `os.MkdirAll`, `os.WriteFile`, `os.Stat`, and `os.ReadFile` with no root confinement, allow-list, or `..` rejection. Export also does `filepath.Join(dir, slug+".json")`; admin slugs are not constrained by `slugPattern`, so a slug such as `foo/../../../tmp/x` is cleaned by `filepath.Join` to a path outside the intended directory. Any backend principal with `golem15.journal.access_import_export` can therefore read arbitrary `*.json` trees and write JSON (and create directories) as the server user. CLI `--path` is operator-local; the same helper on an HTTP admin action is path traversal. +**Fix:** Confine toolbar paths to a storage subdirectory (for example `storage/journal-export`), reject absolute paths and `..` after `filepath.Clean`, and sanitize export filenames to `filepath.Base(slug)`: + +```go +func confinedExportDir(raw, fallback string) (string, error) { + root, err := filepath.Abs("storage/app") + if err != nil { + return "", err + } + candidate := filepath.Clean(filepath.Join(root, fallback)) + if strings.TrimSpace(raw) != "" { + candidate = filepath.Clean(filepath.Join(root, raw)) + } + rel, err := filepath.Rel(root, candidate) + if err != nil || strings.HasPrefix(rel, "..") { + return "", fmt.Errorf("journal: export path is outside storage") + } + return candidate, nil +} +``` + +Do not pass the raw payload path to `os.WriteFile`. Keep unrestricted `--path` on the CLI command only. + +### CR-02: `translations.content_html` bypasses FormatHTML (stored XSS) + +**File:** `../sm-journal-plugin/controllers/api/posts_helpers.go:762-774` +**Issue:** Main `content` is always passed through `classes.FormatHTML` before persist (store/update and admin `preparePostWrite`). `syncTranslations` does that only when `content` is non-empty **and** `content_html` is empty; if the writer sends `translations.pl.content_html`, the value is stored via `translate.SetTranslated` with no `rejectUnsafe` gate. Console import `syncTranslations` (`console/import_posts.go:309-330`) writes `content_html` from JSON as-is. Public `GET /_journal/api/v1/posts/{slug}` returns those attributes in `translations` (`translationsOf`). That defeats the JOURNAL-003/004 control: an editor (or anyone who can import) can persist `