Web Application Firewall (WAF)
Sentinel includes a built-in Web Application Firewall that inspects every incoming HTTP request for malicious payloads. It provides comprehensive coverage for the OWASP Top 10 attack categories, including SQL injection, cross-site scripting (XSS), path traversal, command injection, server-side request forgery (SSRF), XML external entity injection (XXE), local file inclusion (LFI), and open redirect attacks.
The WAF operates as Gin middleware registered by sentinel.Mount(). It runs before your application handlers, so malicious requests are intercepted before they reach your business logic. All detections are recorded as security events and are visible in the Sentinel dashboard.
Tune Before You Block
ModeLog, watch the dashboard, and adjust sensitivity per category before switching to ModeBlock.Enabling the WAF
The WAF is disabled by default. To enable it, set Enabled: true in your WAFConfig and choose a mode. The simplest configuration enables blocking mode with all default rule sensitivities:
1package main23import (4 sentinel "github.com/MUKE-coder/sentinel/v2"5 "github.com/gin-gonic/gin"6)78func main() {9 r := gin.Default()1011 sentinel.Mount(r, nil, sentinel.Config{12 WAF: sentinel.WAFConfig{13 Enabled: true,14 Mode: sentinel.ModeBlock,15 },16 })1718 r.GET("/api/data", func(c *gin.Context) {19 c.JSON(200, gin.H{"status": "ok"})20 })2122 r.Run(":8080")23}
With this configuration, Sentinel inspects every request against all built-in rule categories at their default strictness levels. Malicious requests receive an HTTP 403 response and are logged as security events.
WAF Modes
The WAF supports two primary operating modes. The mode determines what happens when a malicious payload is detected.
| Mode | Constant | Behavior | Use Case |
|---|---|---|---|
| Log | sentinel.ModeLog | Detects and records threats but allows the request to proceed. The event is logged and visible in the dashboard. | Initial rollout, testing, auditing |
| Block | sentinel.ModeBlock | Detects threats and rejects the request with HTTP 403 Forbidden. The response includes a JSON body with the block reason. | Production enforcement |
| Challenge | sentinel.ModeChallenge | Rejects the request with HTTP 429, Retry-After: 30, and code WAF_CHALLENGE. There is no interactive challenge page. | Slowing automated clients |
Any other value — including a typo such as "Block" — falls through to log mode and blocks nothing. ValidateConfig reports it as an error, and the dashboard API rejects it. The mode can be changed on the running WAF with PUT /sentinel/api/waf/rules (since v2.3.0). Since v2.6.0 the change is stored: it survives a restart and reaches every replica within Storage.SyncInterval.
ModeLog (Detect Only)
In log mode, the WAF identifies malicious payloads and creates security events, but it does not block the request. This is ideal for initial deployment when you want to observe what the WAF detects without impacting real traffic.
1WAF: sentinel.WAFConfig{2 Enabled: true,3 Mode: sentinel.ModeLog, // Detect and log, but don't block4}
When a threat is detected in log mode, the request continues to your handler normally. The detection is recorded and appears in the dashboard under the Events page with full details including the matched pattern, severity, and request metadata.
ModeBlock (Block Malicious Requests)
In block mode, detected threats are rejected immediately with an HTTP 403 status code. The request never reaches your application handler. The response body contains a JSON object describing the block:
1WAF: sentinel.WAFConfig{2 Enabled: true,3 Mode: sentinel.ModeBlock, // Detect and block with 4034}
# Example blocked responseHTTP/1.1 403 ForbiddenContent-Type: application/json{"error": "Request blocked by WAF","code": "WAF_BLOCKED"}
Recommended Rollout Strategy
sentinel.ModeLog in production for 1-2 weeks to observe detections and identify any false positives. Review the dashboard, tune rule sensitivities and exclusions, then switch to sentinel.ModeBlock when you are confident in the configuration. After that, stage each new custom rule with Action: "log" — it is recorded but never blocks — and promote it to "block" once it has proven itself.Built-in Rules
Sentinel ships with eight built-in detection rule categories that cover the most common web attack vectors. Each category has its own set of regex patterns and can be independently configured with a strictness level.
Rule Categories
| Category | Field | Default | Description |
|---|---|---|---|
| SQL Injection | SQLInjection | RuleStrict | Detects SQL injection attempts including UNION-based, boolean-based, time-based blind, and error-based injection patterns. |
| Cross-Site Scripting | XSS | RuleStrict | Detects XSS payloads including script tags, event handlers (onerror, onload), javascript: URIs, and encoded variants. |
| Path Traversal | PathTraversal | RuleStrict | Detects directory traversal sequences (../, ..%2f) targeting sensitive system files like /etc/passwd or web.config. |
| Command Injection | CommandInjection | RuleStrict | Detects OS command injection via shell metacharacters (;, |, &&, backticks) and common commands (cat, wget, curl). |
| SSRF | SSRF | RuleMedium | Detects server-side request forgery attempts targeting internal networks (127.0.0.1, 169.254.169.254, localhost, private IP ranges). |
| XXE | XXE | RuleStrict | Detects XML external entity injection via DOCTYPE declarations, ENTITY definitions, and SYSTEM identifiers in request bodies. |
| LFI | LFI | RuleStrict | Detects local file inclusion attempts targeting common sensitive paths (/etc/shadow, /proc/self, php://filter). |
| Open Redirect | OpenRedirect | RuleMedium | Detects open redirect attempts: a redirect parameter (redirect, return, next, url, callback, continue, goto and similar) holding an absolute or protocol-relative URL (//evil.com). List your own hosts in AllowedRedirectHosts so your callbacks aren't reported. |
Strictness Levels
Each rule category supports four strictness levels. Every built-in pattern carries a confidence score (50 for the bare SQL-comment pattern, up to 85 for the most specific ones), and a level keeps the patterns at or above its floor. Higher strictness catches more edge cases but may produce more false positives for certain applications.
| Level | Constant | Description |
|---|---|---|
| Off | sentinel.RuleOff | Disables this rule category. No matches are recorded. |
| Low | sentinel.RuleLow | Only the most precise patterns (confidence ≥ 80). Lowest false-positive rate. |
| Medium | sentinel.RuleMedium | Drops the noisiest patterns (confidence below 60). |
| Strict | sentinel.RuleStrict | Every pattern, including encoded and obfuscated variants. Also what an empty value means. |
Enforced since v2.3.0
PUT /sentinel/api/waf/rules.1WAF: sentinel.WAFConfig{2 Enabled: true,3 Mode: sentinel.ModeBlock,4 Rules: sentinel.RuleSet{5 SQLInjection: sentinel.RuleStrict, // Maximum SQL injection coverage6 XSS: sentinel.RuleStrict, // Maximum XSS coverage7 PathTraversal: sentinel.RuleStrict, // Catch encoded traversal sequences8 CommandInjection: sentinel.RuleLow, // Most precise patterns only — fewer false positives9 SSRF: sentinel.RuleMedium, // Balanced SSRF detection10 XXE: sentinel.RuleStrict, // Full XXE protection11 LFI: sentinel.RuleStrict, // Full LFI protection12 OpenRedirect: sentinel.RuleOff, // Disabled — app handles redirects internally13 },14}
Disabling Rules
sentinel.RuleOff completely disables that detection class. Only do this if you are certain your application is not vulnerable to that attack vector, or if you have other defenses in place (e.g., a cloud WAF handling that category).Custom Rules
In addition to the built-in rules, you can define custom rules to detect application-specific attack patterns. Custom rules are defined as WAFRule structs and are evaluated alongside the built-in rules during request inspection.
WAFRule Struct
| Field | Type | Description |
|---|---|---|
ID | string | Unique identifier for the rule. Used in event logs and the dashboard. Must be unique across all custom rules. |
Name | string | Human-readable name displayed in the dashboard and event details. |
Pattern | string | Go-compatible regular expression. Compiled once at startup for performance. Use raw strings (`...`) to avoid double-escaping. |
AppliesTo | []string | Which parts of the request to inspect. Valid values: "path", "query", "headers", "body". You can specify multiple. |
Severity | Severity | Severity level assigned to matches. Options: sentinel.SeverityLow, sentinel.SeverityMedium, sentinel.SeverityHigh, sentinel.SeverityCritical. |
Action | string | "block" (or empty) lets the global WAF mode decide what happens to a matching request. "log" records the match but never blocks — even in block mode. Any other value is enforced as "block" and reported by ValidateConfig. |
Enabled | bool | Whether the rule is active. Set to false to disable a rule without removing it from the configuration. |
AppliesTo Targets
| Target | What Is Inspected |
|---|---|
"path" | The URL path (e.g., /api/users/123) |
"query" | The raw query string (e.g., id=1&name=test) |
"header" | Every HTTP request header value, checked one at a time |
"body" | The request body (for POST, PUT, PATCH requests) |
1CustomRules: []sentinel.WAFRule{2 {3 ID: "block-wp-admin",4 Name: "Block WordPress admin probes",5 Pattern: `(?i)/(wp-admin|wp-login\.php|xmlrpc\.php|wp-content)`,6 AppliesTo: []string{"path"},7 Severity: sentinel.SeverityMedium,8 Action: "block",9 Enabled: true,10 },11 {12 ID: "block-sensitive-files",13 Name: "Block sensitive file access",14 Pattern: `(?i)\.(env|git|bak|sql|log|ini|cfg|conf|swp)$`,15 AppliesTo: []string{"path"},16 Severity: sentinel.SeverityHigh,17 Action: "block",18 Enabled: true,19 },20 {21 ID: "block-scanner-bots",22 Name: "Block known scanner user agents",23 Pattern: `(?i)(sqlmap|nikto|nessus|dirbuster|gobuster|masscan|nmap)`,24 AppliesTo: []string{"header"},25 Severity: sentinel.SeverityHigh,26 Action: "block",27 Enabled: true,28 },29 {30 ID: "log-api-key-exposure",31 Name: "Detect API key in query string",32 Pattern: `(?i)(api[_-]?key|apikey|secret|token|password)=.+`,33 AppliesTo: []string{"query"},34 Severity: sentinel.SeverityMedium,35 Action: "log",36 Enabled: true,37 },38}
Rule Action vs Global Mode
Action: "log" can only make a rule quieter than the global mode: in block mode, a request that trips only log-action rules is recorded and allowed through. It cannot make a rule block while the WAF is in log mode. A request that trips a log-action rule and an enforced rule (or a built-in pattern) is still blocked. Built-in rules always follow the global mode. Action was never read before v2.3.0.Excluding Routes and IPs
You can exclude specific routes and IP addresses from WAF inspection. Excluded routes and IPs bypass the WAF entirely — no patterns are evaluated and no events are generated for these requests.
ExcludeRoutes
Use ExcludeRoutes to skip WAF inspection for specific URL paths. This is commonly used for health check endpoints, metrics endpoints, or routes that legitimately handle content that might trigger WAF rules (e.g., a CMS editor).
1WAF: sentinel.WAFConfig{2 Enabled: true,3 Mode: sentinel.ModeBlock,4 ExcludeRoutes: []string{5 "/health", // Health check endpoint6 "/readiness", // Kubernetes readiness probe7 "/metrics", // Prometheus metrics8 "/api/v1/webhooks", // Incoming webhooks may contain arbitrary content9 },10}
ExcludeIPs
Use ExcludeIPs to skip WAF inspection for trusted IP addresses or CIDR ranges. This is useful for internal services, monitoring systems, or CI/CD pipelines that might trigger false positives.
1WAF: sentinel.WAFConfig{2 Enabled: true,3 Mode: sentinel.ModeBlock,4 ExcludeIPs: []string{5 "127.0.0.1", // Localhost6 "10.0.0.0/8", // Internal network7 "172.16.0.0/12", // Docker networks8 "192.168.0.0/16", // Private network9 "203.0.113.50", // Monitoring server10 },11}
Be Careful with IP Exclusions
X-Forwarded-For unless the direct connection comes from one of your WAF.TrustedProxies, and then reads the chain right to left, skipping your proxies — the entries a client writes itself are never trusted. Set TrustedProxies to your load balancer's addresses; Gin's own trusted-proxy setting is not used.How Detection Works
Understanding the WAF detection pipeline helps you configure it effectively and debug false positives.
Request Inspection Flow
- Exclusion Check — The middleware first checks if the request IP or route is in the exclusion lists. If so, the request is passed through without inspection.
- Request Decomposition — The classifier scans the URL path, the raw query string and each decoded parameter name and value, sixheaders (
Referer,User-Agent,Cookie,X-Forwarded-For,Content-Type,Origin), and the request body (up toMaxBodyBytes). Anything still percent-encoded after the router decoded it — in the path, a parameter, or a form-encoded body — is decoded up to two more times and every layer is scanned, so a double-encoded payload (%2527for an apostrophe) is caught. A stray%that isn't an escape doesn't stop the decoding. - Pattern Matching — Each extracted component is evaluated against the compiled regex patterns for all enabled rule categories. The patterns are compiled once at startup for performance. Both built-in rules and custom rules are evaluated.
- Threat Event Creation — When a pattern matches, a
ThreatEventis created containing the rule ID, category, severity, matched pattern, the request component that matched, and full request metadata (IP, user agent, timestamp). - Async Pipeline — The threat event is sent to the async event pipeline via a non-blocking ring buffer. Worker goroutines pick up the event and persist it to storage, update threat actor profiles, recompute security scores, and dispatch alerts if the severity threshold is met.
- Response — Based on the WAF mode (or the custom rule action), the request is either blocked with a 403 response or allowed to proceed to the next handler.
# The detection flow visualized:## Request ──> Exclusion Check ──> Decompose Request ──> Pattern Matching# │ │# │ (excluded) ┌─────────┴─────────┐# v v v# Pass Through No Match Match Found# │ │# v v# Pass Through Create ThreatEvent# │# ┌──────────┴──────────┐# v v# ModeLog ModeBlock# │ │# v v# Log + Continue Log + 403
Non-Blocking Event Pipeline
Measured Accuracy
The built-in patterns are measured against two corpora in the repository:detection/testdata/benign.tsv and detection/testdata/attacks.tsv.
- benign.tsv holds legitimate requests picked to look suspicious: SQL keywords in searches, Markdown with backticks, code in support messages, non-Latin text, and URLs in parameters.
- attacks.tsv holds payloads, including double-encoded, form-encoded, and MySQL-comment evasions.
CI fails if a pattern change raises the false-positive count or lowers the detection count.
| Sensitivity | False positives (89 legitimate requests) | Attacks detected (56 payloads) |
|---|---|---|
low everywhere | 7 (7.9%) | 41 (73.2%) |
medium everywhere | 9 (10.1%) | 54 (96.4%) |
strict everywhere | 9 (10.1%) | 56 (100%) |
| Default rule set | 9 (10.1%) | 56 (100%) |
Up to v2.3.1, the patterns flagged 32 of the first 84 legitimate samples (38%) and missed 4 of the first 51 attacks, including double-encoded SQL injection. To reproduce the table, run go test ./detection -run TestCorpus -v. It also lists every sample the default rule set gets wrong.
What the numbers don't tell you
The corpora are small and hand-written. They show how the patterns treat known-tricky inputs, not what your traffic will do.
The remaining false positives are inputs no pattern can tell apart from an attack:
- SQL quoted in a support message
- a hex id that is the whole value
../in a search- an encoded
<script>tag - absolute URLs in callback parameters — unless you list your hosts in
AllowedRedirectHosts, which Sentinel cannot know on its own
Run in ModeLog first and review the Threats page before switching to ModeBlock. Exclude routes that accept code or SQL.
Testing the WAF
After enabling the WAF, you should test it to verify that it correctly detects and handles malicious requests. Below are curl commands you can use to test each major attack category.
Testing SQL Injection
# Classic SQL injection in query parametercurl -v "http://localhost:8080/api/users?id=1'+OR+'1'='1"# UNION-based SQL injectioncurl -v "http://localhost:8080/api/users?id=1+UNION+SELECT+username,password+FROM+users--"# Boolean-based blind injectioncurl -v "http://localhost:8080/api/users?id=1+AND+1=1"# SQL injection in POST bodycurl -v -X POST http://localhost:8080/api/search \-H "Content-Type: application/json" \-d '{"query": "test\"; DROP TABLE users; --"}'
Testing XSS
# Script tag injectioncurl -v "http://localhost:8080/api/search?q=<script>alert('xss')</script>"# Event handler injectioncurl -v "http://localhost:8080/api/search?q=<img+src=x+onerror=alert(1)>"# JavaScript URIcurl -v "http://localhost:8080/api/search?q=javascript:alert(document.cookie)"
Testing Path Traversal
# Basic path traversalcurl -v "http://localhost:8080/api/files/../../../../etc/passwd"# URL-encoded traversalcurl -v "http://localhost:8080/api/files/..%2f..%2f..%2fetc%2fpasswd"# Double-encoded traversalcurl -v "http://localhost:8080/api/files/..%252f..%252f..%252fetc%252fpasswd"
Testing Command Injection
# Semicolon injectioncurl -v "http://localhost:8080/api/ping?host=127.0.0.1;cat+/etc/passwd"# Pipe injectioncurl -v "http://localhost:8080/api/ping?host=127.0.0.1|whoami"# Backtick injectioncurl -v "http://localhost:8080/api/ping?host=\`whoami\`"
Expected Responses
The response you receive depends on the WAF mode:
ModeBlock Response (403 Forbidden)
$ curl -s "http://localhost:8080/api/users?id=1'+OR+'1'='1" | jq .{"error": "Request blocked by WAF","code": "WAF_BLOCKED"}# The matched pattern and location are on the threat in the dashboard,# never in the response: an attacker learns nothing about which rule fired.
ModeLog Response (200 OK, event logged)
$ curl -s "http://localhost:8080/api/users?id=1'+OR+'1'='1" | jq .{"users": []}# The request succeeds, but a WAF event is logged in the dashboard.# Check the Sentinel Events page to see the detection.
Testing in Production
Scanning with ZAP and sqlmap
The security/scan directory in the repository scans a deliberately injectable app (examples/scan-target) with OWASP ZAP and sqlmap. Run it with the WAF off, to confirm the holes are real, and then on block. A payload that gets through belongs in detection/testdata/attacks.tsv. A legitimate request that gets blocked belongs in benign.tsv.
cd security/scanSENTINEL_WAF=off docker compose up -d --build target && docker compose run --rm sqlmapSENTINEL_WAF=block docker compose up -d --build target && docker compose run --rm sqlmapdocker compose down
Custom Rule Examples
Below are practical custom rule examples for common use cases. These demonstrate the flexibility of the WAF rule engine for application-specific security needs.
Block WordPress and CMS Admin Probes
Bots constantly scan for common CMS admin panels. If your application is not WordPress, block these immediately to reduce noise.
1{2 ID: "block-cms-probes",3 Name: "Block CMS admin panel probes",4 Pattern: `(?i)/(wp-admin|wp-login\.php|wp-content|wp-includes|xmlrpc\.php|administrator|phpmyadmin|adminer|cgi-bin)`,5 AppliesTo: []string{"path"},6 Severity: sentinel.SeverityMedium,7 Action: "block",8 Enabled: true,9}
Block Sensitive File Extensions
Prevent access to backup files, configuration files, and version control artifacts that should never be served by your application.
1{2 ID: "block-sensitive-extensions",3 Name: "Block sensitive file extensions",4 Pattern: `(?i)\.(env|git|gitignore|bak|sql|log|ini|cfg|conf|swp|old|orig|dist|save|DS_Store|htaccess|htpasswd)$`,5 AppliesTo: []string{"path"},6 Severity: sentinel.SeverityHigh,7 Action: "block",8 Enabled: true,9}
Block Known Scanner User Agents
Automated security scanners identify themselves via their User-Agent header. Block known scanner signatures to reduce noise in your security logs.
1{2 ID: "block-scanner-agents",3 Name: "Block known vulnerability scanner agents",4 Pattern: `(?i)(sqlmap|nikto|nessus|nmap|masscan|dirbuster|gobuster|wfuzz|ffuf|burpsuite|zap|acunetix|qualys|openvas)`,5 AppliesTo: []string{"headers"},6 Severity: sentinel.SeverityHigh,7 Action: "block",8 Enabled: true,9}
Detect API Keys in Query Strings
API keys should be sent in headers, not query strings (which appear in logs and browser history). This rule logs a warning when credentials appear in URLs.
1{2 ID: "detect-credentials-in-url",3 Name: "Detect credentials in query string",4 Pattern: `(?i)(api[_-]?key|apikey|secret[_-]?key|access[_-]?token|auth[_-]?token|password|passwd)=[^&]+`,5 AppliesTo: []string{"query"},6 Severity: sentinel.SeverityMedium,7 Action: "log", // Log only — don't block, just alert8 Enabled: true,9}
Full Custom Rules Configuration
Here is a complete example combining multiple custom rules with the built-in rule set:
1sentinel.Mount(r, nil, sentinel.Config{2 WAF: sentinel.WAFConfig{3 Enabled: true,4 Mode: sentinel.ModeBlock,5 Rules: sentinel.RuleSet{6 SQLInjection: sentinel.RuleStrict,7 XSS: sentinel.RuleStrict,8 PathTraversal: sentinel.RuleStrict,9 CommandInjection: sentinel.RuleStrict,10 SSRF: sentinel.RuleMedium,11 XXE: sentinel.RuleStrict,12 LFI: sentinel.RuleStrict,13 OpenRedirect: sentinel.RuleMedium,14 },15 CustomRules: []sentinel.WAFRule{16 {17 ID: "block-cms-probes",18 Name: "Block CMS admin panel probes",19 Pattern: `(?i)/(wp-admin|wp-login\.php|xmlrpc\.php|phpmyadmin)`,20 AppliesTo: []string{"path"},21 Severity: sentinel.SeverityMedium,22 Action: "block",23 Enabled: true,24 },25 {26 ID: "block-sensitive-extensions",27 Name: "Block sensitive file extensions",28 Pattern: `(?i)\.(env|git|bak|sql|log|swp)$`,29 AppliesTo: []string{"path"},30 Severity: sentinel.SeverityHigh,31 Action: "block",32 Enabled: true,33 },34 {35 ID: "block-scanner-agents",36 Name: "Block scanner user agents",37 Pattern: `(?i)(sqlmap|nikto|nessus|dirbuster|gobuster|nmap)`,38 AppliesTo: []string{"headers"},39 Severity: sentinel.SeverityHigh,40 Action: "block",41 Enabled: true,42 },43 {44 ID: "detect-credentials-in-url",45 Name: "Detect credentials in query string",46 Pattern: `(?i)(api[_-]?key|secret|token|password)=[^&]+`,47 AppliesTo: []string{"query"},48 Severity: sentinel.SeverityMedium,49 Action: "log",50 Enabled: true,51 },52 },53 ExcludeRoutes: []string{"/health", "/readiness", "/metrics"},54 ExcludeIPs: []string{"10.0.0.0/8", "172.16.0.0/12"},55 },56})
WAF Dashboard
The Sentinel dashboard includes a dedicated WAF page that provides a visual interface for managing and monitoring the Web Application Firewall.
Dashboard Capabilities
- Rule Management — View all active built-in and custom rules. Enable or disable individual rules, and adjust strictness levels without restarting the application.
- Live Input Testing — Test arbitrary input strings against the active rule set directly from the dashboard. The tester shows which rules would match, the severity, and what action would be taken.
- Detection Statistics — View real-time stats on total detections, detections by category, top blocked IPs, most targeted routes, and detection trends over time.
- Event Log — Browse all WAF events with filtering by category, severity, IP, time range, and rule ID. Each event shows the full request details and matched pattern.
- Custom Rule Editor — Create, edit, and test custom rules directly from the dashboard UI without modifying Go code.
Access the WAF dashboard page at http://localhost:8080/sentinel/ui and navigate to the WAF section.
Dashboard Rule Changes
Next Steps
- Rate Limiting — Layer rate limiting on top of WAF protection
- Auth Shield — Protect authentication endpoints from brute force attacks
- Anomaly Detection — Add behavioral analysis for advanced threat detection
- Alerting — Get notified when the WAF detects high-severity threats
- Dashboard — Explore the full security dashboard