Cloud

Azure WAF: diagnose a file upload before raising size limits

A production runbook for separating file upload limits, request body size, inspection depth, WAF rules and backend rejection before a reversible change.

05 Sept 2026 azurewafapplication-gatewayfile-uploadrequest-bodykqlsecurityobservabilityrunbookrollbackproduction

A new document submission flow goes live behind Azure Application Gateway WAF. Files of a few megabytes succeed, while larger ones receive a 403. The API records no request, so one team wants to raise the upload limit immediately and another suggests disabling request body inspection.

Those changes address different controls. WAF can enforce a file upload limit, a request body limit, an inspection depth, or a managed rule triggered by the content. Application Gateway and the backend also retain their own limits. This runbook identifies the rejecting layer, allows only the documented business volume, and keeps a testable rollback.

Freeze the upload contract

Start with the flow that must work. A maximum size is not an isolated setting: it depends on the HTTP format, document type, processing time, and backend capacity.

text upload-incident-scope.txt
Incident: inc-20260905-001
Entry point: Application Gateway v2 with WAF policy
Hostname: documents.example.com
Route: POST /api/documents
Expected format: multipart/form-data with a named file
Expected business maximum: confirm with the product owner
Symptom: small files pass, larger files receive 403
Latest change: application, WAF policy, ruleset, gateway, or backend

Preserve before action
UTC time and transaction ID for one failed request
exact request body and file size
Content-Type and filename presence
response observed by the client
WAF, gateway access, and application logs
current policy values and ruleset version

Do not do during collection
disable request body inspection globally
move the whole policy to Detection without a bounded window
raise several limits at once
replay a sensitive document as a test fixture

WAF treats content as a file upload when it is a multipart/form-data part with a filename header. A large JSON body or a binary sent under another Content-Type belongs to request body handling instead. Prove that distinction from the client or a controlled synthetic capture.

Read four controls without merging them

A current Application Gateway WAF policy exposes separate controls for body inspection, body size enforcement, inspection depth, and file upload enforcement. Record the policy mode and managed ruleset at the same time.

bash 01-waf-policy-settings.sh
set -eu

RG="rg-edge-prod"
POLICY="waf-documents-prod"

az network application-gateway waf-policy show --resource-group "$RG" --name "$POLICY" --query '{id:id,provisioningState:provisioningState,policySettings:policySettings,managedRules:managedRules.managedRuleSets}' --output json

requestBodyCheck determines whether body content is inspected. requestBodyEnforcement and maxRequestBodySizeInKb govern rejection of oversized request bodies. requestBodyInspectLimitInKB controls how deeply rules inspect a body. fileUploadEnforcement and fileUploadLimitInMb apply to content recognized as file uploads.

With CRS 3.2 or newer, these controls can be managed independently. They are not interchangeable. Raising the accepted size while leaving inspection depth lower creates content that is accepted but not inspected. Disabling WAF enforcement also leaves gateway and application limits in place.

Prove which layer rejects the request

A client-side 403 is not yet proof of a WAF block. Correlate firewall and access logs over the same window, URI, and transaction ID when available.

kusto 02-waf-upload-correlation.kql
let Window = 2h;
let Host = "documents.example.com";
let Path = "/api/documents";
AzureDiagnostics
| where TimeGenerated > ago(Window)
| where Category in ("ApplicationGatewayFirewallLog", "ApplicationGatewayAccessLog")
| extend host = tostring(host_s), uri = tostring(requestUri_s)
| extend action = tostring(action_s), ruleId = tostring(ruleId_s)
| extend message = tostring(message_s), detail = tostring(details_message_s)
| extend transactionId = tostring(transactionId_g)
| extend status = toint(httpStatus_d)
| where host == Host and uri startswith Path
| project TimeGenerated, Category, transactionId, action, status, ruleId, message, detail
| order by TimeGenerated asc

Classify the result before changing configuration:

  • a WAF block tied to body size or parsing points to policy settings;
  • a managed rule matching a form value is a rule or exclusion issue, not a size issue;
  • a forwarded gateway request ending in 413, timeout, or 5xx without a WAF block points to the gateway or backend;
  • missing evidence means diagnostics and correlation must be repaired first.

Firewall logs can contain fragments of submitted data. Keep projections narrow, retain configured log scrubbing, and use synthetic files without business data during diagnosis.

Build a canary matrix

One failed file cannot locate the boundary. Build a matrix that changes one dimension at a time, then run it in a representative environment or a bounded production window.

yaml upload-canary-matrix.yml
canaries:
common:
  route: POST /api/documents
  identity: test principal with normal upload role
  content: synthetic non-sensitive bytes
  correlation_header: x-test-correlation-id
cases:
  - name: multipart-small
    content_type: multipart/form-data
    file_size: below_business_limit
    expected: accepted_and_scanned
  - name: multipart-near-boundary
    content_type: multipart/form-data
    file_size: near_configured_file_limit
    expected: explicit_boundary_result
  - name: multipart-over-boundary
    content_type: multipart/form-data
    file_size: above_configured_file_limit
    expected: controlled_rejection
  - name: json-same-body-size
    content_type: application/json
    body_size: same_as_near_boundary_case
    expected: classified_by_request_body_policy
collect:
  - client_status_and_duration
  - content_length_and_content_type
  - waf_transaction_and_rule
  - gateway_status_and_latency
  - backend_request_id_or_absence
  - cpu_memory_and_processing_time

Place cases just below, near, and above the configured boundary. The incident is not the right time to discover a theoretical maximum. Find the smallest range that proves the business requirement and each layer’s behavior.

Choose a bounded change

The evidence determines the setting. If legitimate files exceed the WAF file limit but remain inside the application contract, raise only fileUploadLimitInMb to a justified value. If a non-multipart body is affected, work on request body controls. If a managed rule blocks one field, return to false-positive analysis and a targeted exclusion.

text upload-change-decision.txt
Raise one limit
HTTP format and rejecting layer are proven
business maximum is documented
backend can receive, scan, process, and store the volume
inspection depth matches the threat model
abuse controls and upload concurrency are validated

Block the change
business owner cannot bound the size
managed rule is triggered by content
backend limit or timeout occurs before processing
logs are missing or transactions cannot be correlated
storage, memory, or malware scanning capacity is unknown

Temporary exception
route, method, identity, and window are tightly bounded
compensating application control is active
automatic expiry and owner are recorded
previous policy is exported before action

Do not disable request body inspection to solve a simple size mismatch. That removes body-based rule evaluation while allowing more data through the route. When an exception is unavoidable, keep it local to the flow and shorter-lived than the corrective work.

Validate and prepare rollback

Export the full policy state before changing it. After the update, rerun the matrix. Confirm that the maximum business file succeeds, a file above the contract is rejected, and a synthetic payload intended for a test rule is still inspected. Watch backend latency, memory, errors, and saturation as well. Moving rejection from WAF to the application is not a successful change.

Rollback restores the previous fileUploadLimitInMb, maxRequestBodySizeInKb, requestBodyInspectLimitInKB, both enforcement flags, requestBodyCheck, and policy mode together. Keep the ruleset and exclusions unchanged during this test. If errors rise or the expected part of the body is no longer inspected, restore that state and disable large uploads at the product layer until the path is corrected.

Conclusion

A 403 on a large file does not justify a more permissive WAF policy. First separate multipart file handling, request body size, inspection depth, managed rules, and backend limits. The canary matrix turns that ambiguity into a measurable decision.

The runbook ends with one explicit outcome: raise one limit to the proven need, correct a targeted rule, strengthen the backend, keep the rejection, or restore the previous policy. The upload returns to service only when the team can state what size it accepts, how much it inspects, and how it will roll back.