Browser, email and cloud sync · Lesson 2 of 6
Firefox: places.sqlite and session data
10 min read
Firefox keeps its history in its own SQLite schema, its own JSON files and a compressed session format, with timestamps counted from a different epoch than Chromium. This lesson shows where a Firefox profile lives, which tables answer "what was visited, typed and downloaded, and when", and how to open the session files that record open and recently closed tabs. Lesson 4.3 covers the cache and private browsing.
Finding the profile
On Windows, each profile has two folders with the same name: %APPDATA%\Mozilla\Firefox\Profiles\<random>.<name> (roaming: the databases and JSON files in this lesson) and %LOCALAPPDATA%\Mozilla\Firefox\Profiles\<random>.<name> (local: the cache). The prefix is eight random characters; current releases name the profile created for each installation default-release, for example k3x9p2qa.default-release.
Two files in %APPDATA%\Mozilla\Firefox\ tell you which profile was in use. profiles.ini lists every profile in a [ProfileN] section (Name, IsRelative, Path) and, since Firefox 67, an [Install<hash>] section whose Default= line names the profile used by one installation. installs.ini repeats that mapping, one section per installation hash. A profile named default with Default=1 is often an almost empty legacy profile, so follow the [Install…] entry, and still examine every profile folder present.
On Linux the profiles sit under ~/.mozilla/firefox/ with the cache under ~/.cache/mozilla/firefox/. The Snap package uses ~/snap/firefox/common/.mozilla/firefox/ and the Flatpak ~/.var/app/org.mozilla.firefox/.mozilla/firefox/. From Firefox 147, new installations follow the XDG layout under ~/.config/mozilla/firefox/. Search for every profiles.ini rather than trusting one path.
Most Firefox databases run in write-ahead logging mode. Collect places.sqlite together with places.sqlite-wal and places.sqlite-shm (likewise for cookies, form history and favicons): recent visits may exist only in the WAL. Hash the set, then work on copies, because opening a database with SQLite can checkpoint the WAL into the main file and change both.
places.sqlite
moz_places and moz_historyvisits
moz_places holds one row per URL: url, title, rev_host (the host reversed character by character plus a trailing dot, so transfer.example.com becomes moc.elpmaxe.refsnart.), visit_count, typed (1 if the URL was ever typed), frecency (a ranking score for address bar suggestions, not a time), last_visit_date and guid. moz_historyvisits holds one row per visit: place_id (joins to moz_places.id), visit_date, visit_type and from_visit (the id of the visit that led here, 0 if none). Both dates are PRTime: microseconds since 1970-01-01 UTC (Lesson 3.1).
| visit_type | Name | Meaning |
|---|---|---|
| 1 | LINK | Followed a link |
| 2 | TYPED | Typed or selected in the address bar |
| 3 | BOOKMARK | Opened from a bookmark |
| 4 | EMBED | Embedded content; current versions keep these in memory, so they are rarely on disk |
| 5 | REDIRECT_PERMANENT | Reached through a permanent redirect, such as HTTP 301 |
| 6 | REDIRECT_TEMPORARY | Reached through a temporary redirect, such as HTTP 302 |
| 7 | DOWNLOAD | URL fetched as a download |
| 8 | FRAMED_LINK | Link followed inside a frame |
| 9 | RELOAD | Page reloaded |
Following from_visit backwards reconstructs a navigation chain. Firefox expires old history automatically, so a missing visit is not proof that no visit happened.
Bookmarks, typed input and origins
moz_bookmarks stores bookmarks (type 1), folders (2) and separators (3), with fk pointing to moz_places.id, parent, title, and dateAdded and lastModified in PRTime. moz_inputhistory records what the user typed in the address bar and which result they then chose: input (the typed text), place_id (the page selected) and use_count, a decaying score. It has no timestamp, so date it through the matching visits. moz_origins keeps one row per scheme-and-host (prefix, host, frecency), referenced by moz_places.origin_id.
Downloads
Since Firefox 26, downloads are recorded in places.sqlite; older profiles used a separate downloads.sqlite. A download adds a visit of type 7 for the source URL and two page annotations in moz_annos, attached to that URL's place_id and named through moz_anno_attributes.name:
downloads/destinationFileURI:contentholds the save location as a URI, such asfile:///C:/Users/jsilva/Downloads/Q1_payroll.zip.downloads/metaData:contentholds JSON withstate(1 finished, 2 failed, 3 canceled; other values mark paused or blocked downloads),endTimein milliseconds since 1970 UTC,fileSize(present when the download succeeded) anddeleted.
moz_annos rows also carry dateAdded and lastModified in PRTime. Corroborate with the file system: the file's $MFT entry and its Zone.Identifier stream (Lesson 1.4).
Other profile files
favicons.sqlite: icons (moz_icons) linked to page URLs (moz_pages_w_icons), a corroborating trace that a page was loaded or bookmarked.formhistory.sqlite:moz_formhistorystores values typed into web form fields:fieldname,value,timesUsed,firstUsedandlastUsed(PRTime). Search boxes appear here too, often withfieldnamesearchbar-history.cookies.sqlite:moz_cookieswithhost,name,value,creationTimeandlastAccessed(PRTime) andexpiry. Values are not encrypted. Older releases storeexpiryin Unix seconds and current releases in milliseconds, so check the digit count.permissions.sqlite:moz_permsrecords site permissions (origin,typesuch asdesktop-notificationorcamera,permission,modificationTime).logins.json: saved credentials.hostname,formSubmitURLand the times are readable;encryptedUsernameandencryptedPasswordare encrypted with a key held inkey4.db(NSS key database, SQLite since Firefox 58), protected by the Primary Password if one is set.extensions.json: installed add-ons withid,version,active,installDateandupdateDate(milliseconds) andpath; the packages sit inextensions\as.xpifiles.addons.jsoncaches add-on store metadata. Look for add-ons the user would not have installed knowingly.
Session data
The sessionstore-backups folder holds the session state:
recovery.jsonlz4: the live session, rewritten while Firefox runs (every 15 seconds by default).recovery.baklz4: the previous copy of the recovery file.previous.jsonlz4: the session from before the last restart.upgrade.jsonlz4-<buildID>: a copy made when Firefox updated, named with the 14-digit build ID.- At clean shutdown Firefox also writes
sessionstore.jsonlz4in the profile root.
The files use Mozilla's own LZ4 container: bytes 0–7 are the magic mozLz40\0 (6D 6F 7A 4C 7A 34 30 00), bytes 8–11 the uncompressed size as a little-endian 32-bit integer, and the rest one raw LZ4 block, not an LZ4 frame, so the standard lz4 command-line tool rejects it. Use a dedicated tool such as dejsonlz4 or lz4json, or Python: lz4.block.decompress(data[12:], uncompressed_size=int.from_bytes(data[8:12], 'little')). Equivalently, lz4.block.decompress(data[8:]), because that function reads a 4-byte little-endian size prefix by default.
The JSON reveals windows[].tabs[], each with entries[] (the tab's back-and-forward history: url, title, referrer), an index marking the current entry, and lastAccessed; _closedTabs with closedAt; _closedWindows; and formdata holding text typed into page fields (password fields are not saved). Session times are milliseconds since 1970 UTC. Comparing previous.jsonlz4 with recovery.jsonlz4 shows what changed between sessions.
Firefox and Chromium side by side
| Firefox file | Chromium equivalent | What it answers |
|---|---|---|
places.sqlite: moz_places, moz_historyvisits | History: urls, visits | Which pages, when, and how they were reached |
places.sqlite: moz_annos | History: downloads, downloads_url_chains | What was downloaded, from where, saved where |
places.sqlite: moz_inputhistory | Shortcuts: omni_box_shortcuts | What the user typed in the address bar |
places.sqlite: moz_bookmarks | Bookmarks (JSON) | Pages deliberately saved, and when |
formhistory.sqlite | Web Data: autofill | Values typed into web forms |
cookies.sqlite | Network\Cookies (values encrypted) | Sites visited and sessions held |
logins.json + key4.db | Login Data + Local State key | Saved credentials and their sites |
sessionstore-backups\*.jsonlz4 | Sessions\Session_*, Tabs_* | Open and closed tabs, page form data |
extensions.json | Extensions\ + Preferences | Installed add-ons |
Worked example: typed, then downloaded
A triage collection contains places.sqlite and its WAL. The question: did the user deliberately go to a file-transfer site, and when did a download finish? Run:
SELECT v.id, v.from_visit, strftime('%Y-%m-%dT%H:%M:%SZ', v.visit_date/1000000, 'unixepoch') AS visit_utc, v.visit_type, p.url FROM moz_historyvisits v JOIN moz_places p ON p.id = v.place_id ORDER BY v.visit_date;
| id | from_visit | visit_utc | visit_type | url |
|---|---|---|---|---|
| 1201 | 0 | 2026-05-19T08:41:37Z | 2 | https://transfer.example.com/ |
| 1202 | 1201 | 2026-05-19T08:42:18Z | 1 | https://transfer.example.com/d/7Hq2 |
| 1203 | 1202 | 2026-05-19T08:42:25Z | 7 | https://dl.transfer.example.com/7Hq2/Q1_payroll.zip |
The raw visit_date of visit 1201 is 1,779,180,097,512,334. Integer division by 1,000,000 gives 1,779,180,097 seconds, which is 2026-05-19T08:41:37Z; the division truncates the 0.512334 s, as an answer in whole seconds should. (datetime(visit_date/1000000,'unixepoch') gives the same instant without the T and Z.) Visit 1201 is TYPED with no referrer, and moz_inputhistory has input = trans pointing to that page, so the user typed the start of the address. Visit 1202 was a link followed from 1201, and visit 1203 a download started from 1202.
For the ZIP URL's place (the target of visit 1203), downloads/metaData reads {"state":1,"deleted":false,"endTime":1779180163904,"fileSize":48213977}. Here endTime is milliseconds: 1,779,180,163,904 ÷ 1,000 = 1,779,180,163.904 s, so the download finished at 2026-05-19T08:42:43Z, 18 seconds after the type 7 visit.
Do not mix the browsers' epochs. Chromium stores the same instant as microseconds since 1601: (1,779,180,097 + 11,644,473,600) × 1,000,000 + 512,334 = 13,423,653,697,512,334, and Chromium's query must subtract 11,644,473,600 after dividing. Decode the Firefox value with Chromium's formula and you get 1657-05-19T08:41:37Z; decode Chromium's value as PRTime and you get 2395-05-19T08:41:37Z. Both units are microseconds, so only the epoch distinguishes them; a 16-digit value in 2026 is PRTime and a 17-digit value is Chromium time.
Tools
DB Browser for SQLite runs queries like the one above. Plaso has Firefox SQLite plugins, such as firefox_history and firefox_cookies; compare their output with a manual query, because schemas change between versions. Hindsight parses Chromium-based browsers only and does not read Firefox profiles.
Exam focus: Expect to convert PRTime values to ISO 8601 UTC, to readvisit_typecodes (especially 2 TYPED and 7 DOWNLOAD), and to recover a download's save path and completion time frommoz_annos, remembering thatendTimeis in milliseconds. You may also need to locate the active profile fromprofiles.iniorinstalls.ini, or recognize themozLz40\0header and its 12-byte length.
Key points
- The roaming profile holds the databases and the local profile holds the cache;
profiles.iniandinstalls.iniidentify the profile each installation used. - Join
moz_historyvisits.place_idtomoz_places.id; PRTime is microseconds since 1970 UTC, Chromium time is microseconds since 1601. - Downloads since Firefox 26 are a type 7 visit plus
downloads/destinationFileURIanddownloads/metaDataannotations, withendTimein milliseconds. - Form history, cookie values and the session's form data are stored unencrypted; saved passwords need
key4.db. - Session files are a 12-byte
mozLz40\0header plus one LZ4 block, and they reveal open tabs, closed tabs and typed form data. - Always collect the
-waland-shmfiles and analyze copies.