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
65 lines
2.0 KiB
Go
65 lines
2.0 KiB
Go
package proxy
|
|
|
|
import (
|
|
"slices"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
|
"sneak.berlin/go/smallwebwaf/internal/rules"
|
|
)
|
|
|
|
// trapPath reports whether the request asks for a path in
|
|
// SWWAF_TRAP_PATHS: its path as a path rule sees it, without the query,
|
|
// as sent or once percent-decoded, is one of them. Such a request is a
|
|
// clear sign of attack, as a ban rule's match is: it bans the client's
|
|
// netblock, or in observe mode raises the alert for the ban it would have
|
|
// made. The ban's notes give the path as sent.
|
|
func (rq *request) trapPath(now time.Time) bool {
|
|
path := rules.Path(rq.in)
|
|
if !slices.Contains(rq.h.config.TrapPaths, path) &&
|
|
!slices.Contains(rq.h.config.TrapPaths, rules.DecodeOnce(path)) {
|
|
return false
|
|
}
|
|
|
|
rq.attack = true
|
|
rq.banForAttack(now, bans.Notes{TrapPath: path})
|
|
|
|
return true
|
|
}
|
|
|
|
// checkRules checks the request against the rules of the rule files at
|
|
// now, notes the ids of those it matches in the log line, and returns the
|
|
// action of the rule that refuses it, ActionRuleBlocked for a block rule
|
|
// and ActionBanned for a ban rule, or "" when none does. A ban rule bans
|
|
// the client's netblock for a clear sign of attack, or in observe mode
|
|
// raises the alert for the ban it would have made. Either rule's match
|
|
// is noted as an offence, for the client's history.
|
|
func (rq *request) checkRules(now time.Time) string {
|
|
matched := rq.h.rules.Match(rq.in)
|
|
|
|
for _, rule := range matched {
|
|
rq.line.RuleIDs = append(rq.line.RuleIDs, rule.ID)
|
|
rq.h.metrics.RuleMatched(rule.ID, rule.Action)
|
|
}
|
|
|
|
if len(matched) == 0 {
|
|
return ""
|
|
}
|
|
|
|
// Only the last rule matched can refuse the request.
|
|
switch last := matched[len(matched)-1]; last.Action {
|
|
case rules.ActionBlock:
|
|
rq.ruleBlocked = true
|
|
|
|
return requestlog.ActionRuleBlocked
|
|
case rules.ActionBan:
|
|
rq.attack = true
|
|
rq.banForAttack(now, bans.Notes{RuleID: last.ID, Target: last.Target})
|
|
|
|
return requestlog.ActionBanned
|
|
default:
|
|
return ""
|
|
}
|
|
}
|