browserforensics
Back to all articles

Firefox places.sqlite forensics

2026-06-21 · 4 min

places.sqlite is the single most useful file in a Firefox profile. Unlike Chrome, which splits history and bookmarks across separate stores, Firefox keeps both in one SQLite database — plus the visit graph that links them. Learn its two core tables and you can reconstruct a browsing timeline, the referrer chain that led to each page, and which URLs the user deliberately saved.

The schema you actually need

places.sqlite has a dozen tables, but two carry the case.

TableRole
moz_placesOne row per distinct URL — the hub
moz_historyvisitsOne row per visit to a URL
moz_bookmarksBookmark tree; joins to moz_places for URLs
moz_annosPer-place annotations, including downloads
moz_originsHost/scheme breakdown (newer profiles)

moz_places is keyed by id and holds the URL-level facts:

ColumnMeaning
urlThe full URL
titleLast-seen page title
visit_countNumber of recorded visits
last_visit_datePRTime of the most recent visit
frecencyFrequency + recency ranking score
rev_hostHost reversed (moc.elpmaxe.) for suffix indexing

rev_host trips people up: www.example.com is stored as moc.elpmaxe.www. so Firefox can index by domain suffix. Reverse it back before you report it.

Per-visit detail and the referrer chain

moz_historyvisits is where the timeline lives. Each row records one visit:

ColumnMeaning
place_idForeign key → moz_places.id
visit_datePRTime of this visit
visit_typeHow the visit happened (see below)
from_visitid of the visit that referred this one

from_visit is the gold. It points at the previous visit row, so you can walk the chain backwards and prove how the user reached a page — clicked a link, followed a redirect, or typed the address. A typed visit with no referrer is a deliberate navigation; a link visit with a from_visit set shows the click that produced it.

visit_type values

The visit_type integer tells you the navigation source. Filter it carefully — embed and framed_link visits are usually noise (ads, trackers, iframes), not user intent.

ValueTypeNotes
1linkFollowed a link
2typedUser typed the URL — strong intent signal
3bookmarkOpened from a bookmark
4embedEmbedded resource; not top-level
5redirect_permanent301
6redirect_temporary302
7downloadDownload initiated
8framed_linkLink followed inside a frame
9reloadPage reload

For a defensible "the user intentionally visited X" claim, lean on visit_type 2 (typed) and 3 (bookmark), then corroborate with the from_visit chain.

Timestamps: PRTime

Every timestamp in places.sqlite is PRTime — microseconds since the Unix epoch (1970-01-01 UTC). This is the most common parsing error on Firefox cases: people apply Chrome's WebKit epoch (1601) or treat the value as milliseconds and land decades off.

unix_seconds = visit_date / 1000000

So 1718928000000000 is 2024-06-21 00:00:00 UTC, not the year 56000-something. See the timestamp formats post for the full epoch matrix.

Bookmarks and downloads

Bookmarks live in moz_bookmarks as a tree (folders, separators, and bookmark nodes), joined to moz_places through fk for the actual URL. A saved bookmark is a strong intent artifact — the user chose to keep it.

Downloads are not in their own table. Firefox records them as annotations on the originating place: look in moz_annos for the downloads/destinationFileURI annotation, which carries the local path the file was saved to. That ties a downloaded file back to the URL and visit that produced it.

Acquisition: don't skip the WAL

places.sqlite runs in WAL mode. Recent visits, bookmarks, and downloads may sit in the places.sqlite-wal sidecar and never appear if you parse the main file alone. Always grab places.sqlite, places.sqlite-wal and places.sqlite-shm together, with matching base names. The same trap that loses recent Chrome history loses recent Firefox history.

BrowserForensics parses places.sqlite directly in the browser — history, bookmarks, the visit timeline and downloads — and applies the WAL so the most recent activity is included.

Further reading