> Where you are: Part III, Chapter 18 of 40. Chapter 16 read the receipts Windows keeps about itself; Chapter 17 did the same for macOS and Linux. Now you open the single richest record any of those operating systems carries about its human — the...
In This Chapter
- Why the browser is the richest witness on the machine
- The browser profile: where everything lives
- SQLite: the database under (almost) every browser
- Chrome and Chromium: History, Cookies, Login Data, Web Data
- Firefox: places.sqlite, cookies.sqlite, and formhistory
- Microsoft Edge and Internet Explorer
- Safari
- The cache: pages, images, and downloads the user never saved
- Browser timestamps: a field guide to the epochs
- Private/Incognito browsing: what it does and does NOT remove
- Cloud-storage sync artifacts: Dropbox, Google Drive, OneDrive
- Recovering deleted browser history
- Tool demonstration: a Chromium profile, parsed two ways
- Worked example: the cloud-upload trail
- Common mistakes
- Limitations: knowing when to stop
- Progressive project: recover the browser/internet history for the case
- Summary
Chapter 18: Browser and Internet Forensics — Web History, Cache, Cookies, and the Digital Footprint
Where you are: Part III, Chapter 18 of 40. Chapter 16 read the receipts Windows keeps about itself; Chapter 17 did the same for macOS and Linux. Now you open the single richest record any of those operating systems carries about its human — the web browser. If the registry is the machine's logbook, the browser profile is the user's diary, written in their own hand: what they searched, what they read, where they logged in, what they downloaded, and which cloud service they uploaded to on the way out the door. This chapter threads anchor case #2 (the departing engineer's cloud-upload trail) and, handled strictly clinically, touches anchor case #4 (cache and search-term evidence of access).
Learning paths: This is core territory for the 🔍 Forensic Examiner — browser artifacts appear in nearly every case involving a human suspect. 📜 Legal/eDiscovery practitioners should read the cache-as-possession and search-term-as-intent discussions closely; they decide cases. 🛡️ Incident Response uses browser history to trace phishing clicks, malicious downloads, and data exfiltration to web services. 💾 Data Recovery technicians get a genuine recovery discipline here too: bookmarks, saved passwords, and a profile's worth of history are exactly what a client begs you to restore from a dying drive.
Why the browser is the richest witness on the machine
Ask a person to recount their week and they will give you a sanitized, forgetful, self-flattering summary. Ask their browser and you will get the truth in microsecond resolution: the 2:14 a.m. search they would never admit to, the competitor's careers page opened the day before they gave notice, the medical symptom typed into a search bar, the forty-seven visits to a single URL, the cloud-storage login at 8:52 on a Saturday morning. Modern browsers are not passive windows onto the web. They are aggressive caching, indexing, autocompleting, password-saving, session-restoring databases that record almost everything for the user's convenience — and every one of those conveniences is an artifact.
The forensic payload is enormous because the browser is where the physical machine meets the wider world. A document on disk tells you a file existed. Browser history tells you a person went somewhere, typed something, intended something — and it timestamps the intent. That is why browser evidence anchors so many different kinds of cases: insider data theft (the cloud-upload trail), intrusion and phishing (the malicious link that was clicked, the dropper that was downloaded), fraud (the webmail and banking sessions), harassment and stalking (the accounts accessed), and the most consequential criminal matters, where access dates, search terms, and cache contents are elements the prosecution must prove. The same database that helps a recovery client get their bookmarks back is the database that proves, in a courtroom, that a specific account on a specific machine navigated to a specific URL at a specific instant.
Consider the running anchor for this chapter. In Chapter 16 — Windows Forensics you proved that user jrivera connected a SanDisk thumb drive on Friday evening, opened proprietary CAD files from it, backdated the local copies, and ran CCleaner on Saturday morning at 09:14:22. You noted then that the browser history "ends abruptly Saturday 09:14" — the cleaner wiped it. This chapter resumes exactly there. The engineer cleared his history believing that deleting it destroyed it. He was wrong on both counts: the cleared rows persisted in unallocated space inside the SQLite database, and even the act of clearing left a dated gap that lines up to the minute with the cleaner's execution. By the end of this chapter you will recover that "deleted" browsing session, read the cloud uploads it records, corroborate the bytes with Windows network-usage telemetry, and add a sourced browser timeline to the case — a direct application of the first theme, deleted is not destroyed.
Why This Matters. Browser history is, for most people, the most intimate record they generate — more revealing than their email, their texts, or their search of any single app. It exposes health, sexuality, finances, relationships, politics, and fear. That is precisely why it is decisive evidence and precisely why it demands discipline: scope your search to the warrant or engagement, handle what you incidentally see with care, and remember the sixth theme — the human cost is real. The diary you are reading was never meant for an audience.
A map of the browser's artifact families
Before the byte-level work, hold this map. Every mainstream browser stores the same kinds of evidence; they differ only in file format, location, and timestamp epoch.
BROWSER ARTIFACT FAMILIES (this chapter)
┌──────────────────────────────────────────────────────────────────────────┐
│ HISTORY URLs visited, visit TIME, visit COUNT, TRANSITION type │
│ (typed vs clicked vs redirect) + download history │
│ CACHE Cached pages, images, media, downloads the user NEVER saved │
│ COOKIES Session + persistent + tracking; logins, last-access times │
│ AUTOFILL Form data: names, addresses, emails, phones, card metadata │
│ SAVED LOGINS Stored credentials (encrypted); which sites, when used │
│ BOOKMARKS Deliberately saved sites + the date each was added │
│ SESSIONS/TABS What was open at last run (can survive a private session) │
│ SEARCH TERMS What was typed into the omnibox and into site search boxes │
├──────────────────────────────────────────────────────────────────────────┤
│ STORES, BY BROWSER │
│ Chrome/Edge/Chromium → SQLite: History, Cookies, Login Data, Web Data │
│ + JSON Bookmarks + LevelDB storage (WebKit time) │
│ Firefox → SQLite: places.sqlite, cookies.sqlite, │
│ formhistory.sqlite + key4.db/logins.json (PRTime) │
│ Safari → SQLite History.db + binary plists + binarycookies │
│ (Mac/Cocoa time, since 2001) │
│ IE / legacy Edge → ESE: WebCacheV01.dat (FILETIME); older index.dat │
├──────────────────────────────────────────────────────────────────────────┤
│ INTERNET-SIDE TRACES (survive private browsing — see that section) │
│ OS DNS cache · pagefile/hiberfil · SRUM net usage · proxy/firewall logs │
│ CLOUD SYNC Dropbox / Google Drive / OneDrive local databases + logs │
└──────────────────────────────────────────────────────────────────────────┘
Notice the structure of the chapter inside that map: first the storage engine almost everything shares (SQLite), then each browser in turn, then cache, then a consolidated timestamp guide, then the myth-busting on private browsing, then cloud sync, then recovery of deleted history, and finally the worked example that assembles the anchor. The skill, exactly as in Windows forensics, is correlation: one TYPED visit is a lead; a TYPED visit plus a matching cookie last-access plus an SRUM byte-count plus a DNS-cache entry is a finding.
The browser profile: where everything lives
A browser keeps everything for one user in one directory tree called a profile. Find the profile and you have found the evidence; identify which profile and you have attributed it to a person. Multi-profile support (Chrome's "People", Firefox's profile manager) and multi-user machines make that attribution a live question you must answer explicitly, exactly as you did with per-user registry hives.
The default locations, which you should be able to recite (the full cross-platform table lives in Appendix D — Forensic Artifact Locations):
CHROME (Chromium) profile = the "Default" or "Profile N" folder inside User Data
Windows C:\Users\<u>\AppData\Local\Google\Chrome\User Data\Default\
macOS ~/Library/Application Support/Google/Chrome/Default/
Linux ~/.config/google-chrome/Default/
MICROSOFT EDGE (Chromium) same layout, different vendor path
Windows C:\Users\<u>\AppData\Local\Microsoft\Edge\User Data\Default\
FIREFOX profiles live under Profiles\<random>.default-release\
Windows C:\Users\<u>\AppData\Roaming\Mozilla\Firefox\Profiles\xxxxxxxx.default-release\
macOS ~/Library/Application Support/Firefox/Profiles/xxxxxxxx.default-release/
Linux ~/.mozilla/firefox/xxxxxxxx.default-release/
(the parent profiles.ini maps profile folders to names — read it first)
SAFARI (macOS) not one folder; spread across Library
~/Library/Safari/ (History.db, Bookmarks.plist, Downloads.plist)
~/Library/Cookies/Cookies.binarycookies (cookies)
~/Library/Caches/com.apple.Safari/ (cache)
Two practical notes. First, on Chromium browsers the folder is literally named Default for the first profile and Profile 1, Profile 2, … for additional ones; the friendly names you see in the UI are mapped in the Local State JSON file at the User Data root. Cite the on-disk folder, not the friendly name, in your report. Second, the User Data root holds Local State, which you will need later because it stores the AES key that protects saved passwords and cookies. Grab the whole User Data tree, not just Default.
Chain of Custody. You never open a profile on the original drive. You extract the profile directory from your verified image — with The Sleuth Kit (
fls/icatagainst the relevant MFT or inode entries), a forensic suite's file browser, or a read-only mount — and you hash every file you pull. A defensible note reads: "Extracted…\Chrome\User Data\Default\History(andHistory-wal,History-shm) for userjriverafrom imageWS-ENG-04.E01(SHA-256a3f1…) to working copy; copy SHA-256c7e9…." See Chapter 14 — Forensic Acquisition for imaging and Chapter 5 — The Forensic Process for the chain-of-custody fundamentals.
SQLite: the database under (almost) every browser
Chrome, Edge, Firefox, Safari, Brave, Opera, and most of the rest store their primary artifacts in SQLite databases. Learn to handle SQLite correctly once and you can examine any of them. Get it wrong once and you can corrupt evidence or, worse, miss the most recent and most relevant activity entirely. This section is the load-bearing skill of the chapter.
A SQLite database is a single file beginning with a fixed 16-byte signature, the ASCII string SQLite format 3 followed by a null byte:
SQLite database header (first 32 bytes)
Offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ASCII
00000000 53 51 4C 69 74 65 20 66 6F 72 6D 61 74 20 33 00 SQLite format 3.
└────────── "SQLite format 3\0" (magic) ──────────┘
00000010 10 00 01 01 00 40 20 20 00 00 00 0A 00 00 00 3C .....@ .......<
└ page ┘ └ file change counter ┘ └ db size in pages
size=0x1000=4096
That magic (53 51 4C 69 74 65 20 66 6F 72 6D 61 74 20 33 00) is in Appendix A — File Signatures Reference; recognizing it lets you spot a SQLite database — or a carved fragment of one — in unallocated space, which matters when a browser database has been deleted and you must recover the file before you can recover its rows. The 2-byte big-endian value at offset 16 (10 00 → 4096) is the page size; the database is a sequence of equal-sized pages holding the B-tree that stores the tables.
The cardinal rules: copy the whole set, and never let the engine write
Two facts about SQLite turn careless examiners into impeached witnesses.
First, the database you copy is often not the whole database. Modern browsers run SQLite in Write-Ahead Logging (WAL) mode. New and changed rows are written first to a companion file named <database>-wal, with a shared-memory index file <database>-shm, and only later "checkpointed" back into the main file. This means the most recent browsing — possibly the exact activity you care about — may live only in History-wal, not in History. If you copy History alone, you analyze a stale database and silently lose the latest hours of activity.
A Chrome profile's History, in WAL mode
History ← main database (may LAG real activity)
History-wal ← committed-but-not-checkpointed pages ← recent visits live HERE
History-shm ← shared-memory index for the WAL
Always copy ALL THREE together. They are one logical database.
Second, simply opening the database can change it. If you point a default sqlite3 connection at a profile, the engine may perform a checkpoint — folding the WAL into the main file and altering both files' contents and hashes. On the original evidence that is a fatal chain-of-custody breach; even on a working copy it muddies what was where. The disciplined workflow:
- Preserve the trio (
db,-wal,-shm) together, hashing each. - Work on the copy, never the original.
- To read with the WAL applied (the usual goal), open the copy read-only; SQLite merges the WAL into your view without writing if you avoid checkpointing.
- If you must guarantee zero writes to the main-db copy and process the WAL separately, open with the URI flag
mode=ro&immutable=1— but understand thatimmutable=1makes SQLite ignore the-wal, so you then have to parse the WAL file yourself or you lose its rows.
# Forensically safe read of a Chrome History copy (illustrative; never run on originals).
# Step 0 (in your collection script, not shown): copy History, History-wal, History-shm
# together into ./work/ and record SHA-256 of each in the chain-of-custody log.
import sqlite3
from datetime import datetime, timezone, timedelta
def open_ro(path: str) -> sqlite3.Connection:
# read-only URI; the engine will apply an adjacent -wal without checkpointing our view
con = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
con.execute("PRAGMA query_only = ON;") # belt-and-suspenders: refuse writes
return con
def webkit_to_utc(ts: int):
# Chrome/WebKit time = microseconds since 1601-01-01 UTC
if not ts:
return None
return datetime(1601, 1, 1, tzinfo=timezone.utc) + timedelta(microseconds=ts)
Recovery vs. Forensics. SQLite handling is where the two disciplines visibly diverge over the same file. The 💾 recovery technician copying a client's failing drive wants the bookmarks and saved logins back fast; they may happily let the browser checkpoint and rebuild, because the goal is a working profile, not a pristine one. The 🔍 forensic examiner must do the opposite: freeze the trio, open read-only, change nothing, and be able to testify that the database analyzed is byte-identical to the database imaged. Same
Historyfile; one job optimizes for restoration, the other for admissibility. Whenever an artifact in this chapter serves both, that is the line between them.
Deleted rows do not vanish
When a user clears their history, the browser issues SQL DELETE statements. A DELETE does not erase the row's bytes; it marks the row's cell as free and links the page (or part of it) into the database's freelist. The old content lingers in unallocated page space until a VACUUM rewrites the file — and browsers do not vacuum on every clear. This is the first theme in miniature: deleted is not destroyed. Cleared URLs, visit times, and download records routinely survive inside the very database the user "wiped," recoverable by parsing freelist pages and the unallocated tail of each B-tree page. We return to the tools for this under "Recovering deleted browser history," and it is the technical heart of this chapter's worked example.
Chrome and Chromium: History, Cookies, Login Data, Web Data
Chromium is the engine inside Chrome, Microsoft Edge, Brave, Opera, Vivaldi, and many more. Learn Chromium's four core databases and you have learned the majority of the deployed browser base. All four are SQLite; all four timestamp in WebKit time (microseconds since 1601-01-01 UTC).
The History database
Default\History is the workhorse. Three tables carry most of the value:
urls one row per distinct URL ever visited
id, url, title, visit_count, typed_count, last_visit_time, hidden
visits one row per individual visit (a URL with many visits has many rows)
id, url (→ urls.id), visit_time, from_visit, transition, visit_duration, opener_visit
downloads one row per download
id, target_path, tab_url, start_time, end_time, received_bytes, total_bytes, ...
urls.visit_count and urls.typed_count summarize a URL across its lifetime; the visits table holds the per-event detail. The join you will run constantly pairs them:
SELECT u.url, u.title, u.visit_count, u.typed_count,
v.visit_time, v.transition, v.from_visit, v.visit_duration
FROM urls u
JOIN visits v ON u.id = v.url
ORDER BY v.visit_time;
The single most forensically important column is visits.transition — how the user arrived at the page. The low byte (mask 0xFF) is the core transition type; the high bits are qualifier flags. The distinction between a TYPED visit and a LINK visit is the distinction between intent and incident: a user who types drive.google.com into the address bar chose to go there; a user who landed on a page via a redirect or an embedded ad did not.
Chrome core transition types (transition & 0xFF)
0 LINK clicked a link
1 TYPED typed the URL in the address bar ← intent
2 AUTO_BOOKMARK opened from a bookmark / UI
3 AUTO_SUBFRAME a subframe loaded automatically (ads, embeds — NOT a user visit)
4 MANUAL_SUBFRAME user explicitly navigated a subframe
5 GENERATED omnibox-generated (e.g., a search the omnibox built)
6 START_PAGE the start page / new-tab page
7 FORM_SUBMIT submitted a form
8 RELOAD reloaded (or session-restored)
9 KEYWORD a custom search-engine keyword
10 KEYWORD_GENERATED generated from a keyword search
Qualifier bits (OR'd into the high bytes)
0x01000000 FORWARD_BACK used Back/Forward (not a fresh visit)
0x02000000 FROM_ADDRESS_BAR came from the address bar
0x04000000 HOME_PAGE the configured home page
0x08000000 FROM_API navigation triggered by an extension/app
0x10000000 CHAIN_START first hop of a redirect chain
0x20000000 CHAIN_END last hop of a redirect chain
0x40000000 CLIENT_REDIRECT page-driven (meta refresh / JS) redirect
0x80000000 SERVER_REDIRECT HTTP 3xx redirect
Read those qualifiers carefully before you count visits. A row with AUTO_SUBFRAME is an ad or embed that loaded without the user doing anything; counting it as "the user visited this site" is a beginner error that inflates visit_count and misleads a jury. Likewise CLIENT_REDIRECT/SERVER_REDIRECT hops are not deliberate destinations. The from_visit column lets you reconstruct the chain: which prior visit referred the user to this one, so you can show that a typed search led to a clicked result led to a download.
Now convert a real timestamp. Suppose last_visit_time = 13355054062000000. On disk that 64-bit value is stored little-endian:
visits.visit_time = 13355054062000000 (decimal)
= 0x002F7259D5525780
little-endian on disk: 80 57 52 D5 59 72 2F 00
Convert: 13355054062000000 / 1,000,000 = 13,355,054,062 (seconds since 1601)
13,355,054,062 - 11,644,473,600 = 1,710,580,462 (Unix seconds)
1,710,580,462 → 2024-03-16 09:14:22 UTC
That instant — Saturday 2024-03-16 09:14:22 UTC — is, not by coincidence, the last moment in jrivera's live history table and the exact second CCleaner finished in Chapter 16. You can do the same conversion in SQL so you never copy a giant integer by hand:
-- Chrome/Edge: WebKit microseconds since 1601 → readable UTC
SELECT datetime(last_visit_time/1000000 - 11644473600, 'unixepoch') AS last_visit_utc,
url, title, visit_count, typed_count
FROM urls
ORDER BY last_visit_time DESC
LIMIT 25;
And the same from the command line against a copy (note -readonly):
# Query a COPY of History read-only; -wal must be alongside it to see recent rows.
sqlite3 -readonly ./work/History "
SELECT datetime(v.visit_time/1000000 - 11644473600,'unixepoch') AS visit_utc,
(v.transition & 255) AS core_transition,
u.url, u.title
FROM urls u JOIN visits v ON u.id = v.url
ORDER BY v.visit_time DESC LIMIT 40;"
The downloads table deserves its own attention: it records target_path (where the file was saved), tab_url and the referring chain (downloads_url_chains), received_bytes/total_bytes, and start/end times in WebKit. A download row proves a file came onto the machine from a specific URL at a specific time — central to malware-delivery and contraband cases alike. keyword_search_terms ties a urls row to the search phrase that produced it, giving you the user's exact words.
Cookies — sessions, tracking, and proof of login
Default\Network\Cookies (relocated from Default\Cookies in recent Chrome/Edge builds) is a SQLite database whose cookies table records, for every cookie: host_key (the domain), name, value or encrypted_value, path, creation_utc, expires_utc, last_access_utc, and the is_secure/is_httponly/is_persistent flags — all times in WebKit.
Cookies answer questions history cannot. A session cookie (no expiry, is_persistent = 0) for mail.google.com with a recent last_access_utc is evidence the user had an active logged-in session — not merely that they visited the domain. Persistent authentication and tracking cookies reveal which accounts and which third-party trackers followed the user. The last_access_utc is a precise "this account was used at this time" marker that frequently corroborates a history gap.
Cookie values are not plaintext on modern Chromium. Since Chrome 80 the encrypted_value blob is AES-256-GCM, prefixed with the ASCII tag v10 or v11, with a 12-byte nonce and a 16-byte authentication tag; the AES key itself sits in the Local State JSON file under os_crypt.encrypted_key, base64-encoded and then DPAPI-protected to the Windows user. To decrypt offline you therefore need the user's DPAPI master key, which in turn needs the user's logon password or the domain DPAPI backup key. The mechanics of DPAPI and offline credential decryption belong to Chapter 29 — Encrypted Device Forensics; here, simply recognize the v10/v11 prefix and know what it costs to read past it.
Login Data — saved passwords
Default\Login Data is SQLite; its logins table holds origin_url, action_url, username_value, password_value (an encrypted BLOB), date_created, date_last_used, date_password_modified, times_used, and blacklisted_by_user. The encryption scheme is identical to cookies — v10/v11 AES-256-GCM keyed from Local State, itself DPAPI-bound. Even without decrypting the passwords, the table is evidence: it enumerates every site for which the user saved a credential, the username for each, when it was created, and when it was last used. "This account at this bank, this webmail, this competitor's portal, last used on this date" can be a finding on its own.
Login Data → logins (decryption NOT required to read this much)
origin_url username_value date_last_used (WebKit→UTC)
https://portal.competitor-co.com/ jrivera@gmail.com 2024-03-15 21:08:55
https://www.dropbox.com/login jrivera@gmail.com 2024-03-16 08:49:12
https://mail.google.com/ j.rivera.personal 2024-03-16 09:06:41
Web Data — autofill, profiles, and card metadata
Default\Web Data (SQLite) stores form autofill. The autofill table records every name/value pair the user typed into web forms — search boxes, login names, anything — with date_created, date_last_used, and a use count. autofill_profiles and its companion tables hold structured addresses, full names, email addresses, and phone numbers the user saved for one-click form filling. credit_cards stores card metadata (name on card, expiry, last-modified) with the number itself encrypted. For attribution this database is gold: it often contains the user's real name, home address, personal email, and phone number, typed in their own session — strong evidence tying the physical person to the account, and exactly the kind of identity anchor Chapter 20 — Photo, Video, and Document Forensics and the eventual report will lean on.
Bookmarks, Top Sites, Favicons, Shortcuts, and Sessions
The secondary stores fill in motive and persistence:
Bookmarksis a JSON file (with aBookmarks.bakbackup), and each entry carries adate_addedin WebKit microseconds stored as a string. Bookmarks are deliberate — a saved competitor URL is harder to dismiss as accidental than a single visit.Favicons(SQLite) caches site icons keyed by URL. Because favicons are fetched and cached on visit, a favicon for a domain can prove the site was visited even after history was cleared — the user wipedHistorybut forgotFavicons. The same logic applies toTop Sites(thumbnails of frequent destinations) andShortcuts(the omnibox autocomplete database, which records what the user typed and which suggestion they chose — pure intent).Sessions\holdsSession_*andTabs_*files in the SNSS format (4-byte signatureSNSS), recording the tabs open at the last run. These can reconstruct what was on screen — and, as the private-browsing section explains, can occasionally capture an incognito session that ended in a crash.
Tool Tip. Do not hand-query forty tables across five databases for a first pass. Hindsight (a free, open-source tool) parses an entire Chromium profile — history, downloads, cookies, autofill, login metadata, extensions, and more — into a single timestamped, transition-decoded report, and it understands the WebKit epoch and WAL. NirSoft's BrowsingHistoryView ingests Chrome, Edge, Firefox, and IE at once for triage. Use them to orient, then confirm load-bearing findings with your own read-only SQL against the raw database, so you can testify to the underlying data and not merely to a tool's summary. The tool landscape is surveyed in Appendix C — Tool Reference and Chapter 36 — The Forensic Toolkit.
Firefox: places.sqlite, cookies.sqlite, and formhistory
Firefox stores the same evidence with a different schema and a different epoch — a clean illustration of the fourth theme, technology changes, principles don't. Its timestamps are PRTime: microseconds since the Unix epoch (1970-01-01 UTC), not the 1601 base Chrome uses. Beware: Firefox is inconsistent across columns, and that inconsistency has tripped up many examiners.
The marquee database is places.sqlite, which combines history and bookmarks:
moz_places one row per URL
id, url, title, rev_host (host reversed, for indexing), visit_count, typed,
last_visit_date (PRTime µs), frecency, hidden
moz_historyvisits one row per visit
id, from_visit, place_id (→ moz_places.id), visit_date (PRTime µs),
visit_type, session
moz_bookmarks bookmarks/folders tree (dateAdded, lastModified in PRTime µs)
moz_inputhistory text typed into the address bar, with a use count
The visit join and conversion:
-- Firefox: PRTime is microseconds since 1970, so just /1000000 then unixepoch
SELECT datetime(h.visit_date/1000000, 'unixepoch') AS visit_utc,
h.visit_type, p.url, p.title, p.visit_count, p.typed
FROM moz_places p
JOIN moz_historyvisits h ON p.id = h.place_id
ORDER BY h.visit_date DESC;
Firefox's visit_type mirrors Chrome's transitions but with its own numbering — know it cold:
moz_historyvisits.visit_type
1 LINK 2 TYPED (intent) 3 BOOKMARK 4 EMBED (subframe)
5 REDIRECT_PERMANENT 6 REDIRECT_TEMPORARY 7 DOWNLOAD 8 FRAMED_LINK 9 RELOAD
cookies.sqlite (table moz_cookies) is where the unit trap bites. creationTime and lastAccessed are PRTime microseconds, but expiry is plain Unix seconds. Apply the wrong divisor and you will misdate a cookie by a factor of a million. State your conversion explicitly in your notes for every column.
moz_cookies — MIND THE UNITS
creationTime → microseconds since 1970 (datetime(creationTime/1000000,'unixepoch'))
lastAccessed → microseconds since 1970 (same)
expiry → SECONDS since 1970 (datetime(expiry,'unixepoch')) ← different!
formhistory.sqlite (table moz_formhistory: fieldname, value, timesUsed, firstUsed, lastUsed in PRTime µs) is Firefox's answer to Chrome autofill — every term the user typed into a form, including site search boxes. Saved passwords live in logins.json (encrypted entries) protected by the key database key4.db (SQLite; it replaced the older key3.db), unlocked by the user's primary password if one is set; like Chromium credentials, reading the plaintext is an encrypted-device exercise. Open tabs and recently closed tabs are in sessionstore.jsonlz4 and the sessionstore-backups\ folder — Mozilla's LZ4 wrapper, recognizable by its magic:
sessionstore.jsonlz4 (and any .mozlz4 / .jsonlz4 file)
00000000 6D 6F 7A 4C 7A 34 30 00 <4-byte decompressed size> <LZ4 block...>
└──── "mozLz40\0" ────┘
Decompress the LZ4 block (after the 8-byte magic and 4-byte size) to recover JSON listing every open and recently closed tab, including URLs and form contents — a rich record that, again, can preserve a session the user believed was gone.
Microsoft Edge and Internet Explorer
Modern Microsoft Edge is Chromium. Everything in the Chrome section applies unchanged — same History/Cookies/Login Data/Web Data SQLite databases, same WebKit epoch, same v10/v11 cookie encryption — only the path differs (…\Local\Microsoft\Edge\User Data\Default\). On a Windows image you will routinely find Edge present and used even when the user "prefers Chrome," because Edge is the default and gets opened by links, PDFs, and the occasional habit. Always check it.
Legacy Edge (EdgeHTML, pre-2020) and Internet Explorer are a different animal, and you will still meet them on older images and servers. Their browsing history, cache index, cookies, and download history live in a single ESE (Extensible Storage Engine, a.k.a. JET Blue) database:
C:\Users\<u>\AppData\Local\Microsoft\Windows\WebCache\WebCacheV01.dat (ESE database)
• a "Containers" table maps numeric container IDs to types (History, Cache, Cookies,
iedownload, DOMStore, …)
• each container's records carry FILETIME timestamps (100-ns ticks since 1601)
• older IE (and very old Firefox) used flat index.dat files instead
You cannot read an ESE database with sqlite3; it is a wholly different format. Parse WebCacheV01.dat with esedbexport from libesedb, with NirSoft's ESEDatabaseView, or let BrowsingHistoryView/Hindsight-class tools handle it. The IE/Edge-legacy timestamps are FILETIME, the same epoch as the registry and $Recycle.Bin you converted in Chapter 16 — divide by 10,000,000 and subtract 11,644,473,600 for Unix seconds. Note too that the registry's TypedURLs (covered in Chapter 16) is the IE/Edge-legacy parallel to a browser's typed-history and worth correlating.
Safari
Safari (macOS, iOS) keeps the same evidence in Apple's idioms — SQLite plus binary property lists plus a proprietary cookie format — and on a third epoch: Mac/Cocoa absolute time, seconds since 2001-01-01 UTC.
~/Library/Safari/History.db (SQLite)
history_items id, url, domain_expansion, visit_count
history_visits id, history_item (→ history_items.id), visit_time (Mac epoch),
title, load_successful, redirect_source/destination
~/Library/Safari/Bookmarks.plist (binary plist — bookmarks + Reading List)
~/Library/Safari/Downloads.plist (binary plist — download history)
~/Library/Cookies/Cookies.binarycookies (proprietary "cook" format)
~/Library/Caches/com.apple.Safari/ (cache)
The history join and the Mac-epoch conversion:
-- Safari: Mac/Cocoa time is SECONDS since 2001-01-01; add 978307200 to reach Unix
SELECT datetime(v.visit_time + 978307200, 'unixepoch') AS visit_utc,
i.url, v.title
FROM history_visits v
JOIN history_items i ON v.history_item = i.id
ORDER BY v.visit_time DESC;
Bookmarks, the Reading List (which records the URL and a preview, sometimes the full saved page, and the date added), and download history are binary plists — files beginning with the magic bplist00:
Bookmarks.plist / Downloads.plist (binary property list)
00000000 62 70 6C 69 73 74 30 30 ... bplist00 ...
└──── "bplist00" ────┘
Convert a binary plist to readable XML with Apple's plutil -convert xml1 (or read it directly with Python's plistlib, or a forensic plist viewer). Downloads.plist yields each downloaded file's source URL, save path, size, and timestamps; the Reading List can preserve content a user read but never "saved."
Safari cookies are the proprietary Cookies.binarycookies format, which begins with the four bytes cook:
Cookies.binarycookies
00000000 63 6F 6F 6B 00 00 00 02 ... "cook" + 4-byte page count (big-endian)
└ "cook" ┘
• organized into pages, each holding cookie records
• each record stores domain, name, path, value, flags, plus expiry and creation
as Mac/Cocoa time stored as 8-byte doubles
Parse it with a dedicated tool (BinaryCookieReader-class scripts, or AXIOM/Cellebrite/Autopsy modules); do not try to read it as text. The lesson across Chrome, Firefox, and Safari is the same: four formats, three epochs, one method — locate the store, copy it safely, parse the structure, convert the timestamp correctly, and correlate.
The cache: pages, images, and downloads the user never saved
History records where the user went; the cache records what they actually saw. To render pages quickly, browsers save copies of fetched resources — HTML, CSS, JavaScript, and crucially images, video, and other media — to a local cache. These files were written to disk by the browser automatically, often without any deliberate act by the user, and they persist after the page is closed. That makes the cache simultaneously one of the most evidentially powerful and one of the most legally delicate artifacts in this chapter.
Chromium uses a Simple Cache under Default\Cache\Cache_Data\: numbered block files (data_0, data_1, …) hold small entries, while large resources are stored as standalone files named f_######. An index (and index-dir\) maps cache keys (URLs) to entries. Firefox uses cache2: each entry is a file under cache2\entries\ named by the uppercase SHA-1 hash of the cache key (the URL), with the entry's metadata — the original URL, response headers, and fetch/expiration times — appended at the end of the file. Safari caches under ~/Library/Caches/com.apple.Safari/ with a Cache.db SQLite index.
Two recovery realities follow. First, because cached images are stored as ordinary JPEG/PNG/WebP/GIF data (sometimes inside a block file, sometimes as a bare f_###### file), you can carve them by file signature even when the cache index is corrupt or the entry was partially deleted — FF D8 FF for JPEG, 89 50 4E 47 for PNG, and the rest of the table in Appendix A, using the carving technique from Chapter 7 — File Carving. Second, the cache entry's metadata gives each recovered resource a source URL and a fetch time, so a carved image is not an anonymous blob — it is "this picture, fetched from this address, at this instant."
Recovery vs. Forensics. The cache is a textbook dual-use artifact. For 💾 recovery, it is a second chance at images a client viewed but never saved — a product photo, a receipt, a family snapshot opened in webmail and now lost — reconstructable straight from
Cache_Dataorcache2\entries. For 🔍 forensics, the same cached file, with its source URL and fetch time, places specific content on the machine at a specific moment. One examiner restores a memory; the other proves an event. The bytes are identical.Legal Note. Cache evidence forces the law to distinguish possession from automatic caching. In contraband cases the defense routinely argues that cached material arrived without the user's knowledge — a page loaded, the browser cached an image, the user never "possessed" anything. Courts assess this with technique you now command: was the URL TYPED (deliberate navigation) or merely a subframe/redirect? Are there matching search terms showing the user sought the content? Do download records, repeated visits (
visit_count), longvisit_duration, or saved files corroborate intent? Is the file in the cache only, or also in a user-chosen folder? You do not decide the legal question of knowing possession — but your precise, qualifier-aware reconstruction is what lets the court decide it correctly. Overstating "the user viewed this" from a singleAUTO_SUBFRAMEcache hit is exactly the error that gets findings excluded.Ethics Note. When the cache or history surfaces child-sexual-abuse material — anchor case #4 — your conduct is governed, not improvised. Stop and follow the protocol owned by Chapter 28 — Ethics: do not copy, transmit, or further view the material beyond what your authorized scope strictly requires; preserve in place; document hashes and locations clinically and without describing content; and discharge the mandatory-reporting duty (in the U.S., 18 U.S.C. §2258A and the obligations that flow to NCMEC). Guard your own well-being — secondary trauma is real and managed, not toughed out. This book's stance is fixed: procedure, law, and ethics only; never a description of the content itself.
Browser timestamps: a field guide to the epochs
You have now met three epochs in three browsers, plus FILETIME in the IE/Edge-legacy store. Misreading an epoch is the most common technical error in browser forensics and the easiest for opposing counsel to expose. Keep this table at your bench (it is reproduced in Appendix D):
EPOCH USED BY UNIT ZERO → UNIX SECONDS
WebKit / Chrome Chrome, Edge, Chromium, Brave, Opera micro- 1601-01-01 t/1e6 - 11644473600
(History, Cookies, Login Data, Web seconds
Data, Bookmarks date_added, downloads)
PRTime Firefox places/cookies(creation,last micro- 1970-01-01 t/1e6
Accessed)/formhistory seconds
Unix seconds Firefox moz_cookies.expiry; many JSON seconds 1970-01-01 t
Mac / Cocoa absolute Safari History.db, binarycookies, seconds 2001-01-01 t + 978307200
plists (float)
FILETIME IE / legacy Edge WebCacheV01.dat; DPAPI 100-ns 1601-01-01 t/1e7 - 11644473600
Two habits keep you honest. First, sanity-check every converted time against a known anchor — if a "visit" lands in 1970 or 2050, you used the wrong epoch or unit. Second, remember every stored time is in UTC, but the human acted in local time; convert to UTC for the database, then present in the suspect's local timezone (read from the OS configuration) in the narrative, and state both. Timezone and clock-skew issues are developed fully in Chapter 21 — Timeline Analysis; for now, never present a bare timestamp without saying which zone it is in.
Private/Incognito browsing: what it does and does NOT remove
No belief is more common among suspects, or more wrong, than "I used incognito, so there's nothing to find." Private browsing — Chrome/Edge Incognito, Firefox Private Windows, Safari Private — is a privacy feature aimed at other people who use the same browser profile. It was never designed to defeat forensics, and it does not.
What private mode actually does is narrow: it runs the session against an in-memory profile that is discarded when the last private window closes. During the session, history, cookies, cache, and form data are kept in RAM (or a temporary store) and are not written into the persistent profile databases you examined above. That is the whole of its protection. It deletes nothing that already existed, and it cannot control the many layers below and around the browser that record activity anyway.
PRIVATE BROWSING — what it leaves behind
LEAVES NO trace in the profile DBs: BUT these still capture the session:
History / urls / visits OS DNS cache (ipconfig /displaydns,
Cookies (persistent profile) Get-DnsClientCache) — unless secure
Cache (persistent profile) DNS / DoH bypassed the OS resolver
Autofill / typed history RAM while the browser runs — a memory
capture recovers URLs & page content
STILL persists from a private session: Pagefile.sys / hiberfil.sys / swapfile —
Bookmarks the user saves RAM paged to disk holds URL strings
Files the user downloads (+ the file's SRUM (SRUDB.dat) — per-app bytes sent/
Zone.Identifier / jump list / prefetch) received: the browser moved N bytes
Favicons in some builds Prefetch — proves the browser RAN
Proxy / firewall / IDS / NetFlow / ISP
logs — the network saw every request
Each of those survivors is a real recovery avenue. The OS DNS resolver cache holds the domains looked up during the session until the machine reboots or the cache ages out — ipconfig /displaydns or Get-DnsClientCache on a live system, captured during the Chapter 15 — Live Response and Triage step. The browser's process RAM contains the visited URLs and even rendered page content; a memory image analyzed per Chapter 22 — Memory Forensics routinely recovers an entire "private" session, and fragments of that RAM survive on disk in pagefile.sys and hiberfil.sys. SRUM — the System Resource Usage Monitor database C:\Windows\System32\sru\SRUDB.dat, a Windows artifact from Chapter 16 — logs bytes sent and received per application per user, so it can show chrome.exe transferred hundreds of megabytes during a window when "nothing happened." And the network never forgets: proxy logs, firewall logs, IDS/NetFlow, and ISP records sit entirely outside the suspect's control (Chapter 23 — Network Forensics).
# Live triage of internet-side traces that survive private browsing (run on the LIVE
# system during authorized live response — see Chapter 15 for order-of-volatility).
ipconfig /displaydns | Select-String "Record Name" # OS resolver cache (legacy view)
Get-DnsClientCache | Select-Object Entry, Data, TimeToLive # structured DNS cache
# SRUM and pagefile are collected as files for OFFLINE parsing (SrumECmd / strings):
# C:\Windows\System32\sru\SRUDB.dat (per-app network usage; ESE database)
# C:\pagefile.sys C:\hiberfil.sys (RAM remnants — carve for URL strings)
War Story. An employee accused of leaking pre-release financials swore he had "only used incognito." His persistent history was indeed clean. But the OS DNS cache, captured live, still listed the file-sharing domain he had resolved twenty minutes earlier; SRUM showed his browser had uploaded 1.2 GB during the lunch hour against a daily baseline of a few megabytes; and a memory image recovered the full upload URL and the document filenames straight from the browser's heap. Incognito had hidden the session from the next person to sit at his desk — and from no one else. The lesson is the third theme: every action leaves a trace, and turning off one recorder does not silence the other six.
Cloud-storage sync artifacts: Dropbox, Google Drive, OneDrive
The browser is one road off the machine; cloud sync clients are another, and they leave their own local databases. When a file lands in a synced folder, it leaves the device — which is precisely what an exfiltration investigation, or a recovery client who lost a laptop, needs to establish. These clients run outside the browser, but they are squarely "internet artifacts," and they corroborate (or contradict) the browser story.
Dropbox keeps its state under %LOCALAPPDATA%\Dropbox\instance1\ (and %APPDATA%\Dropbox\). Historically its databases — filecache.dbx, config.dbx, host.dbx, deleted.dbx — were SQLite and partly readable; modern Dropbox encrypts the .dbx files with SQLCipher, the key derived from machine identifiers and DPAPI, so plaintext parsing usually requires the live account or specialized tooling. But one file remains readable and immediately useful: info.json reveals the account email and the local path of the Dropbox folder — enough to prove a personal Dropbox was linked on a corporate machine, and to point you at the synced folder whose contents and slack you then examine.
Google Drive for Desktop stores its metadata under %LOCALAPPDATA%\Google\DriveFS\<account_id>\metadata_sqlite_db — a SQLite database whose items and stable_ids tables list synced file names, sizes, MIME types, trashed flags, and modification/viewed timestamps, with a local content_cache\ of file content. The older Backup-and-Sync client used …\Google\Drive\user_default\snapshot.db and sync_config.db. Either way you can enumerate what was synced to which Google account.
OneDrive keeps settings under %LOCALAPPDATA%\Microsoft\OneDrive\settings\Personal\ (and \Business1\) — <cid>.dat/<cid>.ini/global.ini files listing synced items and the account cid — plus the activity-rich OneDrive log files %LOCALAPPDATA%\Microsoft\OneDrive\logs\Personal\*.odl (and .odlgz, .odlsent). The .odl logs are obfuscated but parseable with published ODL tools and reconstruct file-level sync activity over time; HKCU\Software\Microsoft\OneDrive mirrors the configuration. The broader cloud picture — server-side logs, API acquisition, jurisdiction — is Chapter 31 — Cloud Forensics; here you are reading the local footprints the clients leave.
Recovery vs. Forensics. A cloud sync database is, for 💾 recovery, an inventory of what a dead machine had — feed the client the list of files that were synced to their Drive or OneDrive and they can re-download every one, no carving required. For 🔍 forensics, that same
itemstable orinfo.jsonis proof that corporate data was placed into a personal cloud account, when, and under which identity. One reads the catalog to restore; the other reads it to prove a breach.
Recovering deleted browser history
Suspects clear history; clients lose it to a failing drive or a careless reinstall. Either way, the first theme governs: deleted is not destroyed, and browser history is one of the most recoverable artifacts there is, in three layers.
Layer 1 — the WAL. The most recent activity often sits in <database>-wal, uncheckpointed. If you preserved the trio, you already have it; simply opening the copy read-only surfaces those rows. Examiners who grabbed only History miss exactly the rows a suspect generated last.
Layer 2 — deleted rows inside a live database. Clearing history issues SQL DELETEs, which free cells without erasing bytes; the old rows persist in freelist pages and in the unallocated tail of B-tree pages until a VACUUM. Tools that parse SQLite internals recover them: undark and walitean (which also mines the WAL), FQLite, and the Epilog/SQLite Forensic Explorer-class commercial parsers. They walk the freelist and page slack and reconstruct deleted records the standard SQL interface cannot see.
Layer 3 — a deleted database file. If the database file itself was deleted (a cleaner removed History, or the profile was wiped), recover the file first using logical recovery from the file system's metadata (Chapter 6 — Logical Recovery) or by carving for the SQLite header signature (Chapter 7 — File Carving, signature in Appendix A). Then apply Layers 1–2 to the recovered file. Cached images, likewise, carve straight out of Cache_Data/cache2 slack by their JPEG/PNG signatures.
# Illustrative: recover deleted rows from a cleared Chrome History (work on a COPY).
# 1) carve the database file from unallocated if it was deleted (SQLite magic):
foremost -t all -i unallocated.bin -o carved/ # or scalpel; see Appendix H
# 2) parse freelist + page slack for deleted history rows:
undark -i ./work/History --freespace > deleted_rows.txt
# 3) mine the WAL specifically for recent, uncheckpointed visits:
walitean ./work/History ./work/History-wal # reconstructs live + WAL rows
Limitation. Recovery from a SQLite database is excellent until a
VACUUMruns. AVACUUM(which some "optimize"/"clean" tools trigger, and which browsers occasionally perform) rewrites the file compactly and overwrites the freed cells, destroying the deleted rows for good. Likewise, continued browsing reuses freed pages and overwrites old records. The recoverability window is real but finite — which is one more reason to image early (theme two) and stop the moment you have what scope allows (theme five).
Tool demonstration: a Chromium profile, parsed two ways
A defensible examination uses an automated parser for breadth and your own queries for the load-bearing facts. Here is a typical pass over jrivera's extracted Edge/Chrome profile.
First, orient with Hindsight, which ingests the whole profile, decodes transitions, converts WebKit time, and reads the WAL:
$ python hindsight.py -i ".\work\User Data\Default" -f xlsx -o jrivera_browser
Hindsight — Chromium browsing analysis
Detected browser: Microsoft Edge (Chromium) Profile: Default
History ......... 9,214 URLs / 21,778 visits (WAL applied: +63 rows)
Downloads ....... 142 records
Cookies ......... 3,401 (encrypted_value v10/v11 — values not decrypted)
Autofill ........ 318 entries
Login Data ...... 47 saved logins (metadata only)
Output: jrivera_browser.xlsx (timeline, transition-decoded, UTC)
Then confirm the specific finding with read-only SQL you can testify to — the deliberate, TYPED visits to personal cloud services on the Saturday morning, with their qualifiers intact:
$ sqlite3 -readonly ./work/History
sqlite> SELECT datetime(v.visit_time/1000000-11644473600,'unixepoch') AS utc,
...> (v.transition & 255) AS core, u.url
...> FROM urls u JOIN visits v ON u.id=v.url
...> WHERE u.url LIKE '%drive.google.com%' OR u.url LIKE '%mail.google.com%'
...> OR u.url LIKE '%dropbox.com%'
...> ORDER BY v.visit_time;
2024-03-16 08:49:12|1|https://www.dropbox.com/login (core 1 = TYPED)
2024-03-16 08:51:03|1|https://drive.google.com/drive/my-drive (core 1 = TYPED)
2024-03-16 08:58:40|7|https://drive.google.com/...upload (core 7 = FORM_SUBMIT)
2024-03-16 09:06:41|0|https://mail.google.com/mail/u/1/ (core 0 = LINK)
Tool Tip. Run at least two independent tools against any finding that will appear in your report, and confirm both against the raw database. "Hindsight, BrowsingHistoryView, and my own SQL all return the same TYPED visit to
drive.google.comat 08:51:03 UTC" is a sentence that survives cross-examination; a single tool's CSV is a sentence opposing counsel will attack. Tool validation is a Daubert expectation — see Chapter 27 — Expert Testimony.
Worked example: the cloud-upload trail
Return to anchor case #2. From Chapter 16 you already proved the USB copy and the CCleaner run at Saturday 09:14:22. Counsel now asks the browser-side question: did jrivera also exfiltrate the CAD files to a personal cloud account from this machine? You proceed artifact by artifact, every step a technique this chapter taught.
1 — Locate and preserve the profile. You extract …\Microsoft\Edge\User Data\ (Edge is the corporate default and was used) from WS-ENG-04.E01, capturing Default\History, History-wal, History-shm, Default\Network\Cookies, Default\Login Data, Default\Web Data, and the root Local State. Each file is hashed into the chain-of-custody log; you work only on the copies, opened read-only.
2 — The live history confirms the gap. Querying History (WAL applied), the most recent surviving row is 2024-03-16 09:14:22 UTC — and then nothing. The history does not taper; it stops, to the second, at the moment CCleaner finished. A clean cutoff is not normal use; it is a wipe, and its timestamp is itself evidence (theme three: the absence is the trace).
3 — Recover the cleared session. You run undark against the History copy's free space and walitean across History + History-wal. The freelist and WAL yield the deleted Saturday-morning rows the cleaner thought it had destroyed:
RECOVERED (deleted) Chrome/Edge history rows — WS-ENG-04, user jrivera (UTC)
visit_time core transition url
2024-03-16 08:47:55 TYPED https://www.google.com/search?q=how+to+permanently
+delete+browser+history
2024-03-16 08:49:12 TYPED https://www.dropbox.com/login
2024-03-16 08:51:03 TYPED https://drive.google.com/drive/my-drive
2024-03-16 08:58:40 FORM_SUBMIT https://drive.google.com/...upload (12 items)
2024-03-16 09:06:41 LINK https://mail.google.com/mail/u/1/#sent
2024-03-16 09:11:20 TYPED https://drive.google.com/drive/my-drive
The search at 08:47:55 — how to permanently delete browser history — is the user, in his own words, planning the wipe minutes before he performed it. The TYPED visits to dropbox.com and drive.google.com are deliberate navigations, not redirects or subframes; you verified the transition & 0xFF core values, so you can say "the user typed these addresses" and defend it.
4 — Corroborate the accounts. Login Data's logins table lists saved credentials for https://www.dropbox.com/login (username jrivera@gmail.com) and https://mail.google.com/ (j.rivera.personal) with date_last_used on the morning of 2024-03-16 — personal accounts on a corporate machine. Web Data autofill carries the same personal email and his home address, tying the physical person to the accounts.
5 — Corroborate the bytes. SRUM (SRUDB.dat, parsed with SrumECmd) shows the network-usage record for msedge.exe under jrivera's SID: the 08:00–09:00 and 09:00–10:00 buckets total roughly 490 MB sent, against a multi-week daily baseline of a few megabytes. The twelve .sldprt files staged on the thumb drive Friday total ~480 MB — the upload volume matches the data set. The live DNS cache, captured during triage, still listed drive.google.com and dropbox.com.
6 — Note what is absent, and why. There is no DriveFS or Dropbox sync database for a personal account on this machine — he uploaded through the browser rather than installing a sync client, plausibly to avoid leaving a local database. But the browser told on him anyway: the choice to avoid the sync client is consistent with the same track-covering intent as the CCleaner run, and you state it as a correlation, not a mind-read.
Assembled, the browser timeline slots cleanly into the Chapter 16 machine timeline:
WS-ENG-04 — BROWSER/INTERNET TIMELINE (UTC), user jrivera
Fri 19:04 Opened TurbineHousing_v7.sldprt from E: [Ch.16: LNK / Jump List]
Sat 08:47 Searched "how to permanently delete browser history" [recovered history]
Sat 08:49 TYPED dropbox.com/login (saved login jrivera@gmail.com) [history + Login Data]
Sat 08:51 TYPED drive.google.com/my-drive [recovered history]
Sat 08:58 FORM_SUBMIT drive.google.com upload — 12 items [recovered history]
Sat 08–10 msedge.exe sent ~490 MB (baseline few MB) [SRUM]
Sat 09:06 mail.google.com (#sent) [recovered history]
Sat 09:12 TurbineHousing_v7.sldprt + sibling → Recycle Bin [Ch.16: $I metadata]
Sat 09:14:22 CCleaner run #3; live history ends this second [Ch.16 + history gap]
Every line is sourced to an artifact, every artifact came from a hashed image, and the browser evidence the suspect "deleted" is what makes the upload provable. That is the chapter's thesis in one timeline.
Ethics Note. You can now state, with sourcing, that personal cloud accounts were used to upload a data set matching the staged files, minutes before a search-documented history wipe. You cannot state that the files uploaded were the trade secrets unless their names or hashes appear in the upload records — say what the artifacts show and only that. "≈490 MB matching the staged volume was uploaded to a personal Drive account" is a finding; "he stole the turbine design via Google Drive" is a conclusion for counsel and the trier of fact. Keep findings and inferences rigorously separate, as Chapter 26 — The Forensic Report requires.
Common mistakes
- Copying
Historywithout-waland-shm. The most recent — and often most relevant — activity lives in the WAL. Grab the whole trio, or analyze a stale database and miss the last hours. - Letting the SQLite engine checkpoint the evidence. A default connection can fold the WAL into the main file and change both hashes. Work on copies, open read-only, set
PRAGMA query_only, and never point a live tool at the original. - Using the wrong timestamp epoch. WebKit (1601, µs), PRTime (1970, µs), Mac/Cocoa (2001, s), and FILETIME (1601, 100-ns) are not interchangeable. A misconverted time is a wrong date in court. Sanity-check every conversion; cite the epoch in your notes.
- Confusing a visit with a deliberate visit.
AUTO_SUBFRAME,CLIENT_REDIRECT, andSERVER_REDIRECTare not user navigations. Decodetransition & 0xFFand the qualifier bits before you claim "the user went to this site," and never inflatevisit_countwith subframe and redirect rows. - Treating "in the cache" as "the user viewed it" — or as "the user possessed it." Caching can be automatic. Establish intent with TYPED navigation, search terms, downloads, repeat visits, and dwell time; let the cache corroborate, not carry, the claim.
- Assuming incognito means nothing exists. Private browsing only avoids the persistent profile. DNS cache, RAM/pagefile, SRUM, and network logs routinely reconstruct the session.
- Reading Firefox
moz_cookies.expiryas microseconds.creationTime/lastAccessedare µs since 1970;expiryis seconds. Mixing them misdates cookies by a factor of a million. - Attributing a profile to the wrong person. Multi-profile and multi-user machines demand that you name the on-disk profile folder and the OS user, not the friendly UI name, behind every finding.
- Forgetting Edge on a "Chrome user's" machine (and vice versa). The default browser gets used by links and PDFs even when the user prefers another. Examine every installed browser's profile.
Limitations: knowing when to stop
Browser artifacts are rich, not omniscient, and a professional report states their limits as plainly as their findings.
A visit record proves the browser requested a URL; it does not prove a human was watching. Background tabs reload, prefetch and preconnect fetch links the user never clicked, extensions navigate via the FROM_API qualifier, and a page can pull dozens of subframes. Always separate deliberate (TYPED, FORM_SUBMIT, LINK with a real from_visit chain) from automatic activity, and say which you are counting. visit_duration helps but is not proof of attention.
Timestamps depend on the machine's clock, which can be wrong, deliberately changed, or in a different timezone than you assume; cross-check browser times against server-side records, event logs, and other artifacts, and resolve skew explicitly per Chapter 21. Attribution is bounded by who had access: a shared account, an unlocked machine, a borrowed laptop, or malware acting through the browser can all generate activity the named user did not. The presence of a profile proves the profile did something, which is not identical to "this person did it" — you build the person-link from corroborating identity artifacts (autofill, saved logins, photos/EXIF, badge and CCTV records) and state it as a chain of inference.
Sync changes the meaning of "on this machine." Chrome/Edge/Firefox account sync replicates history, bookmarks, and passwords across every signed-in device, so a row in this profile may record activity that happened on the user's phone or home PC, not this workstation. Note when sync is enabled and qualify accordingly. Encrypted values (cookie/password v10/v11, Firefox key4.db, SQLCipher .dbx) may be unreadable without the user's DPAPI key or primary password; you can often enumerate which sites and when without ever decrypting the secret, and you should report exactly that boundary. And the recovery window for deleted rows closes at the next VACUUM or page reuse.
Finally, the hardest limit: a disciplined user on a managed network using browser sync off, secure DNS, a privacy-respecting VPN, and full-disk encryption you cannot break (Chapter 29) can leave you with genuinely insufficient browser evidence. The professional answer, theme five made concrete, is to write it: "the available browser artifacts are insufficient to determine whether X occurred." That is a finding, not a failure — and it is far safer on cross-examination than a conclusion the data cannot carry.
Progressive project: recover the browser/internet history for the case
Continue building your Forensic Case File (introduced in Chapter 5, acquired in Chapters 14–15, layered with Windows/macOS/Linux artifacts in Chapters 16–17). This chapter you add the internet layer.
- Extract and preserve every browser profile on your image. For each Chromium profile, capture
History/Cookies/Login Data/Web Datawith their-wal/-shmcompanions and the rootLocal State; for Firefox,places.sqlite/cookies.sqlite/formhistory.sqlite(+-wal) andlogins.json/key4.db; for Safari,History.db, the plists, andCookies.binarycookies; for any IE/legacy Edge,WebCacheV01.dat. Hash every file into your chain-of-custody worksheet (Appendix F). - Parse and convert. Run an automated parser (Hindsight / BrowsingHistoryView / ESEDatabaseView for ESE) for breadth, then confirm load-bearing rows with your own read-only SQL, converting each store's epoch correctly and decoding Chromium transitions / Firefox visit types. Cross-check at least one key finding with a second tool.
- Recover what was deleted. Mine the WAL and the SQLite freelist/page-slack for cleared history; carve cached images by signature; if a database file was deleted, recover or carve the file first, then its rows. Note the recoverability window.
- Reach into the internet side. Pull the OS DNS cache (if a live image exists), parse SRUM for per-app bytes, and enumerate cloud-sync footprints (Dropbox
info.json, DriveFSmetadata_sqlite_db, OneDrive.odl/settings) to test for cloud exfiltration. Treat private-browsing claims by checking these layers explicitly. - Answer and source four questions for your case, each citing the exact artifact (store + table + tool): (a) What URLs were deliberately visited (TYPED/FORM_SUBMIT), and when? (b) What was downloaded or uploaded, and to/from where? (c) Which accounts were logged in, per cookies/login metadata? (d) Is there deleted or private-mode activity recoverable from WAL, freelist, cache, DNS, SRUM, or memory?
- Add a sourced browser/internet timeline to the case file, in UTC with local-time annotation, every entry tied to an artifact and flagged with any limitation (visit-vs-presence, sync ambiguity, undecrypted values). You will merge it into the master timeline in Chapter 21 and fold it into the report in Chapter 26. Save all outputs into the case-file folder; the capstone in Chapter 38 assembles everything.
Summary
The browser profile is the most revealing record on most machines, and this chapter taught you to read it across every major platform. You learned that almost everything lives in SQLite — recognizable by the SQLite format 3\0 magic — and that handling it correctly is the whole game: preserve the db/-wal/-shm trio together because recent activity hides in the WAL, work on copies opened read-only so the engine never checkpoints your evidence, and remember that cleared rows persist in freelist and page slack because deleted is not destroyed. You walked Chromium (Chrome, Edge, and the rest) through its four core databases — History (with the all-important transition field that separates TYPED intent from incidental subframes and redirects), Cookies (session vs. persistent, with v10/v11 AES-GCM values keyed from Local State), Login Data, and Web Data — plus bookmarks, favicons, shortcuts, and sessions. You did the same for Firefox (places.sqlite, cookies.sqlite, formhistory.sqlite, with PRTime and its unit traps), Edge/IE legacy (the WebCacheV01.dat ESE database in FILETIME), and Safari (History.db, binary plists, and Cookies.binarycookies on the Mac/Cocoa epoch) — four formats, three epochs, one method, the fourth theme in action. You learned the cache as both a recovery resource and the legally delicate question of automatic caching versus knowing possession, handled clinically where anchor #4 touches it. You internalized the epoch field guide so you never misdate evidence, and you dismantled the private-browsing myth: incognito only skips the persistent profile, while DNS cache, RAM and pagefile, SRUM, and network logs reconstruct the session anyway. You added cloud-sync footprints (Dropbox, Google Drive, OneDrive) as exfiltration evidence, and you recovered deleted history from the WAL, the freelist, and carved files. Finally, you resumed anchor case #2 and recovered the very upload session the suspect believed he had wiped — search-documented intent, TYPED navigations to personal cloud accounts, ~490 MB of matching uploads in SRUM — proving once more that the data a person deletes is often the data that convicts them, while you held every finding to exactly what the artifacts support.
You can now: - Locate, preserve (the
db/-wal/-shmtrio), and safely query browser SQLite databases read-only without altering the evidence, and recognize the SQLite,mozLz40,bplist00, andcooksignatures. - Parse ChromiumHistory/Cookies/Login Data/Web Data, decodetransitioncore types and qualifiers, and convert WebKit, PRTime, Mac/Cocoa, and FILETIME timestamps correctly. - Examine Firefox, Safari, and IE/legacy-Edge stores, and extract autofill, saved-login metadata, downloads, bookmarks, and session/tab data across browsers. - Distinguish cache-as-evidence from automatic caching, and recover cached media and deleted history rows from the WAL, the SQLite freelist, and carved database files. - Explain and prove what private/incognito browsing does not remove — DNS cache, RAM/pagefile, SRUM, network logs — and read Dropbox/Drive/OneDrive sync artifacts as exfiltration evidence. - Build a sourced, epoch-correct, transition-aware browser/internet timeline and state each finding with its limitations (visit-vs-presence, clock skew, sync ambiguity, undecrypted values).
What's next. Chapter 19 — Email, Chat, and Social Media Forensics — follows the conversation off the web page and into the inbox and the DM: PST/OST and mbox/EML stores, webmail and message headers, and the chat and social-platform artifacts where so much modern intent is recorded — proof, again, that technology changes, principles don't.
Practice in exercises.md, test yourself with the quiz, apply it in the case studies, review the key takeaways, and go deeper with further reading.