Why does AWS WAF block legitimate POST requests with “403 Forbidden” after adding a size constraint on the body for JSON payloads?

descricao: “Learn why AWS WAF returns 403 on valid JSON POSTs after adding a size constraint, and how to fix false positives.”

I was knee‑deep in a production rollout for a new API endpoint when our monitoring started screaming 403 Forbidden on perfectly valid POSTs. The only change we’d made a few minutes earlier was a SizeConstraint rule in AWS WAF to limit the request body to 2 KB for JSON payloads. Suddenly, every client that sent a normal JSON object—sometimes just a few bytes—was being rejected. I had to dig into the WAF logs, replay traffic, and eventually rewrite our rule set to stop the false positives.

In this post I’ll walk you through the exact conditions that cause AWS WAF to mistake a legitimate JSON body for an oversized request, why the default inspection behavior matters, and—most importantly—how to configure the rule so it protects you without breaking your API.


What does the AWS WAF SizeConstraint actually measure?

When you add a SizeConstraintStatement to a WAF rule, you tell the service to look at a single part of the request (the URI, query string, headers, cookies, or the entire body) and compare its byte length to the limit you set. The key details are:

  1. Byte counting is raw, not logical. AWS counts every byte that arrives over the wire, including whitespace, line‑breaks, and even the UTF‑8 byte‑order‑mark if present.
  2. The body is inspected before any transformation (like JSON parsing or URL decoding) unless you explicitly add a Transformation.
  3. Multipart or streamed bodies are treated as a single blob; if you enable inspect all request body the whole payload must fit the limit.

Because of those three points, a JSON payload that looks tiny in a text editor can exceed the limit once it’s serialized and transmitted.

Pro Tip: Enable AWSManagedRulesCommonRuleSet alongside your custom size rule. The managed rule set logs the exact byte count it used, which makes debugging far easier.


Why did my perfectly valid JSON exceed the 2 KB limit?

1. Hidden characters and pretty‑printing

Our developers love pretty‑printed JSON for readability during local testing. A payload that looks like this:

{
  "userId": "12345",
  "action": "login",
  "metadata": {
    "ip": "192.168.0.1",
    "userAgent": "Mozilla/5.0"
  }
}

contains line‑feeds (\n) and spaces that add up quickly. When you run JSON.stringify with the default space argument set to 2, the string length can jump from ~150 bytes to over 300 bytes. Multiply that by a few nested objects and you’re easily over 2 KB.

2. Base64‑encoded binary blobs inside JSON

One of our endpoints accepted an image as a Base64 string inside the JSON body. A 1 KB image becomes roughly a 1.33 KB Base64 string, and with surrounding JSON syntax the total can cross the threshold.

3. Compression vs. raw size

AWS WAF does not look at Content‑Encoding: gzip. The service measures the uncompressed payload after it’s been decompressed by the load balancer. If you send gzipped JSON, the underlying size can be larger than the compressed bytes you see in your client logs.

4. The “body inspection” flag defaults to ALL

When you create a WebACL via the console and select Inspect body, the default is to inspect all body bytes, regardless of the Content‑Type. If you only care about application/json, you need to add a ByteMatchStatement that first checks the Content‑Type header before the size rule runs.


How to reproduce the false positive locally

  1. Create a simple WAF WebACL with a single rule:
   {
     "Name": "BlockLargeJSON",
     "Priority": 1,
     "Statement": {
       "SizeConstraintStatement": {
         "FieldToMatch": { "Body": {} },
         "ComparisonOperator": "GT",
         "Size": 2048,
         "TextTransformations": [{ "Priority": 0, "Type": "NONE" }]
       }
     },
     "Action": { "Block": {} },
     "VisibilityConfig": {
       "SampledRequestsEnabled": true,
       "CloudWatchMetricsEnabled": true,
       "MetricName": "BlockLargeJSON"
     }
   }
  1. Deploy a test API Gateway (or ALB) behind the WebACL.
  2. Send a POST with a 1.9 KB pretty‑printed JSON payload using curl:
   curl -X POST https://myapi.example.com/endpoint \
        -H "Content-Type: application/json" \
        -d @payload-pretty.json -v
  1. Observe the 403 Forbidden response and check the WAF logs – you’ll see the size field reported as 2056 bytes.

How to fix the rule without losing protection

Step 1: Narrow the field to match

Instead of applying the size constraint to any body, first verify the request is JSON. Use a ByteMatchStatement on the Content‑Type header:

{
  "Name": "JSONOnlySizeCheck",
  "Priority": 10,
  "Statement": {
    "AndStatement": {
      "Statements": [
        {
          "ByteMatchStatement": {
            "SearchString": "application/json",
            "FieldToMatch": { "SingleHeader": { "Name": "content-type" } },
            "TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
            "PositionalConstraint": "CONTAINS"
          }
        },
        {
          "SizeConstraintStatement": {
            "FieldToMatch": { "Body": {} },
            "ComparisonOperator": "GT",
            "Size": 2048,
            "TextTransformations": [{ "Priority": 0, "Type": "NONE" }]
          }
        }
      ]
    }
  },
  "Action": { "Block": {} },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "JSONOnlySizeCheck"
  }
}

Now the size rule only runs when the header indicates JSON, preventing non‑JSON POSTs (like form‑encoded) from being blocked incorrectly.

Step 2: Trim whitespace before measurement

Add a TextTransformation of type COMPRESS_WHITE_SPACE. This collapses multiple spaces, line‑feeds, and tabs into a single space before the size check:

"TextTransformations": [
  { "Priority": 0, "Type": "COMPRESS_WHITE_SPACE" }
]

The transformation runs after the ByteMatchStatement but before the SizeConstraintStatement, ensuring the byte count reflects the logical size of the JSON rather than its pretty‑printed form.

Step 3: Account for Base64 payloads

If you must accept Base64 strings, create a separate rule with a higher threshold (e.g., 4 KB) and tag the request with a custom header (X-Contains-Base64: true). Then use an OrStatement to allow either the normal JSON rule or the Base64‑specific rule.

Step 4: Enable logging and metric alerts

Turn on WAF logging to an S3 bucket or CloudWatch Log Group. Include the requestBody field (sampledRequestsEnabled: true). With a metric filter you can alert when the size field approaches the limit, giving you a heads‑up before users hit 403.

Pro Tip: Set the SampledRequestsEnabled flag to true only on the rules you’re actively debugging. Excessive sampling can increase costs.


Edge cases you might run into

ScenarioWhy it trips the ruleMitigation
Chunked Transfer EncodingThe body is streamed; WAF buffers the entire payload before measuring, which can cause timeouts for very large streams.Enforce a Maximum Body Size on the load balancer (e.g., max-body-size on ALB) to reject early.
Multipart/form‑data with JSON partThe rule sees the entire multipart payload, including boundary strings, inflating size.Use a FieldToMatch of JSONBody (available in newer WAF versions) or separate the JSON part into its own endpoint.
Compressed (gzip) payloadsWAF decompresses before measuring, so the compressed size you think you’re sending is irrelevant.Keep your size limit generous enough for the decompressed payload, or reject compressed bodies altogether with a ByteMatchStatement on Accept-Encoding.
Nested arrays with many elementsThe logical JSON depth isn’t an issue, but each element adds bytes; developers may think “only 10 items, should be fine.”Add a RateBasedStatement that monitors request count per IP; large JSONs often correlate with abuse patterns.

FAQ

How can I see the exact byte count AWS WAF used for a blocked request?

Check the WAF log entry under terminatingRuleIdsize. If you enabled SampledRequestsEnabled, the log includes a requestBody field with size in bytes.

Does AWS WAF count UTF‑8 multi‑byte characters as multiple bytes?

Yes. Each code point can be 1–4 bytes. A string with many non‑ASCII characters will inflate the size dramatically.

Can I apply a size constraint only to specific API paths?

Absolutely. Wrap the SizeConstraintStatement in a ScopeDownStatement that includes a ByteMatchStatement on the URI field (e.g., startsWith /api/v1/secure).

What happens if the request body exceeds the limit before WAF can inspect it?

The underlying ALB or API Gateway will return a 413 Payload Too Large before WAF sees the request. WAF only acts on requests that successfully reach it.

Is there a way to automatically strip whitespace from incoming JSON before WAF checks size?

WAF itself cannot modify the payload, but you can add a CloudFront Lambda@Edge function that normalizes JSON (removes whitespace) and then forwards to the origin. The size rule will then see the compacted payload.


Conclusion

The short answer to the title question is: AWS WAF blocks legitimate POSTs because it measures raw byte length, including whitespace, Base64 expansion, and decompressed content, and because the rule was applied too broadly. By narrowing the field match to JSON requests, adding a whitespace‑compressing transformation, and handling special cases like Base64 blobs or multipart forms, you keep the protection while eliminating false positives.

In production I learned that the real size of a JSON payload is rarely the number you eyeball in a text editor. Always inspect the WAF logs, enable sampling during debugging, and think about how your clients serialize data. With those safeguards in place, your API can stay both secure and usable.


Stay tuned for more deep‑dive security posts—follow SpiritCode for the next engineering story.