Compression in browser artifacts: Snappy, mozLz4 and more
2026-06-21 · 4 min
A lot of browser data never touches disk in plaintext. It is compressed
first, which means a naive strings or grep over the raw file quietly
misses the content you came for. Worse, the formats are browser-specific:
the same LZ4 algorithm wears a different header in Firefox than anywhere
else, and generic tools choke on it. Know where compression hides and you
stop losing evidence to it.
Where compression appears
Four places account for almost everything you will meet:
- LevelDB blocks — Local Storage, Session Storage and IndexedDB all live in LevelDB, whose data blocks are Snappy-compressed by default. A per-block trailer flags whether a given block is stored raw or compressed.
- Firefox session restore —
sessionstore.jsonlz4and therecovery.jsonlz4files use Mozilla's mozLz4 wrapper around an LZ4 block. - HTTP cache bodies — cached response bodies are stored compressed
per the response's
Content-Encoding: gzip, brotli or deflate. - Inner framing — some IndexedDB values and V8 snapshot blobs add their own length prefixes or framing on top of whatever the container already did.
Snappy in LevelDB
Snappy is Google's speed-first compression codec — modest ratios, very fast decompression. LevelDB uses it block by block: each block carries a one-byte compression-type trailer, so a single SSTable can mix raw and Snappy blocks. There is no file-level magic number; you only know a block is Snappy because the trailer says so.
Practical consequence: you cannot read an .ldb or .log file by
scanning bytes. You have to parse the block structure, check each
trailer, and Snappy-decompress where indicated before the keys and values
become legible. Skip that step and your search hits only the blocks that
happened to be stored uncompressed.
mozLz4 in Firefox sessions
Firefox writes open tabs, window state and crash-recovery data into
session-restore files with a .jsonlz4 extension. Despite the name, the
payload is not standard .lz4 — it is Mozilla's own mozLz4 wrapper:
offset 0 8 bytes magic "mozLz40\0"
offset 8 4 bytes uint32 LE decompressed size
offset 12 … a single raw LZ4 block
The magic string and the explicit decompressed-size field are what set mozLz4 apart from the standard LZ4 frame format. Feed one of these files to a generic LZ4 tool and it fails on the header — it never reaches the actual block. To read it you strip the 12-byte header, read the size field, and LZ4-decompress the remainder into a buffer of that size. What comes out is plain JSON describing every window, tab and back/forward entry.
HTTP cache bodies
The disk cache stores response bodies exactly as they arrived on the
wire, so the encoding is whatever the server negotiated:
Content-Encoding: gzip, br (brotli) or deflate. The encoding is
recorded in the cached response headers, not guessed from the body. To
reconstruct a cached page or asset you read the stored headers, pick the
matching decompressor, and inflate the body. Brotli in particular is
common on modern sites and is invisible to tools that only know gzip.
Why it matters for DFIR
Every one of these formats defeats the reflex of running strings over a
raw artifact. The bytes on disk are not the bytes you want to read or
search — they are the compressed bytes. If you index, keyword-search or
eyeball a profile without decompressing first, you will conclude data is
absent when it is merely encoded. Two specific traps:
- Snappy has no signature. You cannot find compressed blocks by magic bytes; you must parse LevelDB structure to know where they are.
- mozLz4 is not
.lz4. The custom header means off-the-shelf LZ4 utilities reject the file outright. The.jsonlz4extension is the only obvious tell.
Decompression also has to happen before timeline and search, not as an afterthought — the values that anchor an event (a tab URL, a Local Storage token) live inside the compressed payload.
Reading it in the tool
BrowserForensics handles the two browser-specific formats natively and
entirely client-side. It implements Snappy to walk LevelDB blocks for
Local Storage, Session Storage and IndexedDB, and it implements
mozLz4 to open Firefox sessionstore.jsonlz4 and recovery.jsonlz4
directly — header, size field, LZ4 block and all. You point it at a copied
artifact and read the decoded content; nothing is uploaded, and you do not
need a separate decompression step on the command line.