SQLite forensics for browsers
2026-06-21 · 4 min
If you do browser forensics, you do SQLite forensics. The overwhelming majority of the artifacts you care about live in SQLite databases, and the file you double-click is rarely the whole story. The recent activity and the deleted records hide in sidecars and unallocated space that a naive copy throws away.
Almost everything is a SQLite database
The high-value stores across the major browsers are SQLite:
- Chromium —
History,Cookies,Login Data,Web Data(autofill, payment methods). - Firefox —
places.sqlite(history and bookmarks),cookies.sqlite,formhistory.sqlite. - Safari —
History.db,Cache.db.
Different schemas, same engine. Learn the file format once and it pays off across every browser you touch.
How a SQLite file is laid out
A SQLite database is a single file divided into fixed-size pages (1 KB
to 64 KB; 4096 bytes is the common default). The first 16 bytes are the
magic header SQLite format 3\0, which is how you recognise one in a
hex view or a carve even when the extension is wrong.
The schema itself is data. The sqlite_master table maps every table
and index to its root page and stores the CREATE statements, so you
can reconstruct the layout of an unfamiliar database without
documentation. Tables and indexes are B-trees walked from those root
pages.
The sidecars you must capture
A SQLite database is rarely just the .db. Modern browsers run in
write-ahead logging mode, which spreads live state across companion
files:
-wal(write-ahead log). Committed changes that have not yet been checkpointed back into the main file. This is the most recent activity — the visits, downloads and cookies from the current session. Miss it and you miss the part of the timeline the investigation is usually about.-shm(shared memory). The index into the WAL that lets readers find the latest version of each page. SQLite needs it to interpret the WAL.-journal. The rollback journal used in the older (non-WAL) journaling mode. You will still meet it on some databases.
Always capture these together, with matching base names. Copy only the
.db and you silently drop whatever is sitting in the WAL — frequently
rows that exist nowhere else yet.
Deleted rows do not vanish
Deleting a row in SQLite does not zero its bytes. When records or whole pages are freed, SQLite links the pages onto a freelist and marks the cells reusable. The old cell content — the actual history entry, cookie or login record — lingers in that unallocated/free space until the engine reuses it.
That is the basis for recovery beyond what the WAL holds. Carving the freelist and the unallocated regions of the page can resurrect deleted records long after a "clear browsing history" operation, right up until the cells are overwritten. It is a deeper technique than reading the live tables, but it is where genuinely deleted evidence comes back.
Reading it in the tool
BrowserForensics parses SQLite entirely client-side in your browser —
the database never leaves your machine. Load a database with its -wal
sidecar alongside and the tool applies the WAL automatically, so the
rows you see reflect the most recent committed state rather than the
stale main file. Freelist and unallocated carving is a manual,
lower-level follow-up to be aware of when live tables come up short.
What to take away
- Treat the database and its
-wal/-shm/-journalsidecars as one evidence unit, captured read-only and together. - The WAL is recency; the freelist and unallocated space are deletion recovery. They answer different questions.
- Confirm the magic header and read
sqlite_masterbefore trusting any parser's view of an unfamiliar file.