check / check (push) Waiting to run
SWWAF_WAF_BODY_LIMIT (default off, at most 1G) has the Core Rule Set read form data and multipart up to the limit, the rest streaming on, and JSON and XML no larger than it, with text/json and the application and text types ending in +json or +xml. The part read is held for the app. A size or time limit met while reading ends the request. Content-Encoding is refused again on these kinds. A body Coraza cannot parse, or a multipart body failing its strict checks, adds 5, as does a multipart body the limit cuts before the colon of a part's header. Coraza is built with no_fs_access, so writes no file. Rule 900300 moves to phase 2. Judgement call: Content-Encoding is refused on a JSON or XML body too large to read, as SPEC.md allows. Model: opus-5-5
125 lines
3.8 KiB
Go
125 lines
3.8 KiB
Go
package proxy
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
|
"sneak.berlin/go/smallwebwaf/internal/config"
|
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
|
"sneak.berlin/go/smallwebwaf/internal/waf"
|
|
)
|
|
|
|
// checkCoreRuleSet inspects the request with the Core Rule Set, unless
|
|
// SWWAF_WAF_MODE is off or SWWAF_WAF_EXEMPT_PATHS exempts its path, as
|
|
// pathExempt decides, and notes the rules it matched and its score in the
|
|
// log line, and the rules in the metrics. A score at or over
|
|
// SWWAF_WAF_ANOMALY_THRESHOLD is a match: it raises the waf_block alert,
|
|
// and in block mode refuses the request, which is an offence its client's
|
|
// history counts, and so returns ActionWAFBlocked. It returns "" for a
|
|
// request it does not refuse, and for one whose body meets a size or time
|
|
// limit while the Core Rule Set reads it, which it notes nothing of.
|
|
func (rq *request) checkCoreRuleSet() string {
|
|
cfg := rq.h.config
|
|
if cfg.WAFMode == config.WAFModeOff || pathExempt(rq.in.URL, cfg.WAFExemptPaths) {
|
|
return ""
|
|
}
|
|
|
|
start := time.Now()
|
|
|
|
result := rq.inspect()
|
|
if rq.refused.Load() != nil {
|
|
return "" // the refusal for that limit, which check returns
|
|
}
|
|
|
|
rq.line.DurationWAF = new(requestlog.Milliseconds(time.Since(start)))
|
|
rq.line.WAFRuleIDs = result.RuleIDs
|
|
rq.line.WAFScore = &result.Score
|
|
|
|
for _, id := range result.RuleIDs {
|
|
rq.h.metrics.WAFMatched(cfg.WAFMode, id)
|
|
}
|
|
|
|
threshold := cfg.WAFAnomalyThreshold
|
|
if threshold == 0 || result.Score < threshold {
|
|
return ""
|
|
}
|
|
|
|
rq.alertWAFBlock(result)
|
|
|
|
if cfg.WAFMode == config.WAFModeDetect {
|
|
return ""
|
|
}
|
|
|
|
rq.wafBlocked = true
|
|
|
|
return requestlog.ActionWAFBlocked
|
|
}
|
|
|
|
// inspect runs the Core Rule Set on the request, which reads the part of
|
|
// its body it inspects within SWWAF_CLIENT_REQUEST_TIMEOUT, and keeps that
|
|
// part for the app. A client that runs out of time is refused with 408
|
|
// here, and a body over SWWAF_REQUEST_MAX_BYTES with 413 as it is read;
|
|
// check returns the refusal. A body that breaks off for any other reason
|
|
// is passed on as far as it came, and the request to the app fails there,
|
|
// as it would have without the Core Rule Set.
|
|
func (rq *request) inspect() waf.Result {
|
|
if rq.body == nil {
|
|
// Nothing is read of no body, so nothing can go wrong reading it.
|
|
result, _, _ := rq.h.coreRuleSet.Inspect(rq.in, rq.client, http.NoBody)
|
|
|
|
return result
|
|
}
|
|
|
|
_ = rq.rc.SetReadDeadline(rq.clientRequestDeadline())
|
|
result, read, err := rq.h.coreRuleSet.Inspect(rq.in, rq.client, rq.body)
|
|
// The timeouts that run while the request goes to the app take over.
|
|
_ = rq.rc.SetReadDeadline(time.Time{})
|
|
|
|
rq.body.readByCoreRuleSet = read
|
|
|
|
if errors.Is(err, os.ErrDeadlineExceeded) {
|
|
rq.refuse(refusal{
|
|
status: http.StatusRequestTimeout,
|
|
action: requestlog.ActionTimedOut,
|
|
limit: "SWWAF_CLIENT_REQUEST_TIMEOUT",
|
|
})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// alertWAFBlock raises the waf_block alert for the request, which the Core
|
|
// Rule Set scored at result, at or over SWWAF_WAF_ANOMALY_THRESHOLD. Its
|
|
// detail gives the rule ids, the score, the method and the path with the
|
|
// query, and, for a request that is not refused for it, the mode: detect,
|
|
// or observe in observe mode.
|
|
func (rq *request) alertWAFBlock(result waf.Result) {
|
|
detail := map[string]any{
|
|
"rule_ids": result.RuleIDs,
|
|
"score": result.Score,
|
|
"method": rq.in.Method,
|
|
"path": rq.in.URL.RequestURI(),
|
|
}
|
|
|
|
switch {
|
|
case rq.h.config.WAFMode == config.WAFModeDetect:
|
|
detail["mode"] = config.WAFModeDetect
|
|
case rq.h.config.Observe:
|
|
detail["mode"] = "observe"
|
|
}
|
|
|
|
rq.h.alerts.Raise(alerts.Alert{
|
|
Event: alerts.EventWAFBlock,
|
|
Client: rq.client,
|
|
Netblock: rq.h.clientGroup(rq.client),
|
|
ASN: rq.line.ASN,
|
|
ASName: rq.line.ASName,
|
|
Country: rq.line.Country,
|
|
Reason: "scored by the Core Rule Set at or over SWWAF_WAF_ANOMALY_THRESHOLD",
|
|
Detail: detail,
|
|
})
|
|
}
|