AWS WAF SQL Injection Rules: field_to_match, Text Transformations, and Rule Groups

2 min readCloud Infrastructure

AWS WAF SQLi rules inspect request fields and block injection attempts before they reach your application. The field_to_match choice determines coverage: query string catches URL parameter attacks, body catches POST payload attacks. Text transformations decode obfuscated payloads before inspection. Missing either leaves gaps that attackers exploit.

awswafsecurity

How WAF SQLi inspection works

AWS WAF evaluates each request against rule statements in priority order. An sqli_match_statement extracts a specific request field, applies text transformations to normalize encoding, then checks the result against SQLi attack patterns.

The pipeline:

  1. Extract field (field_to_match)
  2. Apply transformations (URL decode, HTML entity decode, etc.)
  3. Match against SQLi pattern database
  4. Block or allow based on rule action

Text transformations run in priority order before pattern matching — missing a transformation lets encoded payloads bypass the rule

ConceptAWS WAF

Attackers encode SQL injection payloads to evade WAF inspection: %27 instead of ', <script> instead of the raw string, base64-encoded payloads, etc. Text transformations normalize these encodings before the SQLi pattern matching runs. If you inspect body but only apply URL_DECODE, an attacker using HTML entity encoding bypasses your rule.

Prerequisites

  • HTTP request structure
  • URL encoding basics
  • SQL injection attack patterns

Key Points

  • Transformations apply in priority order (lowest number first). Order matters when chaining decode steps.
  • URL_DECODE converts %27 → '. HTML_ENTITY_DECODE converts &#x27; → '.
  • LOWERCASE enables case-insensitive matching (SELECT vs select vs SeLeCt).
  • Applying multiple transformations increases CPU cost — WAF charges per WCU (Web ACL Capacity Unit).

Core SQLi rule: query string and body

resource "aws_wafv2_web_acl" "main" {
  name        = "main-waf"
  scope       = "CLOUDFRONT"   # or "REGIONAL" for ALB/API Gateway
  description = "WAF with SQL injection protection"

  default_action {
    allow {}
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "main-waf"
    sampled_requests_enabled   = true
  }

  # Rule 1: Inspect query string (GET parameters)
  rule {
    name     = "SQLi-QueryString"
    priority = 10

    action {
      block {}
    }

    statement {
      sqli_match_statement {
        field_to_match {
          all_query_arguments {}
        }

        text_transformation {
          priority = 0
          type     = "URL_DECODE"
        }
        text_transformation {
          priority = 1
          type     = "HTML_ENTITY_DECODE"
        }
        text_transformation {
          priority = 2
          type     = "LOWERCASE"
        }
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "SQLi-QueryString"
      sampled_requests_enabled   = true
    }
  }

  # Rule 2: Inspect request body (POST payloads)
  rule {
    name     = "SQLi-Body"
    priority = 20

    action {
      block {}
    }

    statement {
      sqli_match_statement {
        field_to_match {
          body {
            oversize_handling = "MATCH"   # treat oversized body as a match (block)
          }
        }

        text_transformation {
          priority = 0
          type     = "URL_DECODE"
        }
        text_transformation {
          priority = 1
          type     = "HTML_ENTITY_DECODE"
        }
        text_transformation {
          priority = 2
          type     = "LOWERCASE"
        }
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "SQLi-Body"
      sampled_requests_enabled   = true
    }
  }
}

oversize_handling = "MATCH" causes WAF to treat request bodies larger than 8KB as a match (and block them). CONTINUE inspects only the first 8KB. NO_MATCH skips inspection for oversized bodies. For APIs expecting large uploads, use CONTINUE and handle large bodies separately.

field_to_match options

| Field | What it covers | When to use | |---|---|---| | all_query_arguments | All URL parameters | Default — covers GET requests and URL parameters | | body | POST/PUT request body | Forms, JSON payloads, REST API bodies | | single_header | A specific header value | APIs using custom headers (e.g., X-User-Input) | | headers | All request headers | Broad header coverage | | uri_path | URL path (e.g., /user/123) | Path parameters in REST APIs | | json_body | Parsed JSON body | JSON APIs — inspects values inside JSON structure | | cookies | Cookie values | Session-based injection attempts |

For REST APIs that accept user input in JSON bodies, use json_body instead of body:

statement {
  sqli_match_statement {
    field_to_match {
      json_body {
        match_pattern {
          all {}   # inspect all JSON values
        }
        match_scope      = "VALUE"   # or "KEY" to inspect JSON keys
        oversize_handling = "MATCH"
      }
    }

    text_transformation {
      priority = 0
      type     = "URL_DECODE"
    }
    text_transformation {
      priority = 1
      type     = "HTML_ENTITY_DECODE"
    }
  }
}

json_body with match_scope = "VALUE" inspects the values within the JSON structure — useful when an API receives {"username": "' OR 1=1--", "password": "..."}.

📝AWS Managed Rule Groups: SQLi protection without custom rules

AWS maintains managed rule groups that include SQLi protection. The AWSManagedRulesSQLiRuleSet covers common injection patterns across all request fields:

rule {
  name     = "AWS-SQLi-ManagedRules"
  priority = 1

  override_action {
    none {}   # use the rule group's default actions
  }

  statement {
    managed_rule_group_statement {
      name        = "AWSManagedRulesSQLiRuleSet"
      vendor_name = "AWS"

      # Optional: override specific rules to count instead of block during testing
      rule_action_override {
        name = "SQLi_BODY"
        action_to_use {
          count {}
        }
      }
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "AWS-SQLi-ManagedRules"
    sampled_requests_enabled   = true
  }
}

Start with count action overrides during rollout — inspect CloudWatch metrics and sampled requests to verify no legitimate traffic is flagged before switching to block. AWS updates managed rule groups as new attack patterns emerge.

AWSManagedRulesCommonRuleSet also contains SQLi rules alongside XSS, path traversal, and other OWASP Top 10 coverage.

Attaching WAF to CloudFront or ALB

# Attach to CloudFront (scope must be CLOUDFRONT, deployed in us-east-1)
resource "aws_cloudfront_distribution" "main" {
  web_acl_id = aws_wafv2_web_acl.main.arn
  # ... rest of CloudFront config
}

# Attach to ALB (scope must be REGIONAL, same region as ALB)
resource "aws_wafv2_web_acl_association" "alb" {
  resource_arn = aws_lb.main.arn
  web_acl_arn  = aws_wafv2_web_acl.main.arn
}

# Attach to API Gateway stage
resource "aws_wafv2_web_acl_association" "api_gateway" {
  resource_arn = aws_api_gateway_stage.main.arn
  web_acl_arn  = aws_wafv2_web_acl.main.arn
}

CloudFront WAF must be in us-east-1 with scope = "CLOUDFRONT". Regional WAFs (ALB, API Gateway, AppSync) use scope = "REGIONAL" and must be in the same region as the protected resource.

Your WAF has a SQLi rule inspecting all_query_arguments with URL_DECODE transformation. An attacker sends a request with a query parameter value of SELECT%2520FROM where %25 is a double-encoded percent sign. The rule doesn't block it. Why?

hard

A single URL_DECODE pass converts %2520 to %20 (a space), not to a space character directly. The result after one decode is 'SELECT%20FROM' which looks like a URL-encoded space, not a raw SQL statement.

  • Aall_query_arguments doesn't support URL_DECODE — use single_query_argument instead
    Incorrect.all_query_arguments supports all text transformations including URL_DECODE. The field type isn't the issue.
  • BThe payload is double-encoded — a single URL_DECODE pass decodes %2520 to %20, not to a space. The SQLi pattern doesn't match %20 between SELECT and FROM. A second URL_DECODE is needed to fully normalize the payload
    Correct!%25 URL-decodes to %, and %20 is a space. So %2520 decodes to %20 in one pass. The word boundary between SELECT and FROM is preserved as literal %20 characters, which the SQLi pattern may not match. Add a second URL_DECODE transformation (priority=1) after the first (priority=0) to handle double-encoding. WAF applies transformations in priority order — two URL_DECODE passes fully normalize double-encoded payloads.
  • CWAF doesn't inspect URL-encoded values in query parameters — only decoded values
    Incorrect.The text_transformation step exists precisely to decode encoded values before inspection. WAF does decode and inspect.
  • DThe rule needs COMPRESS_WHITE_SPACE transformation to normalize the space between SELECT and FROM
    Incorrect.COMPRESS_WHITE_SPACE normalizes multiple spaces to single spaces. The problem here is double-encoding preventing the space from appearing at all after one decode pass.

Hint:How many URL_DECODE passes are needed to fully normalize %2520?