# Ghost's fixes: on Ghost's paths, some fields skip the rules that refused # ordinary Ghost requests there. Each exclusion runs with the request, before the # rules it switches off, so this file is loaded before the Core Rule Set's # rules. Each comment names a request a rule refused. An exclusion on a field of # a body applies only when the query has no parameter of that name, so it # matters only while SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE is set. # # 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/. 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 # while SWW_WAF_APPS is unset, or lists ghost for the request's hostname. # The skip reaches only phase 1, so a rule of a later phase must check # tx.app_ghost itself. 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 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=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: # 930120: search=.htaccess # 932120: search=powershell # 932250: search=ls -la # 932260: search=python3 SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/" \ "id:900351,phase:1,pass,nolog,\ ctl:ruleRemoveTargetById=930120;ARGS:search,\ ctl:ruleRemoveTargetById=932120;ARGS:search,\ ctl:ruleRemoveTargetById=932250;ARGS:search,\ ctl:ruleRemoveTargetById=932260;ARGS:search" # url, the address the editor looks up for an embed or a bookmark, skips two # rules: # 930120: url=https://github.com/example/site/blob/main/.htaccess # 932120: url=https://example.com/blog/powershell-tips SecRule REQUEST_FILENAME "@streq /ghost/api/admin/oembed/" \ "id:900352,phase:1,pass,nolog,\ ctl:ruleRemoveTargetById=930120;ARGS:url,\ ctl:ruleRemoveTargetById=932120;ARGS:url" # The query fields that filter the analytics skip the eight rules that 900353 # lists, on /ghost/api/admin/stats/: # 932120: source=powershell.example.com # 932235, 932260: utm_campaign=powershell & python3 # 932250: utm_term=ls -la # These fields filter the figures by the site a visit came from, or a # campaign: 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:/^(?:source|utm_(?:source|medium|campaign|content|term))$/,\ ctl:ruleRemoveTargetById=932120;ARGS:/^(?:source|utm_(?:source|medium|campaign|content|term))$/,\ ctl:ruleRemoveTargetById=932125;ARGS:/^(?:source|utm_(?:source|medium|campaign|content|term))$/,\ ctl:ruleRemoveTargetById=932230;ARGS:/^(?:source|utm_(?:source|medium|campaign|content|term))$/,\ ctl:ruleRemoveTargetById=932235;ARGS:/^(?:source|utm_(?:source|medium|campaign|content|term))$/,\ ctl:ruleRemoveTargetById=932250;ARGS:/^(?:source|utm_(?:source|medium|campaign|content|term))$/,\ ctl:ruleRemoveTargetById=932260;ARGS:/^(?:source|utm_(?:source|medium|campaign|content|term))$/,\ ctl:ruleRemoveTargetById=932380;ARGS:/^(?:source|utm_(?:source|medium|campaign|content|term))$/" # pathname, the page the analytics show, skips three rules on # /ghost/api/admin/stats/, which refused the path of a post and of tag pages: # 930120: pathname=/tag/proc/ # 932120: pathname=/python3-and-powershell/ # 932260: pathname=/tag/python3/ SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/stats/" \ "id:900385,phase:1,pass,nolog,\ ctl:ruleRemoveTargetById=930120;ARGS:pathname,\ ctl:ruleRemoveTargetById=932120;ARGS:pathname,\ ctl:ruleRemoveTargetById=932260;ARGS:pathname" # 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 that # 900353 lists: 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 on the Admin API, # which refused words that name commands and files: # 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: # title, name and slug of posts, pages, tags, labels, members, newsletters # and snippets; a post's tags and a member's labels, by name or slug # custom_excerpt, description, note and sender_name # meta_title, meta_description, og_title, og_description, twitter_title and # twitter_description SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/" \ "id:900353,phase:1,pass,nolog,chain" SecRule &ARGS_GET:/^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))?)$/ "@eq 0" \ "ctl:ruleRemoveTargetById=930120;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=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 eight rules. SecRule REQUEST_FILENAME "@beginsWith /members/api/" \ "id:900359,phase:1,pass,nolog,chain" SecRule &ARGS_GET:/^json\.name$/ "@eq 0" \ "ctl:ruleRemoveTargetById=930120;ARGS:/^json\.name$/,\ 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 # These fields take a