browserforensics
Back to all articles

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 restoresessionstore.jsonlz4 and the recovery.jsonlz4 files 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 .jsonlz4 extension 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.

Further reading