browserforensics
Back to all articles

Firefox session restore: jsonlz4 forensics

2026-06-21 · 3 min

Firefox keeps a running record of what was open so it can restore your windows after a crash or restart. That same record is a forensic gift: it holds the URLs of currently open tabs, their per-tab navigation history, and a list of recently closed tabs with timestamps — often surviving long after places.sqlite has been cleared.

The files

Session state lives in the profile root and in sessionstore-backups/:

FileWhen written
sessionstore.jsonlz4On clean shutdown (open tabs at exit)
sessionstore-backups/recovery.jsonlz4Live state, written every ~15s while running
sessionstore-backups/recovery.baklz4Previous copy of recovery.jsonlz4
sessionstore-backups/previous.jsonlz4Session before the current one
sessionstore-backups/upgrade.jsonlz4-*Snapshot taken at a version upgrade

recovery.jsonlz4 is the one that matters on a live or crashed machine — it reflects what was open at the moment of acquisition. On a cleanly shut down host, look to sessionstore.jsonlz4 instead. The upgrade.jsonlz4-* files are easy to overlook and can carry tabs from weeks earlier.

The mozLz4 container

These are not standard LZ4 files. Firefox wraps a single LZ4 block in a small Mozilla-specific header it calls mozLz4:

offset 0   8 bytes   magic "mozLz40\0"
offset 8   4 bytes   decompressed size, uint32 little-endian
offset 12  …         one LZ4 block (raw, not framed)

Generic lz4 tooling chokes on the magic. Read the 12-byte header, take the declared decompressed size, then run a raw LZ4 block decompression over the remainder. The result is plain UTF-8 JSON. The tool does exactly this in the browser — it strips the mozLz40\0 magic, reads the size, and inflates the block client-side.

The JSON shape

Decompressed, the structure is a tree of windows and tabs:

{
  "windows": [
    {
      "tabs": [
        {
          "entries": [
            { "url": "https://example.com/", "title": "Example" },
            { "url": "https://example.com/login", "title": "Sign in" }
          ],
          "index": 2,
          "lastAccessed": 1718900000000
        }
      ],
      "_closedTabs": [
        { "state": { /* … */ }, "title": "Old page", "closedAt": 1718890000000 }
      ]
    }
  ]
}

Read it like this:

  • entries is the per-tab navigation history — the back/forward stack. Every URL the tab walked through is here, not just the current page.
  • index is 1-based and points at the entry currently displayed. Entries after index are the forward history.
  • lastAccessed on each open tab is the last time it was focused.
  • _closedTabs is the recently closed list (the "Recently Closed Tabs" menu). Each holds a nested state with its own entries, plus a closedAt.

Timestamps

lastAccessed and closedAt are Unix milliseconds — not the PRTime microseconds Firefox uses in places.sqlite. Convert by dividing by 1000 before treating them as epoch seconds, or you will land tens of thousands of years in the future. See browser timestamp formats for the wider zoo of Firefox time bases.

What to recover

  • Open tabs at acquisition — the full URL list per window, with focus times. Pulls from recovery.jsonlz4 (running) or sessionstore.jsonlz4 (clean exit).
  • Per-tab back/forward historyentries often shows the path a user took into a site, including intermediate pages history collapsed away.
  • Recently closed tabs_closedTabs with closedAt. These are tabs the user deliberately closed, which is frequently the interesting set.

The tool decompresses each mozLz4 file and parses open and recently-closed tabs into a single timeline you can sort by lastAccessed/closedAt.

Pitfalls

  • Compare recovery.jsonlz4 against sessionstore.jsonlz4. A clean shutdown updates the latter; a crash leaves it stale while recovery holds the truth.
  • Don't stop at the current page — mine entries for the navigation history hiding inside each tab.
  • A blank _closedTabs list does not mean nothing was closed; Firefox caps the kept count and the user may have cleared it.

Further reading