December 20, 2023 · 5 min read
WAF Rules That Actually Block Common Attacks
Practical AWS WAF configuration patterns to protect your applications from SQL injection, XSS, and other OWASP Top 10 vulnerabilities.
December 20, 2023 · 5 min read
Practical AWS WAF configuration patterns to protect your applications from SQL injection, XSS, and other OWASP Top 10 vulnerabilities.
Most WAF configurations either block too much (breaking legitimate traffic) or too little (providing false security). Here's how to configure AWS WAF rules that actually work in production.
AWS Managed Rules are a good start, but they're designed for the general case. Your application has specific patterns that require custom tuning.
Common issues:
Start with the baseline:
resource "aws_wafv2_web_acl" "main" {
name = "production-waf"
scope = "REGIONAL"
default_action {
allow {}
}
rule {
name = "AWSManagedRulesCommonRuleSet"
priority = 10
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
# Exclude rules that cause false positives
rule_action_override {
action_to_use {
count {}
}
name = "SizeRestrictions_BODY"
}
}
}
visibility_config {
sampled_requests_enabled = true
cloudwatch_metrics_enabled = true
metric_name = "AWSCommonRules"
}
}
}
SQL injection remains a top attack vector:
rule {
name = "AWSManagedRulesSQLiRuleSet"
priority = 20
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesSQLiRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
sampled_requests_enabled = true
cloudwatch_metrics_enabled = true
metric_name = "AWSSQLiRules"
}
}
Block requests with known malicious patterns:
rule {
name = "AWSManagedRulesKnownBadInputsRuleSet"
priority = 30
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesKnownBadInputsRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
sampled_requests_enabled = true
cloudwatch_metrics_enabled = true
metric_name = "AWSBadInputsRules"
}
}
Prevent brute force and DDoS:
rule {
name = "RateLimitRule"
priority = 5
action {
block {}
}
statement {
rate_based_statement {
limit = 2000
aggregate_key_type = "IP"
scope_down_statement {
not_statement {
statement {
ip_set_reference_statement {
arn = aws_wafv2_ip_set.allowed_ips.arn
}
}
}
}
}
}
visibility_config {
sampled_requests_enabled = true
cloudwatch_metrics_enabled = true
metric_name = "RateLimitRule"
}
}
Block traffic from countries you don't serve:
rule {
name = "GeoBlockRule"
priority = 1
action {
block {}
}
statement {
geo_match_statement {
country_codes = ["RU", "CN", "KP", "IR"]
}
}
visibility_config {
sampled_requests_enabled = true
cloudwatch_metrics_enabled = true
metric_name = "GeoBlockRule"
}
}
Stop scanners hitting common exploit paths:
rule {
name = "BlockCommonExploitPaths"
priority = 15
action {
block {}
}
statement {
or_statement {
statement {
byte_match_statement {
field_to_match {
uri_path {}
}
positional_constraint = "CONTAINS"
search_string = "wp-admin"
text_transformation {
priority = 0
type = "LOWERCASE"
}
}
}
statement {
byte_match_statement {
field_to_match {
uri_path {}
}
positional_constraint = "CONTAINS"
search_string = "phpmyadmin"
text_transformation {
priority = 0
type = "LOWERCASE"
}
}
}
statement {
byte_match_statement {
field_to_match {
uri_path {}
}
positional_constraint = "CONTAINS"
search_string = ".env"
text_transformation {
priority = 0
type = "LOWERCASE"
}
}
}
}
}
visibility_config {
sampled_requests_enabled = true
cloudwatch_metrics_enabled = true
metric_name = "BlockExploitPaths"
}
}
Extra protection for authentication:
rule {
name = "LoginRateLimit"
priority = 6
action {
block {}
}
statement {
rate_based_statement {
limit = 100
aggregate_key_type = "IP"
scope_down_statement {
byte_match_statement {
field_to_match {
uri_path {}
}
positional_constraint = "STARTS_WITH"
search_string = "/api/auth"
text_transformation {
priority = 0
type = "LOWERCASE"
}
}
}
}
}
visibility_config {
sampled_requests_enabled = true
cloudwatch_metrics_enabled = true
metric_name = "LoginRateLimit"
}
}
Always test rules in count mode first:
rule_action_override {
action_to_use {
count {}
}
name = "GenericRFI_BODY"
}
Review CloudWatch logs for false positives before switching to block.
If an API endpoint legitimately contains patterns that trigger rules:
rule {
name = "ExcludeWebhooksFromSQLi"
priority = 9 # Higher priority than SQLi rules
action {
allow {}
}
statement {
byte_match_statement {
field_to_match {
uri_path {}
}
positional_constraint = "STARTS_WITH"
search_string = "/api/webhooks/"
text_transformation {
priority = 0
type = "LOWERCASE"
}
}
}
visibility_config {
sampled_requests_enabled = true
cloudwatch_metrics_enabled = true
metric_name = "WebhookExclusion"
}
}
Enable full logging for debugging:
resource "aws_wafv2_web_acl_logging_configuration" "main" {
log_destination_configs = [aws_cloudwatch_log_group.waf.arn]
resource_arn = aws_wafv2_web_acl.main.arn
logging_filter {
default_behavior = "DROP"
filter {
behavior = "KEEP"
condition {
action_condition {
action = "BLOCK"
}
}
requirement = "MEETS_ANY"
}
}
}
Use curl to test specific patterns:
# Test SQL injection rule
curl -X POST "https://api.example.com/search" \
-d "query='; DROP TABLE users;--"
# Test XSS rule
curl -X POST "https://api.example.com/comment" \
-d "text=<script>alert('xss')</script>"
# Test rate limiting
for i in {1..100}; do
curl -s "https://api.example.com/api/endpoint" &
done
| Priority | Rule | Action |
|---|---|---|
| 1 | Geo-blocking | Block |
| 5 | Global rate limit | Block |
| 6 | Login rate limit | Block |
| 9 | Path exclusions | Allow |
| 10 | Core rule set | Block/Count |
| 15 | Custom exploit paths | Block |
| 20 | SQLi rules | Block |
| 30 | Known bad inputs | Block |
WAF is not a set-and-forget solution. Monitor, tune, and iterate based on your actual traffic patterns and attack attempts.