--- 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 `