waf

package
v0.15.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 26, 2026 License: MIT Imports: 19 Imported by: 0

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.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
)

func (Action) String

func (a Action) String() string

String implements fmt.Stringer for log readability.

type FailMode

type FailMode int

FailMode controls behaviour when rule evaluation errors.

const (
	// FailOpen logs the error and lets the request through. Default.
	FailOpen FailMode = iota
	// FailClosed treats an evaluation error as ActionBlock (status 500).
	FailClosed
)

type Logger

type Logger interface {
	Logf(format string, args ...any)
}

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

type LoggerFunc func(format string, args ...any)

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
	// 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

func (w *WAF) Rules() []string

Rules returns a snapshot of the currently loaded rule IDs in evaluation order. Useful for admin endpoints / introspection.

func (*WAF) ServeHandler

func (w *WAF) ServeHandler(h http.Handler) http.Handler

ServeHandler implements parapet.Middleware.

func (*WAF) SetRules

func (w *WAF) SetRules(rules []Rule) error

SetRules atomically replaces the ruleset.

It compiles every Rule first and only swaps in the new ruleset if all rules compile successfully. This means a bad rule cannot brick the WAF — the previous ruleset stays in place and the caller gets an error describing every failure.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL