Fix Nginx 502 Bad Gateway 'client intended to send too large body' in Reverse Proxy Configs
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5–15 mins
TL;DR
- What broke: Nginx is enforcing a default or explicitly low
client_max_body_size(default: 1MB) and returning a413 Request Entity Too Largebefore the upstream even sees the request — which your load balancer or upstream proxy then surfaces as a 502 Bad Gateway. - How to fix it: Raise
client_max_body_sizein the correcthttp {},server {}, orlocation {}block, and alignproxy_request_buffering,proxy_read_timeout, and your upstream app server's own body limits. - Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your
nginx.confand get a corrected diff without sending your config to a third-party server.
The Incident (What Does the Error Mean?)
You will see this in /var/log/nginx/error.log:
2024/01/15 03:42:17 [error] 1234#1234: *5678 client intended to send too large body: 8388608 bytes,
client: 203.0.113.45, server: api.example.com, request: "POST /api/v2/upload HTTP/1.1",
host: "api.example.com"
And the client receives:
HTTP/1.1 502 Bad Gateway
or in some proxy chain configurations:
HTTP/1.1 413 Request Entity Too Large
Immediate consequence: Nginx has already closed the connection. The upstream (Node.js, Gunicorn, uWSGI, a microservice) never received the request. Any retry logic in the client will hammer Nginx again with the same result. File uploads, large JSON payloads, multipart form data, and webhook bodies with large event arrays are the most common triggers.
The Attack Vector / Blast Radius
This is a misconfiguration-driven availability failure, not a security exploit — but the blast radius is significant:
Silent 502 storm: Depending on your upstream health check config, repeated 502s from Nginx can cause your load balancer (ALB, HAProxy, Cloudflare) to mark the backend as unhealthy and pull it from rotation. A single large-payload endpoint can take down routing for the entire service.
Asymmetric limit mismatch: The most dangerous variant is when
client_max_body_sizeis set in thehttp {}block but overridden (or forgotten) in a nestedlocation {}block. Developers raise the global limit, test it, and ship — then a newlocation /api/internal/block added six months later inherits the default 1MB cap. Production breaks only for that route.Proxy chain amplification: In a multi-layer proxy setup (CDN → Nginx → App Server), each layer has its own body size limit. If Nginx passes the request but your uWSGI or Gunicorn has a lower
--limit-request-lineorMAX_CONTENT_LENGTH, you get a 502 from Nginx wrapping a 413 from the app — doubly confusing to debug.DoS surface (inverse): Setting
client_max_body_size 0(unlimited) to "fix" this is a denial-of-service vector. An attacker can stream arbitrarily large bodies, exhausting disk buffers under/var/cache/nginxand RAM. Never set to0on a public-facing endpoint.
How to Fix It (The Solution)
Basic Fix
Locate the relevant server {} or location {} block in your nginx.conf or site config under /etc/nginx/conf.d/ or /etc/nginx/sites-enabled/.
http {
# ...
- client_max_body_size 1m;
+ client_max_body_size 50m;
server {
listen 80;
server_name api.example.com;
location /api/v2/upload {
proxy_pass http://upstream_app;
+ client_max_body_size 100m;
}
}
}
Rule: The most-specific block wins. Set a conservative global default in
http {}and override upward only in the specificlocation {}blocks that need it.
Enterprise Best Practice (Full Hardened Config)
Raising client_max_body_size alone is not enough. You must align buffer sizes, timeouts, and upstream limits simultaneously. A body that fits within the size limit can still fail if Nginx times out while buffering it.
http {
- client_max_body_size 1m;
+ client_max_body_size 10m; # Conservative global default
+ client_body_buffer_size 128k; # Buffer in RAM before spilling to disk
+ client_body_temp_path /var/cache/nginx/client_temp 1 2;
+ client_body_timeout 60s; # Time to receive the full body, not just first byte
server {
listen 443 ssl http2;
server_name api.example.com;
# Standard API routes — keep global 10m limit
location /api/ {
proxy_pass http://upstream_app;
proxy_http_version 1.1;
proxy_set_header Connection "";
- # Missing buffer/timeout directives caused 502 under load
+ proxy_request_buffering on; # Buffer full body before sending upstream
+ proxy_read_timeout 120s; # Upstream has 120s to respond after body delivered
+ proxy_send_timeout 60s;
+ proxy_connect_timeout 10s;
}
# File upload endpoint — explicitly raised limit
location /api/v2/upload {
proxy_pass http://upstream_app;
proxy_http_version 1.1;
- # No size override — was inheriting 1m global default
+ client_max_body_size 200m;
+ client_body_buffer_size 1m;
+ proxy_request_buffering off; # Stream directly for large files; avoids disk I/O
+ proxy_read_timeout 300s;
}
# Webhook receiver — small bodies only, hard cap for security
location /webhooks/ {
proxy_pass http://upstream_app;
+ client_max_body_size 512k; # Webhooks should never exceed 512KB
+ proxy_request_buffering on;
}
}
}
Critical alignment checklist — all three must agree:
| Layer | Directive | Must Be ≥ |
|---|---|---|
| Nginx | client_max_body_size |
Your max expected payload |
| Gunicorn | --limit-request-line / --limit-request-field-size |
Same |
| uWSGI | buffer-size |
Same |
| Node.js (express) | express.json({ limit: '...' }) |
Same |
| Flask | MAX_CONTENT_LENGTH |
Same |
| Django | DATA_UPLOAD_MAX_MEMORY_SIZE |
Same |
After editing, always validate before reload:
nginx -t && systemctl reload nginx
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
This class of misconfiguration is 100% preventable with static analysis in your pipeline.
1. Nginx Config Linting with gixy
gixy is a static analysis tool for Nginx configs. Add it to your CI:
# .github/workflows/nginx-lint.yml
- name: Lint Nginx config with gixy
run: |
pip install gixy
gixy /etc/nginx/nginx.conf
gixy will flag missing client_max_body_size overrides and dangerous proxy_pass configurations.
2. Checkov for IaC (Terraform/Helm-deployed Nginx)
If you deploy Nginx via Helm or Terraform-managed ConfigMaps:
checkov -d ./helm/nginx --framework kubernetes
Write a custom Checkov check:
# checkov/custom_checks/nginx_body_size.py
from checkov.common.models.enums import CheckResult, CheckCategories
from checkov.kubernetes.checks.resource.base_spec_check import BaseK8Check
class NginxBodySizeCheck(BaseK8Check):
def __init__(self):
name = "Ensure nginx ConfigMap sets client_max_body_size explicitly"
id = "CKV_CUSTOM_NGINX_001"
super().__init__(name=name, id=id,
categories=[CheckCategories.GENERAL_SECURITY],
supported_entities=['ConfigMap'])
def scan_spec_conf(self, conf):
data = conf.get('data', {})
nginx_conf = data.get('nginx.conf', '')
if 'client_max_body_size' in nginx_conf:
return CheckResult.PASSED
return CheckResult.FAILED
3. Integration Test with curl in Your Pipeline
Don't wait for production to discover the limit. Add a smoke test:
#!/bin/bash
# ci/test_upload_limit.sh
# Generate a 50MB test file and assert Nginx accepts it
dd if=/dev/urandom of=/tmp/test_payload.bin bs=1M count=50 2>/dev/null
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST \
-H "Content-Type: application/octet-stream" \
--data-binary @/tmp/test_payload.bin \
https://staging-api.example.com/api/v2/upload)
if [ "$HTTP_STATUS" == "413" ] || [ "$HTTP_STATUS" == "502" ]; then
echo "FAIL: Upload rejected with $HTTP_STATUS — check client_max_body_size"
exit 1
fi
echo "PASS: Upload accepted with $HTTP_STATUS"
4. OPA/Conftest Policy for Nginx ConfigMaps
# policies/nginx_body_size.rego
package nginx
deny[msg] {
input.kind == "ConfigMap"
nginx_conf := input.data["nginx.conf"]
not contains(nginx_conf, "client_max_body_size")
msg := sprintf("ConfigMap '%v' nginx.conf is missing client_max_body_size directive", [input.metadata.name])
}
Run in CI:
conftest test ./k8s/nginx-configmap.yaml --policy ./policies/