Path rules and trap paths match the path decoded once too (closes #85)
check / check (push) Waiting to run
check / check (push) Waiting to run
A path rule matched only the path as the client sent it, so /%2eenv got past the default env-file rule while an app that decodes the path serves /.env. A path rule now matches the path as sent or decoded once, as a uri rule already did, and so does a path in SWWAF_TRAP_PATHS: /%77p-login.php matches /wp-login.php. The query is cut off before the path is decoded, and the path is decoded only once, so /%252eenv is not /.env. A ban's notes keep the path as sent. SPEC.md and README.md say so. Model: opus-5-5
This commit is contained in:
+26
-16
@@ -50,6 +50,13 @@ const quietTime = 2 * time.Second
|
||||
// header:<Name>.
|
||||
const headerTarget = "header:"
|
||||
|
||||
// pathTarget and uriTarget are the targets matched both as received and
|
||||
// once percent-decoded: the path, and the path and query together.
|
||||
const (
|
||||
pathTarget = "path"
|
||||
uriTarget = "uri"
|
||||
)
|
||||
|
||||
// escapeLength is the length of a percent escape, such as %2e.
|
||||
const escapeLength = 3
|
||||
|
||||
@@ -374,7 +381,7 @@ func parse(line string) (Rule, bool, error) {
|
||||
// isTarget reports whether target is one a rule may have.
|
||||
func isTarget(target string) bool {
|
||||
switch target {
|
||||
case "path", "query", "uri", "method", "host", "user_agent", "referer":
|
||||
case pathTarget, "query", uriTarget, "method", "host", "user_agent", "referer":
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -384,35 +391,38 @@ func isTarget(target string) bool {
|
||||
}
|
||||
|
||||
// matches reports whether the rule's regex matches its target in r. For
|
||||
// uri it is matched against the path and query as received, and against
|
||||
// them once percent-decoded, so that an encoded probe cannot slip past.
|
||||
// path and uri it is matched against the target as received, and against
|
||||
// it once percent-decoded, so that an encoded probe cannot slip past.
|
||||
func (rule Rule) matches(r *http.Request) bool {
|
||||
if rule.Target == "uri" {
|
||||
uri := pathAndQuery(r)
|
||||
target := value(rule.Target, r)
|
||||
|
||||
return rule.regex.MatchString(uri) || rule.regex.MatchString(decodeOnce(uri))
|
||||
if rule.Target == pathTarget || rule.Target == uriTarget {
|
||||
return rule.regex.MatchString(target) || rule.regex.MatchString(DecodeOnce(target))
|
||||
}
|
||||
|
||||
return rule.regex.MatchString(value(rule.Target, r))
|
||||
return rule.regex.MatchString(target)
|
||||
}
|
||||
|
||||
// Path returns r's path as the client sent it, before any decoding or
|
||||
// re-encoding, up to the first ?: what a path rule is matched against.
|
||||
// re-encoding, up to the first ?: what a path rule is matched against, as
|
||||
// it is and once percent-decoded.
|
||||
func Path(r *http.Request) string {
|
||||
path, _, _ := strings.Cut(pathAndQuery(r), "?")
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
// value returns what a rule with target, other than uri, is matched
|
||||
// against in r: the path, as Path gives it, and the query as the client
|
||||
// sent it, before any decoding or re-encoding, after the first ?, and a
|
||||
// header's values joined by ", ", as HTTP joins those of a header sent
|
||||
// more than once.
|
||||
// value returns what a rule with target is matched against in r: the
|
||||
// path, as Path gives it, the path and query, as pathAndQuery gives
|
||||
// them, and the query as the client sent it, before any decoding or
|
||||
// re-encoding, after the first ?, and a header's values joined by ", ",
|
||||
// as HTTP joins those of a header sent more than once.
|
||||
func value(target string, r *http.Request) string {
|
||||
switch target {
|
||||
case "path":
|
||||
case pathTarget:
|
||||
return Path(r)
|
||||
case uriTarget:
|
||||
return pathAndQuery(r)
|
||||
case "query":
|
||||
_, query, _ := strings.Cut(pathAndQuery(r), "?")
|
||||
|
||||
@@ -464,11 +474,11 @@ func header(r *http.Request, name string) string {
|
||||
return strings.Join(r.Header.Values(name), ", ")
|
||||
}
|
||||
|
||||
// decodeOnce returns s with each percent escape, such as %2e, replaced by
|
||||
// DecodeOnce returns s with each percent escape, such as %2e, replaced by
|
||||
// the byte it stands for. A % that is not followed by two hex digits is
|
||||
// left as it is, so that a malformed escape cannot keep the rest of s
|
||||
// from being decoded.
|
||||
func decodeOnce(s string) string {
|
||||
func DecodeOnce(s string) string {
|
||||
var decoded strings.Builder
|
||||
|
||||
for i := 0; i < len(s); i++ {
|
||||
|
||||
Reference in New Issue
Block a user