SECURE_NODE // ATHENS_GR
← BACK TO RESEARCH Technique · API Security · BOLA

BOLA Behind a gzip-Compressed Request Body

17 JUL 2026| ≈ 6 MIN READ| BOLAgzipAPI1-2023RequestInspectionAuthorization
SANITIZED ▸ Methodology only. No client name, real host, real data, or live finding. All requests reproduced against a synthetic lab endpoint. This is a technique writeup, not an account of a specific engagement.

A POST endpoint with Content-Encoding: gzip renders as a block of unreadable bytes in Burp's inspector. Testers see it and move on. There's nothing to click, nothing that looks like a parameter. That instinct is exactly wrong. The compressed body is where the authorization parameter was hiding, and it took one decompression step to turn an "untestable" endpoint into a confirmed BOLA.

SEVERITY
High
authorization bypass
OWASP
API1:2023
broken object level auth
VECTOR
Compressed body
parameter hidden from Burp

The request that looks untestable

Any POST endpoint can carry its parameters inside a compressed body. On the wire it looks unremarkable: standard headers, an ordinary session cookie, and a body that Burp renders as binary noise. Nothing to click, nothing that reads as a parameter:

POST /api/report HTTP/1.1
Host: api.acme-lab.test
Cookie: session=<session>
Content-Type: application/json
Content-Encoding: gzip
Content-Length: 48

(body: raw bytes, gzip stream, not rendered by Burp)

That's exactly where most testers stop. It's also exactly where the object-reference parameter is hiding.

What the body actually is, byte for byte

Don't take "it's binary" on faith. The body's first two bytes are the gzip magic number 1f 8b, and everything after them is DEFLATE-compressed JSON:

00000000  1f8b 0800 0000 0000 02ff ab56 ca4c 51b2   ...........V.LQ.
00000010  5232 3450 d251 2a2e 492c 292d 06f2 824b   R24P.Q*.I,)-...K
00000020  9393 538b 8b95 6a01 cd02 322a 1e00 0000   ..S...j...2*....

It's not obfuscation or encryption. It's standard DEFLATE-compressed JSON. Reversing it takes one library call.

Steps 01–07: recovering the plaintext and confirming BOLA

STEP 01 · EXPORT THE REQUEST AS BASE64

In Burp: right-click the request → Save item(s) (or Copy as Base64). This captures the exact bytes that went on the wire, headers and compressed body, with no rendering transformation applied.

STEP 02 · DECODE BASE64 TO RAW BYTES, SPLIT AT HEADER BOUNDARY

HTTP always separates headers from body with \r\n\r\n. Everything after that boundary is the still-compressed body.

import base64

data = base64.b64decode(open("request_b64.txt").read())
idx  = data.find(b"\r\n\r\n")
headers, body = data[:idx], data[idx+4:]
STEP 03 · VERIFY THE MAGIC BYTES

Don't trust the Content-Encoding header alone; check the first two bytes of the body against the gzip signature before assuming the encoding.

Offset Bytes Meaning
0x001f 8bgzip magic number
0x0208compression method: DEFLATE
0x0300flags
STEP 04 · INFLATE

gzip.decompress() handles the standard case. If the stream was corrupted in transit and throws a CRC error, fall back to zlib.decompressobj(-zlib.MAX_WBITS), which inflates the raw DEFLATE stream without checking the trailer checksum.

import gzip, json

result = gzip.decompress(body)
obj    = json.loads(result)
print(json.dumps(obj, indent=2))
STEP 05 · THE HIDDEN FIELD IS NOW PLAIN TEXT

The decompressed output reveals an id parameter that was never visible in the URL path, cookies, or Burp's rendered view. It existed only inside the compressed body.

{
 "id": "10",
 "status": "Success"
}

Skip decompression and you'd report this endpoint as "not testable," which is the exact opposite of the truth.

STEP 06 · EDIT AND REPLAY

Change "id": "10" to "11", drop the Content-Encoding: gzip header (send the edited body as plain JSON so header and payload agree), and replay in Burp Repeater with the original session cookie unchanged.

STEP 07 · CONFIRMED BOLA

The server returns another user's record. The path, session cookie, and all other context stayed identical; only the compressed body's object reference changed.

The BOLA test, before and after

id 10 · own session
200 OK, returns the caller's own record.
id 11 · same session
200 OK, returns a different user's record.

Same session, same path, same everything except the object reference inside the compressed body. Swapping id returned a record the caller had no authorization to see, with no server-side re-validation. Confirms OWASP API1:2023.

Why this class of bug stays hidden

Any SPA that gzips its request bodies client-side, common with Angular and Vue front ends talking to a JSON API, can hide identical bugs. The tester who stops at "body is binary, move on" misses the entire authorization surface of that endpoint. The compression isn't an obstacle; it's a single import gzip away from plain text.

The same principle applies to other content encodings: Brotli (br), Zstandard (zstd), and even base64-wrapped blobs. The shape of the body's encoding is irrelevant. What matters is whether an object reference is inside it and whether the server validates that reference against the caller's actual authorization.

Fixing it

  • Re-validate the object reference server-side on every request. The server must confirm that the authenticated session user is authorized to access the requested id, not just that a valid session token is present. Compression of the request body is irrelevant to this check.
  • Never trust client-supplied object identifiers for scoping. Derive the record owner from the verified session, not from the request body, unless the caller holds an explicit delegation that is also validated server-side.
  • Audit all endpoints that accept Content-Encoding. Each one should be inspected as if the body is plaintext, because under test conditions it is. The encoding is a transport convenience, not a security control.
  • Treat any privilege flag in a client-controlled body as a separate finding. A boolean that requests elevated behaviour belongs to the server's decision, derived from the session role, never a field the client can set to true.

The methodology, generalized

  • Never conclude "not testable" from a body that fails to render as text; verify what it is first.
  • Export the raw item from Burp (base64/file), not a screenshot; screenshots of rendered bytes can't be reversed.
  • Check the magic number before assuming the encoding: 1f 8b for gzip, don't just trust the header.
  • Inflate, parse, then re-read the JSON as if seeing the endpoint for the first time; object references hide in the fields nobody expected to be editable.
  • Any identifier found this way is a BOLA candidate the moment the server doesn't re-validate it against the caller's authorization.