From 101cc0aba5478cbf1691c0cdc3519d1a0381e098 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Thu, 12 Nov 2026 09:13:25 +0100 Subject: [PATCH] Core Rule Set: a Ghost post's or page's canonical URL passes when it names a command or a file (closes #374) While bodies are read, 932120 refused the canonical URL `https://example.com/blog/powershell/` and 930120 refused `https://example.com/blog/.htaccess/`. The new exclusions 900383 and 900384 switch both rules off for `posts.N.canonical_url` under `/ghost/api/admin/posts/` and `pages.N.canonical_url` under `/ghost/api/admin/pages/`, as `url` does, and only when the query has no field of that name. The recorded requests gain the saves the Ghost 6.76.0 admin sends when those addresses are set in a post's and a page's settings. Model: opus-5-5 --- SPEC.md | 6 ++- internal/coreruleset/ghost.conf | 17 +++++++++ internal/coreruleset/ghost_test.go | 37 ++++++++++++++++--- .../coreruleset/testdata/ghost-requests.txt | 2 + 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/SPEC.md b/SPEC.md index 3c9fb13..d13884d 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1773,6 +1773,10 @@ member. A staff user's address skips them too: `username` when signing in, and `email` when creating the first staff user, asking for a password reset or saving a profile. +The `canonical_url` of a post or a page skips 930120 and 932120, as `url` does. +A post's skips them under `/ghost/api/admin/posts/`, and a page's under +`/ghost/api/admin/pages/`. + The articles carry HTML on purpose. These fields on the Admin API hold them: - `lexical`, which the editor saves, and `mobiledoc` and `html`, which @@ -1813,7 +1817,7 @@ fields, on the Admin API: (941170 and 941210). Path traversal and SQL injection are still refused in every field above. Script injection is too, outside the articles and code injection. `; wget http://example.com/x.sh` is still refused in the query fields, the plain -text and the addresses. +text, the addresses and the canonical URLs. Coraza names a staff user's `profile_image` field `json.users.0.profile_image`. That name holds `.profile`, a file on the list of system files, so the name diff --git a/internal/coreruleset/ghost.conf b/internal/coreruleset/ghost.conf index e4b0acd..edf796d 100644 --- a/internal/coreruleset/ghost.conf +++ b/internal/coreruleset/ghost.conf @@ -216,6 +216,23 @@ SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/" \ SecRule &ARGS_GET:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html|codeinjection_(?:head|foot))|settings\.\d+\.value)$/ "@eq 0" \ "ctl:ruleRemoveTargetById=941110;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html|codeinjection_(?:head|foot))|settings\.\d+\.value)$/" +# The canonical URL of a post or a page skips the two rules that url skips: +# 930120: https://example.com/blog/.htaccess/ +# 932120: https://example.com/blog/powershell/ +# Each of these fields skips them only on its own path: +# posts..canonical_url, under /ghost/api/admin/posts/ +# pages..canonical_url, under /ghost/api/admin/pages/ +SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/posts/" \ + "id:900383,phase:1,pass,nolog,chain" + SecRule &ARGS_GET:/^json\.posts\.\d+\.canonical_url$/ "@eq 0" \ + "ctl:ruleRemoveTargetById=930120;ARGS:/^json\.posts\.\d+\.canonical_url$/,\ + ctl:ruleRemoveTargetById=932120;ARGS:/^json\.posts\.\d+\.canonical_url$/" +SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/pages/" \ + "id:900384,phase:1,pass,nolog,chain" + SecRule &ARGS_GET:/^json\.pages\.\d+\.canonical_url$/ "@eq 0" \ + "ctl:ruleRemoveTargetById=930120;ARGS:/^json\.pages\.\d+\.canonical_url$/,\ + ctl:ruleRemoveTargetById=932120;ARGS:/^json\.pages\.\d+\.canonical_url$/" + # Saving a staff user's profile sends the field profile_image, whose name holds # .profile, a file the list of system files names (930120). SecRule REQUEST_FILENAME "@rx ^/ghost/api/admin/users/[0-9a-f]{24}/$" \ diff --git a/internal/coreruleset/ghost_test.go b/internal/coreruleset/ghost_test.go index 8744dba..34fbc84 100644 --- a/internal/coreruleset/ghost_test.go +++ b/internal/coreruleset/ghost_test.go @@ -143,6 +143,16 @@ var ghostAddressFields = []ghostField{ {http.MethodPut, ghostUser, `{"users":[{"email":"VALUE"}]}`}, } +// ghostCanonicalURLFields hold the canonical URL of a post or a page, and skip +// 930120 and 932120, as url does. +// +//nolint:gochecknoglobals // a constant cannot be a list +var ghostCanonicalURLFields = []ghostField{ + {http.MethodPut, ghostPost, `{"posts":[{"canonical_url":"VALUE"}]}`}, + {http.MethodPost, ghostPosts, `{"posts":[{"canonical_url":"VALUE"}]}`}, + {http.MethodPut, ghostPage, `{"pages":[{"canonical_url":"VALUE"}]}`}, +} + // ghostPostFields hold a post or a page, skip the rules of 900355, and take a //