Browser cache forensics: Chrome, Firefox and Safari
2026-06-21 · 4 min
The HTTP cache is the artifact people forget to clear. A user can wipe
History and still leave behind a complete inventory of fetched URLs in
the cache — every script, image and document the browser pulled, each with
fetch timestamps. It proves a resource was on the wire even when the
history row is gone, and the timestamps slot straight into a timeline.
Why the cache outlives history
Browsers treat the cache as performance data, not user data. "Clear
browsing history" and the cache are separate toggles, and tools that wipe
SQLite history rarely touch the cache directory. The result is a frequent
mismatch: an empty urls table, a cache full of entries for the same
sites. Each cache backend stores the request URL alongside fetch
metadata, so even without the response body you recover what was
requested and when.
Chrome / Edge: Simple Cache
Modern Chromium uses the Simple Cache backend on most platforms — one file per cached entry under the profile:
…\User Data\<profile>\Cache\Cache_Data\
Each entry file opens with a SimpleFileHeader. Critically, the header
stores the entry key, which is the cached URL. Parse the directory and
you get a URL inventory plus per-entry size, without decoding any bodies.
| Field | Meaning |
|---|---|
| Magic | Identifies a Simple Cache entry |
| Version | Header format version |
| Key length | Byte length of the URL key |
| Key | The cached request URL |
The older blockfile cache (data_0–data_3 plus f_###### external
files) is a different format and is not covered here.
Firefox: cache2
Firefox uses the cache2 backend, again one file per entry:
<profile>/cache2/entries/
Each file holds the response, followed by a metadata block appended at the end of the file. That trailer carries the request URL, a fetch count, and a set of timestamps:
| Metadata | Meaning |
|---|---|
| Key / URI | Cached request URL |
| Fetch count | Number of times served from cache |
| Last fetched | Unix seconds |
| Last modified | Unix seconds |
| Expiration | Unix seconds |
The fetch count is useful on its own: it distinguishes a one-off load from a resource the user hit repeatedly. See browser timestamp formats for the unix-seconds conversions.
Safari: Cache.db
Safari does not scatter files — it uses a single SQLite database,
Cache.db:
| Table / column | Meaning |
|---|---|
cfurl_cache_response.request_key | Cached request URL |
cfurl_cache_response.time_stamp | When the entry was stored |
| blob tables | The cached response bodies |
Because it is SQLite, you query it directly read-only — and the same WAL
caveats apply, so grab Cache.db-wal and Cache.db-shm with it.
What the tool does
BrowserForensics groups an entire cache directory into a single
artifact. Drop the Cache_Data or cache2/entries folder in and the
hundreds of per-entry files collapse into one table of URLs and
timestamps, ready to sort, filter and export.
Honest limits:
- It recovers the URL and metadata inventory, not the cached response bodies.
- It does not parse the legacy Chrome blockfile cache (
data_#/f_*).
For most investigations the inventory is the point: the question is usually did this machine fetch that URL, and when, not can I rebuild the rendered page.
Using cache in an investigation
- Corroborate cleared history. Cache URLs for a site whose history row is missing is a strong deletion signal.
- Prove a fetch happened. The presence of an entry shows the resource was requested, independent of whether the user "visited" in the UI sense.
- Feed the timeline. Fetch and store timestamps are timeline events — fold them in alongside visits and downloads when you build a browser activity timeline.
Always copy the whole cache directory, not a selection of files: the per-entry layout means a partial copy is a partial inventory.