Documentation
¶
Overview ¶
Package waf provides a Web Application Firewall middleware for parapet.
Rules are written in the Common Expression Language (CEL, https://github.com/google/cel-go) which allows safe, sandboxed expressions to be evaluated against incoming HTTP requests without restarting the process.
Quick start ¶
w := waf.New()
_ = w.SetRules([]waf.Rule{{
ID: "block-sqli",
Expression: `request.query.contains("' OR '1'='1") || request.path.matches("(?i).*union.*select.*")`,
Action: waf.ActionBlock,
Status: http.StatusForbidden,
}})
srv := parapet.NewFrontend()
srv.Use(w)
Hot reload ¶
Rules can be replaced atomically at runtime via WAF.SetRules. Existing in-flight requests continue to use the previous compiled ruleset; new requests use the new ruleset on the very next call. Compilation happens inside SetRules so the request path is never blocked by parsing or type-checking.
Variables exposed to expressions ¶
The top-level identifier `request` is a map with these fields:
request.method string request.host string request.path string request.query string // raw query string request.uri string // request URI request.proto string // "HTTP/1.1", "HTTP/2.0", ... request.scheme string // "http" or "https" request.remote_ip string // best-effort client IP (X-Real-IP -> X-Forwarded-For -> RemoteAddr) request.country string // ISO 3166-1 alpha-2 from WAF.Country (e.g. "TH"); "" if unresolved/unset request.asn int // autonomous system number from WAF.ASN (e.g. 13335); 0 if unresolved/unset request.content_length int request.headers map<string, string> // single value per name (canonicalised, lowercase keys) request.cookies map<string, string> request.args map<string, string> // first value of each query parameter request.user_agent string request.referer string request.body string // populated only when WAF.InspectBody > 0; truncated to that many bytes
Custom functions ¶
ipInCidr(ip, cidr) bool // CIDR membership regexMatch(s, pattern) bool // pre-compiled regex (cached) containsAny(s, list) bool // substring contains any of the list entries hasPrefixAny(s, list) bool // hasPrefix any of the list entries lower(s) string // ascii lowercase upper(s) string // ascii uppercase urlDecode(s) string // percent-decode (errors return empty string)
Performance ¶
Each Rule is compiled once when SetRules is called, into a stateless, thread-safe cel.Program. Evaluation uses ContextEval with a CostLimit and per-request deadline (WAF.EvalTimeout) to keep the request path bounded.
Security notes ¶
- Cost limit is enforced (default 1_000_000 units) to prevent runaway rules.
- CEL macros (all/exists/filter/map/comprehensions) are kept enabled by default but bounded by the cost limit. Set WAF.DisableMacros = true to refuse rules that try to use them.
- Body inspection is OFF by default. Enabling it buffers up to InspectBody bytes; set this conservatively to avoid memory-amplification attacks.
- Failed rule compilation returns an error from SetRules; the previous ruleset stays in place.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Action ¶
type Action int
Action describes what the WAF should do when a Rule matches.
const ( // ActionLog records the match (via WAF.Logger) and lets the request continue. // Useful for shadow-deploying new rules. ActionLog Action = iota // ActionAllow short-circuits the WAF chain: no further rules are evaluated // and the request is forwarded to the next handler. This is the inverse of // ActionBlock and is intended for explicit allowlists (e.g. trusted health // checkers, internal scanners). ActionAllow // ActionBlock terminates the request with the rule's Status (defaults to 403). ActionBlock )
type Logger ¶
Logger is the interface used by the WAF to emit match events. It deliberately avoids a hard dependency on a structured logger package; callers can adapt to slog, zap, or stdlib as they prefer.
type LoggerFunc ¶
LoggerFunc adapts a plain function into the Logger interface.
func (LoggerFunc) Logf ¶
func (f LoggerFunc) Logf(format string, args ...any)
Logf implements Logger.
type MatchEvent ¶
type MatchEvent struct {
Request *http.Request
RuleID string
Action Action
// Status is the rule's configured Status field (defaulted to 403 when
// the rule didn't set one). It is the HTTP status that *would be*
// returned for an ActionBlock match; for ActionLog and ActionAllow it
// carries the configured value but does not describe the actual response.
Status int
Expression string
ClientIP string
// Elapsed is the time spent inside the WAF up to and including this
// match — i.e. evaluating every rule that ran before this one plus the
// matching rule itself. It is NOT the per-rule evaluation latency.
Elapsed time.Duration
}
MatchEvent is delivered to OnMatch (if set) for every rule that fires. It is intentionally lightweight so handlers don't have to copy the request.
type Rule ¶
type Rule struct {
// ID is a stable identifier used in logs. Required.
ID string
// Description is an optional human-readable summary recorded in match logs.
Description string
// Expression is a CEL expression that must evaluate to bool.
// See package docs for the available variables and functions.
Expression string
// Action determines what to do when the expression returns true.
Action Action
// Status is the HTTP status returned for ActionBlock matches.
// Defaults to 403 Forbidden when zero.
Status int
// Message is the response body for ActionBlock matches. Defaults to a
// generic "Forbidden" string. Returned as text/plain.
Message string
// Priority controls evaluation order; lower values run first.
// Allowlist rules should typically have the smallest Priority so they
// short-circuit before any block rules are considered.
Priority int
}
Rule is a single WAF rule definition.
Rules are sorted by ascending Priority (smaller runs first) when SetRules is called. Within equal priorities, declaration order is preserved.
type WAF ¶
type WAF struct {
// Logger receives one line per matched rule. nil disables logging.
Logger Logger
// OnMatch is invoked for every rule that fires (any Action). Optional.
// Runs synchronously on the request goroutine — keep it cheap.
OnMatch func(MatchEvent)
// EvalTimeout is the per-request deadline for evaluating the entire
// ruleset. Defaults to 5ms. A timeout treats the request as Allow
// (i.e. fails open) but logs the error — this is the safer default for
// a reverse proxy because failing closed during a config bug would
// drop legitimate traffic. Override with FailMode to change behaviour.
EvalTimeout time.Duration
// CostLimit caps CEL evaluator cost per rule. 0 = use defaultCostLimit.
// Set explicitly to a smaller number when running untrusted rules.
CostLimit uint64
// FailMode controls behaviour when a rule errors at evaluation time
// (panic recovered, timeout, cost exceeded, type mismatch).
// Default is FailOpen: error rules are skipped and the request continues.
FailMode FailMode
// DisableMacros prevents rules from using all/exists/filter/map/etc.
// when set to true. Recommended when rules come from less-trusted sources.
DisableMacros bool
// InspectBody enables body inspection up to N bytes. 0 = body is empty
// in expressions (request.body == ""). The buffered body is restored
// to r.Body so downstream handlers can still read it.
InspectBody int64
// Country resolves the client's ISO 3166-1 alpha-2 country code for a
// request, exposed to rules as `request.country` (e.g. for GeoIP filtering:
// `request.country == "TH"`). The WAF stays storage-agnostic — the caller
// supplies the lookup (a GeoIP database, an edge header, etc.). nil leaves
// `request.country` as the empty string; the field is always present in the
// map, so a rule referencing it never errors on a missing key.
Country func(r *http.Request) string
// ASN resolves the client's autonomous system number for a request,
// exposed to rules as `request.asn` (e.g. for ASN filtering:
// `request.asn == 13335`). The WAF stays storage-agnostic — the caller
// supplies the lookup (an IP-to-ASN database, an edge header, etc.). nil
// leaves `request.asn` as 0; the field is always present in the map, so a
// rule referencing it never errors on a missing key. The value is an int64
// (not uint) so rules compare against plain CEL integer literals; valid
// ASNs fit losslessly, and 0 — reserved by RFC 7607 — means unresolved.
ASN func(r *http.Request) int64
// contains filtered or unexported fields
}
WAF is the middleware. Construct with New.
All fields except the atomic rules pointer should be configured before the first request is served. Rules can be replaced at any time via SetRules.
func New ¶
func New() *WAF
New creates a WAF with no rules loaded. Call SetRules before mounting it.
func (*WAF) Rules ¶
Rules returns a snapshot of the currently loaded rule IDs in evaluation order. Useful for admin endpoints / introspection.
func (*WAF) ServeHandler ¶
ServeHandler implements parapet.Middleware.