diff --git a/README.md b/README.md index b488122..b5f051b 100644 --- a/README.md +++ b/README.md @@ -610,8 +610,9 @@ lowers a client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md). its searches, its file names, its account names, its comments and its documents; - `ghost`, whose fixes let through its filters, its searches, its names, its - email addresses, its code injection, its settings and a post saved from - its editor, HTML included. + email addresses, its code injection, its settings, its analytics, its + members' comments, ActivityPub and a post saved from its editor, HTML + included. - A random password, as a password manager makes one, gets through these forms: - signing in, signing up, and changing or resetting a password, on WordPress, Drupal, MediaWiki, Mastodon, Nextcloud, gitea and Ghost; diff --git a/SPEC.md b/SPEC.md index 6ac58b1..5b4b659 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1747,31 +1747,62 @@ Ghost's fixes: on Ghost's paths, some fields skip the rules that refused ordinary Ghost requests there. `internal/coreruleset/ghost.conf` lists each rule and the request it refused. Ghost's admin and integrations call the Admin API under `/ghost/api/admin/`. Themes and Ghost's scripts call the Content API under -`/ghost/api/content/`, and members call `/members/api/`. +`/ghost/api/content/`, and members call `/members/api/`. The admin calls +ActivityPub, a service of its own, under `/.ghost/activitypub/`. -Three query fields skip some rules: +The rules of plain text are 930120, 932120, 932125, 932230, 932235, 932250, +932260 and 932380. They refused words that name commands and files, such as +`python3`, `ls -la` and `.htaccess`, and titles such as `powershell & python3`. -- `filter`, a query in Ghost's own filter language, skips 932120 on the Admin - API and the Content API, so `tag:powershell` passes; +These query fields skip some rules: + +- `filter`, a query in Ghost's own filter language, skips the rules of plain + text except 930120, on the Admin API and the Content API; - `search`, a search for a member, skips 930120, 932120, 932250 and 932260 on the Admin API; - `url`, the address the editor looks up for an embed or a bookmark, skips - 930120 and 932120. + 930120 and 932120; +- `pathname`, `source` and the `utm_` fields, which filter the analytics, skip + the rules of plain text under `/ghost/api/admin/stats/`; +- `email_segment`, the members a post is sent to as a newsletter, skips 932120 + on the Admin API's posts; +- `query`, a search for an account in ActivityPub, skips the rules of plain + text; +- `email`, when an invitation is checked, skips 930120, 932120 and 932260. -The fields of JSON bodies where a person types plain text skip 930120, 932120, -932125, 932230, 932250, 932260 and 932380. They are these fields of posts, -pages, tags, labels, members, newsletters and snippets, on the Admin API: +The fields of JSON bodies where a person types plain text skip the rules of +plain text. They are these fields of posts, pages, tags, labels, members, +newsletters and snippets, on the Admin API: - `title`, `name` and `slug`; - a post's `tags` and a member's `labels`, by name or slug; - `custom_excerpt`, `description`, `note` and `sender_name`; - the `meta_`, `og_` and `twitter_` titles and descriptions. -A member's `name` at `/members/api/` skips them too. A member's `email` skips -930120, 932120 and 932260, at `/members/api/` and when the admin adds or edits a -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. +These fields on the Admin API skip them too: + +- a staff user's `name`, `slug`, `bio`, `location` and `website`; +- `blogTitle` and `name` when creating the first staff user; +- `name` when accepting an invitation; +- `name`, `description` and `benefits` of tiers; +- `name`, `code`, `display_title` and `display_description` of offers; +- `title`, `description` and `excerpt` of recommendations; +- `name` and `description` of integrations, and `name` of webhooks; +- `feature_image_alt` and `feature_image_caption` of posts and pages. + +These fields skip them on their own paths: + +- a member's `name` at `/members/api/`; +- `html` of a member's comment at `/members/api/comments/`; +- `labels`, when the admin imports members from a CSV file; +- in ActivityPub, `content` of a note or a reply, and `name`, `username` and + `bio` of the account. + +A member's `email` skips 930120, 932120 and 932260, at `/members/api/` and when +the admin adds or edits a 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, saving a profile, inviting a staff user or +accepting an invitation. The articles carry HTML on purpose. These fields on the Admin API hold them: @@ -1791,20 +1822,24 @@ The articles skip these rules: Code injection is the HTML that Ghost adds to the head and the foot of a page. These fields on the Admin API hold it: -- `codeinjection_head` and `codeinjection_foot` of posts and pages; +- `codeinjection_head` and `codeinjection_foot` of posts, pages and tags; - `value` of settings, since the site's settings hold its code injection. Code injection skips these rules: -- 930120, 932120, 932125, 932230, 932250, 932260 and 932380, as plain text does; +- the rules of plain text except 932235; - 921130, 941100, 941160 and 941270, which refused `` and `` elements. `` and `javascript:alert(1)` are still refused in the articles and code injection (941110, 941170 and 941210). Path traversal and SQL injection are still refused in every field above. Script injection is too, -outside the articles. `; wget http://example.com/x.sh` is still refused in the -query fields, the plain text and the addresses. +outside the articles. + +`; wget http://example.com/x.sh` is still refused in the addresses, `search`, +`url`, `email_segment` and `email`. The plain text, the articles and the other +query fields skip 932235, which refuses it, since 932235 also refuses +`powershell & python3`. 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 8deb8a1..df28a69 100644 --- a/internal/coreruleset/ghost.conf +++ b/internal/coreruleset/ghost.conf @@ -7,7 +7,8 @@ # # Ghost's admin and its integrations call the Admin API under /ghost/api/admin/, # themes and Ghost's own scripts call the Content API under /ghost/api/content/, -# and members call /members/api/. Coraza names a field of a JSON body by its +# and members call /members/api/. The admin calls ActivityPub, a service of its +# own, under /.ghost/activitypub/. Coraza names a field of a JSON body by its # path, so the title of the first post saved is json.posts.0.title. # Every rule below is skipped unless Inspect has set tx.app_ghost. It sets it @@ -17,11 +18,22 @@ SecRule &TX:app_ghost "@eq 0" \ "id:900365,phase:1,pass,nolog,skipAfter:END_GHOST" -# filter, a query in Ghost's own filter language, skips 932120 on the Admin API -# and the Content API: filter=tag:powershell, filter=name:~'powershell'. +# filter, a query in Ghost's own filter language, skips seven rules on the +# Admin API and the Content API: +# 932120: filter=tag:powershell, filter=name:~'powershell' +# 932125, 932230, 932250: filter=title:~'powershell & ls -la' +# 932235, 932260: filter=title:~'powershell & python3', which the admin's +# post picker sends when a person types that title +# 932380: filter=title:~'python3 & powershell' SecRule REQUEST_FILENAME "@rx ^/ghost/api/(?:admin|content)/" \ "id:900350,phase:1,pass,nolog,\ - ctl:ruleRemoveTargetById=932120;ARGS:filter" + ctl:ruleRemoveTargetById=932120;ARGS:filter,\ + ctl:ruleRemoveTargetById=932125;ARGS:filter,\ + ctl:ruleRemoveTargetById=932230;ARGS:filter,\ + ctl:ruleRemoveTargetById=932235;ARGS:filter,\ + ctl:ruleRemoveTargetById=932250;ARGS:filter,\ + ctl:ruleRemoveTargetById=932260;ARGS:filter,\ + ctl:ruleRemoveTargetById=932380;ARGS:filter" # search, the search for a member by name or address, skips four rules on the # Admin API: @@ -45,10 +57,51 @@ SecRule REQUEST_FILENAME "@streq /ghost/api/admin/oembed/" \ ctl:ruleRemoveTargetById=930120;ARGS:url,\ ctl:ruleRemoveTargetById=932120;ARGS:url" -# The fields a person types plain text into skip seven rules on the Admin API: +# The query fields of the analytics pages skip the eight rules of plain text, +# which 900353 lists, on /ghost/api/admin/stats/: +# 930120: pathname=/.htaccess +# 932120: source=powershell.example.com +# 932235, 932260: utm_campaign=powershell & python3 +# 932250: utm_term=ls -la +# These fields filter the figures by a page, the site a visit came from, or a +# campaign: pathname, source, utm_source, utm_medium, utm_campaign, +# utm_content and utm_term. +SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/stats/" \ + "id:900367,phase:1,pass,nolog,\ + ctl:ruleRemoveTargetById=930120;ARGS:/^(?:pathname|source|utm_(?:source|medium|campaign|content|term))$/,\ + ctl:ruleRemoveTargetById=932120;ARGS:/^(?:pathname|source|utm_(?:source|medium|campaign|content|term))$/,\ + ctl:ruleRemoveTargetById=932125;ARGS:/^(?:pathname|source|utm_(?:source|medium|campaign|content|term))$/,\ + ctl:ruleRemoveTargetById=932230;ARGS:/^(?:pathname|source|utm_(?:source|medium|campaign|content|term))$/,\ + ctl:ruleRemoveTargetById=932235;ARGS:/^(?:pathname|source|utm_(?:source|medium|campaign|content|term))$/,\ + ctl:ruleRemoveTargetById=932250;ARGS:/^(?:pathname|source|utm_(?:source|medium|campaign|content|term))$/,\ + ctl:ruleRemoveTargetById=932260;ARGS:/^(?:pathname|source|utm_(?:source|medium|campaign|content|term))$/,\ + ctl:ruleRemoveTargetById=932380;ARGS:/^(?:pathname|source|utm_(?:source|medium|campaign|content|term))$/" + +# email_segment, the members a post is sent to as a newsletter, skips 932120 +# on the Admin API's posts: email_segment=label:powershell-users. +SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/posts/" \ + "id:900368,phase:1,pass,nolog,\ + ctl:ruleRemoveTargetById=932120;ARGS:email_segment" + +# query, a search for an account in ActivityPub, skips the eight rules of plain +# text: query=python3, query=@powershell@example.com. +SecRule REQUEST_FILENAME "@streq /.ghost/activitypub/v1/actions/search" \ + "id:900369,phase:1,pass,nolog,\ + ctl:ruleRemoveTargetById=930120;ARGS:query,\ + ctl:ruleRemoveTargetById=932120;ARGS:query,\ + ctl:ruleRemoveTargetById=932125;ARGS:query,\ + ctl:ruleRemoveTargetById=932230;ARGS:query,\ + ctl:ruleRemoveTargetById=932235;ARGS:query,\ + ctl:ruleRemoveTargetById=932250;ARGS:query,\ + ctl:ruleRemoveTargetById=932260;ARGS:query,\ + ctl:ruleRemoveTargetById=932380;ARGS:query" + +# The fields a person types plain text into skip eight rules, the rules of +# plain text, on the Admin API: # 930120: the title "Using python3 and ls -la to tidy .htaccess files" # 932120: the page's slug about-python3-powershell-htaccess # 932125, 932230, 932250: the tag "Linux & ls -la" +# 932235: the title "powershell & python3" # 932260: the tag python3 # 932380: the newsletter "Shell tips weekly: python3 & powershell" # These fields hold plain text: @@ -64,11 +117,12 @@ SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/" \ ctl:ruleRemoveTargetById=932120;ARGS:/^json\.(?:posts|pages|tags|labels|members|newsletters|snippets)\.\d+\.(?:title|name|slug|custom_excerpt|description|note|sender_name|(?:meta|og|twitter)_(?:title|description)|(?:tags|labels)\.\d+(?:\.(?:name|slug))?)$/,\ ctl:ruleRemoveTargetById=932125;ARGS:/^json\.(?:posts|pages|tags|labels|members|newsletters|snippets)\.\d+\.(?:title|name|slug|custom_excerpt|description|note|sender_name|(?:meta|og|twitter)_(?:title|description)|(?:tags|labels)\.\d+(?:\.(?:name|slug))?)$/,\ ctl:ruleRemoveTargetById=932230;ARGS:/^json\.(?:posts|pages|tags|labels|members|newsletters|snippets)\.\d+\.(?:title|name|slug|custom_excerpt|description|note|sender_name|(?:meta|og|twitter)_(?:title|description)|(?:tags|labels)\.\d+(?:\.(?:name|slug))?)$/,\ + ctl:ruleRemoveTargetById=932235;ARGS:/^json\.(?:posts|pages|tags|labels|members|newsletters|snippets)\.\d+\.(?:title|name|slug|custom_excerpt|description|note|sender_name|(?:meta|og|twitter)_(?:title|description)|(?:tags|labels)\.\d+(?:\.(?:name|slug))?)$/,\ ctl:ruleRemoveTargetById=932250;ARGS:/^json\.(?:posts|pages|tags|labels|members|newsletters|snippets)\.\d+\.(?:title|name|slug|custom_excerpt|description|note|sender_name|(?:meta|og|twitter)_(?:title|description)|(?:tags|labels)\.\d+(?:\.(?:name|slug))?)$/,\ ctl:ruleRemoveTargetById=932260;ARGS:/^json\.(?:posts|pages|tags|labels|members|newsletters|snippets)\.\d+\.(?:title|name|slug|custom_excerpt|description|note|sender_name|(?:meta|og|twitter)_(?:title|description)|(?:tags|labels)\.\d+(?:\.(?:name|slug))?)$/,\ ctl:ruleRemoveTargetById=932380;ARGS:/^json\.(?:posts|pages|tags|labels|members|newsletters|snippets)\.\d+\.(?:title|name|slug|custom_excerpt|description|note|sender_name|(?:meta|og|twitter)_(?:title|description)|(?:tags|labels)\.\d+(?:\.(?:name|slug))?)$/" -# name, the name a member saves at /members/api/, skips the same seven rules. +# name, the name a member saves at /members/api/, skips the same eight rules. SecRule REQUEST_FILENAME "@beginsWith /members/api/" \ "id:900359,phase:1,pass,nolog,chain" SecRule &ARGS_GET:/^json\.name$/ "@eq 0" \ @@ -76,10 +130,96 @@ SecRule REQUEST_FILENAME "@beginsWith /members/api/" \ ctl:ruleRemoveTargetById=932120;ARGS:/^json\.name$/,\ ctl:ruleRemoveTargetById=932125;ARGS:/^json\.name$/,\ ctl:ruleRemoveTargetById=932230;ARGS:/^json\.name$/,\ + ctl:ruleRemoveTargetById=932235;ARGS:/^json\.name$/,\ ctl:ruleRemoveTargetById=932250;ARGS:/^json\.name$/,\ ctl:ruleRemoveTargetById=932260;ARGS:/^json\.name$/,\ ctl:ruleRemoveTargetById=932380;ARGS:/^json\.name$/" +# More fields of plain text skip the same eight rules on the Admin API, which +# refused "python3 & powershell club", "Weekly ls -la and .htaccess tips" and +# "powershell & python3 deal" in them: +# users..name, slug, bio, location and website, a staff user's profile +# setup..blogTitle and name, creating the first staff user +# invitation..name, accepting a staff invitation +# tiers..name, description and benefits +# offers..name, code, display_title and display_description +# recommendations..title, description and excerpt +# integrations..name and description, and webhooks..name +# posts..feature_image_alt and feature_image_caption, and the same fields +# of pages +# Coraza matches a field's name in lower case, so blogTitle is blogtitle. +SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/" \ + "id:900370,phase:1,pass,nolog,chain" + SecRule &ARGS_GET:/^json\.(?:users\.\d+\.(?:name|slug|bio|location|website)|setup\.\d+\.(?:blogtitle|name)|invitation\.\d+\.name|tiers\.\d+\.(?:name|description|benefits\.\d+)|offers\.\d+\.(?:name|code|display_title|display_description)|recommendations\.\d+\.(?:title|description|excerpt)|integrations\.\d+\.(?:name|description)|webhooks\.\d+\.name|(?:posts|pages)\.\d+\.feature_image_(?:alt|caption))$/ "@eq 0" \ + "ctl:ruleRemoveTargetById=930120;ARGS:/^json\.(?:users\.\d+\.(?:name|slug|bio|location|website)|setup\.\d+\.(?:blogtitle|name)|invitation\.\d+\.name|tiers\.\d+\.(?:name|description|benefits\.\d+)|offers\.\d+\.(?:name|code|display_title|display_description)|recommendations\.\d+\.(?:title|description|excerpt)|integrations\.\d+\.(?:name|description)|webhooks\.\d+\.name|(?:posts|pages)\.\d+\.feature_image_(?:alt|caption))$/,\ + ctl:ruleRemoveTargetById=932120;ARGS:/^json\.(?:users\.\d+\.(?:name|slug|bio|location|website)|setup\.\d+\.(?:blogtitle|name)|invitation\.\d+\.name|tiers\.\d+\.(?:name|description|benefits\.\d+)|offers\.\d+\.(?:name|code|display_title|display_description)|recommendations\.\d+\.(?:title|description|excerpt)|integrations\.\d+\.(?:name|description)|webhooks\.\d+\.name|(?:posts|pages)\.\d+\.feature_image_(?:alt|caption))$/,\ + ctl:ruleRemoveTargetById=932125;ARGS:/^json\.(?:users\.\d+\.(?:name|slug|bio|location|website)|setup\.\d+\.(?:blogtitle|name)|invitation\.\d+\.name|tiers\.\d+\.(?:name|description|benefits\.\d+)|offers\.\d+\.(?:name|code|display_title|display_description)|recommendations\.\d+\.(?:title|description|excerpt)|integrations\.\d+\.(?:name|description)|webhooks\.\d+\.name|(?:posts|pages)\.\d+\.feature_image_(?:alt|caption))$/,\ + ctl:ruleRemoveTargetById=932230;ARGS:/^json\.(?:users\.\d+\.(?:name|slug|bio|location|website)|setup\.\d+\.(?:blogtitle|name)|invitation\.\d+\.name|tiers\.\d+\.(?:name|description|benefits\.\d+)|offers\.\d+\.(?:name|code|display_title|display_description)|recommendations\.\d+\.(?:title|description|excerpt)|integrations\.\d+\.(?:name|description)|webhooks\.\d+\.name|(?:posts|pages)\.\d+\.feature_image_(?:alt|caption))$/,\ + ctl:ruleRemoveTargetById=932235;ARGS:/^json\.(?:users\.\d+\.(?:name|slug|bio|location|website)|setup\.\d+\.(?:blogtitle|name)|invitation\.\d+\.name|tiers\.\d+\.(?:name|description|benefits\.\d+)|offers\.\d+\.(?:name|code|display_title|display_description)|recommendations\.\d+\.(?:title|description|excerpt)|integrations\.\d+\.(?:name|description)|webhooks\.\d+\.name|(?:posts|pages)\.\d+\.feature_image_(?:alt|caption))$/,\ + ctl:ruleRemoveTargetById=932250;ARGS:/^json\.(?:users\.\d+\.(?:name|slug|bio|location|website)|setup\.\d+\.(?:blogtitle|name)|invitation\.\d+\.name|tiers\.\d+\.(?:name|description|benefits\.\d+)|offers\.\d+\.(?:name|code|display_title|display_description)|recommendations\.\d+\.(?:title|description|excerpt)|integrations\.\d+\.(?:name|description)|webhooks\.\d+\.name|(?:posts|pages)\.\d+\.feature_image_(?:alt|caption))$/,\ + ctl:ruleRemoveTargetById=932260;ARGS:/^json\.(?:users\.\d+\.(?:name|slug|bio|location|website)|setup\.\d+\.(?:blogtitle|name)|invitation\.\d+\.name|tiers\.\d+\.(?:name|description|benefits\.\d+)|offers\.\d+\.(?:name|code|display_title|display_description)|recommendations\.\d+\.(?:title|description|excerpt)|integrations\.\d+\.(?:name|description)|webhooks\.\d+\.name|(?:posts|pages)\.\d+\.feature_image_(?:alt|caption))$/,\ + ctl:ruleRemoveTargetById=932380;ARGS:/^json\.(?:users\.\d+\.(?:name|slug|bio|location|website)|setup\.\d+\.(?:blogtitle|name)|invitation\.\d+\.name|tiers\.\d+\.(?:name|description|benefits\.\d+)|offers\.\d+\.(?:name|code|display_title|display_description)|recommendations\.\d+\.(?:title|description|excerpt)|integrations\.\d+\.(?:name|description)|webhooks\.\d+\.name|(?:posts|pages)\.\d+\.feature_image_(?:alt|caption))$/" + +# A member's comment, html at /members/api/comments/, skips the same eight +# rules, which refused "I ran ls -la and fixed my .htaccess with python3". The +# rules for scripts and HTML still refuse a