check / check (push) Successful in 5m51s
Every setting of discriminator and rproxy is read through viper, in place of the os.LookupEnv function Main passed down. NewViper turns AutomaticEnv and AllowEmptyEnv on, so a setting that is set but empty reads as set. A setting may also be given as a file, the _FILE form, which rproxy's settings now have too; rproxy still writes nothing to disk. The settings are renamed. Discriminator's take the prefix SWW_WAF_, rproxy's take SWW_RPROXY_, and the shared SWW_COMMAND keeps SWW_. The Core Rule Set's settings take SWW_WAF_CORE_RULE_SET_ rather than SWW_WAF_WAF_. Model: deepseek-v4-pro (first version); opus-5-5 (rework)
595 lines
20 KiB
Go
595 lines
20 KiB
Go
package coreruleset_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"slices"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
|
)
|
|
|
|
// mastodonThreshold is SWW_WAF_CORE_RULE_SET_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, passwordField},
|
|
{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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMastodonCommandAttackIsRefusedOutsideAPost(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
crs := atDefaults(t)
|
|
|
|
for _, target := range mastodonQueryFields {
|
|
got := inspect(t, crs, get(target+commandInjection))
|
|
if got.Score < mastodonThreshold {
|
|
t.Errorf("%s%s let through: %+v", target, commandInjection, got)
|
|
}
|
|
}
|
|
|
|
for _, sent := range mastodonFormFields {
|
|
if mastodonForm(t, sent, commandInjection) < mastodonThreshold {
|
|
t.Errorf("%v let through", sent)
|
|
}
|
|
}
|
|
|
|
attack, err := url.QueryUnescape(commandInjection)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, sent := range mastodonJSONFields {
|
|
if mastodonJSON(t, sent, attack) < 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{commandInjection},
|
|
) {
|
|
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)
|
|
}
|
|
}
|
|
|
|
// mastodonEmail is the email address on the sign-up, sign-in, password
|
|
// reset and account settings forms, as the start of a form body.
|
|
const mastodonEmail = "user[email]="
|
|
|
|
func TestMastodonEmailAddressesSkipThreeRules(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, target := range []string{mastodonAccount, mastodonSignIn, "/auth/password"} {
|
|
sent := mastodonBodyField{http.MethodPost, target, mastodonEmail}
|
|
|
|
for _, address := range []string{
|
|
"python3%40example.com",
|
|
"powershell%40example.com",
|
|
"jane.profile%40example.com",
|
|
"notes.htaccess%40example.com",
|
|
} {
|
|
if mastodonForm(t, sent, address) >= mastodonThreshold {
|
|
t.Errorf("%v with %s refused", sent, address)
|
|
}
|
|
}
|
|
|
|
// The address keeps 932250 and the rules that refuse attacks.
|
|
for _, value := range slices.Concat(
|
|
[]string{"ls%20-la"}, mastodonAttacks, []string{commandInjection},
|
|
) {
|
|
if mastodonForm(t, sent, value) < mastodonThreshold {
|
|
t.Errorf("%v with %s let through", sent, value)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Elsewhere the address keeps every rule.
|
|
for _, target := range []string{"/oauth/authorize", "/authorize", "/x/auth"} {
|
|
sent := mastodonBodyField{http.MethodPost, target, mastodonEmail}
|
|
if mastodonForm(t, sent, "python3%40example.com") < mastodonThreshold {
|
|
t.Errorf("%v let through", sent)
|
|
}
|
|
}
|
|
}
|
|
|
|
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"},
|
|
{mastodonSignIn, mastodonEmail + "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)
|
|
}
|
|
}
|
|
}
|