check / check (push) Waiting to run
Adds nextcloud.conf, the thirteenth change. Nextcloud's WebDAV paths allow PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, REPORT, SEARCH, LOCK and UNLOCK, and the body of a PUT there, an uploaded file, is not read. The cookie nc_username, and the fields that carry a search, a name, a file name or a folder, skip the four rules that read them as commands or system files. Comments, a share's note, a status and a document's text also skip the rules that refused ordinary sentences, and the document the two that refused a shell code block. Requests recorded from Nextcloud 35.0.2, its web pages, sync client 3.16.7 and the OCS API pass at the defaults and while bodies are read. Model: opus-5-5
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_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)
|
|
}
|
|
}
|
|
}
|