browserforensics
Back to all articles

Safari History.db forensics

2026-06-21 · 3 min

Safari's History.db is a plain SQLite database. Two tables carry the timeline — history_items holds one row per URL, history_visits holds one row per visit — and you join them on history_item = id. Get the join and the timestamp epoch right and the rest is straightforward.

Where it lives

/Users/<user>/Library/Safari/History.db
/Users/<user>/Library/Safari/History.db-wal
/Users/<user>/Library/Safari/History.db-shm

Always grab the -wal and -shm sidecars alongside the main file. Recent visits sit in the write-ahead log before they are checkpointed into the database; acquire History.db alone and you miss them. See Safari history file locations for the rest of the Safari artifact set.

history_items

One row per distinct URL. The columns that matter:

ColumnMeaning
idPrimary key — joined by history_visits.history_item
urlThe full URL
domain_expansionThe host, used for autocomplete
visit_countTotal visits across all time
daily_visit_countsPer-day visit tallies (blob)

This is the aggregated view. For timeline work you want the visits table, not these rolled-up counts.

history_visits

One row per individual visit. This is where the timeline lives:

ColumnMeaning
idVisit primary key
history_itemFK → history_items.id
visit_timeMac absolute time of the visit (float64)
titlePage title at visit time
load_successful0 if the load failed
http_non_getNon-GET request flag
redirect_sourceVisit id this visit was redirected from
redirect_destinationVisit id this visit redirected to
originHow the visit was initiated

To get a usable record — URL, title, and time together — join the two tables:

SELECT i.url, v.title, v.visit_time, v.load_successful
FROM history_visits v
JOIN history_items i ON i.id = v.history_item
ORDER BY v.visit_time;

Timestamps

visit_time is Mac absolute time — seconds (often fractional, stored as float64) since 2001-01-01 00:00:00 UTC. Add the offset to reach Unix:

unix_seconds = visit_time + 978307200

It is UTC, with no embedded timezone. Keep the timeline in UTC and label any local rendering. See browser timestamp formats for the full epoch reference.

Following redirects

redirect_source and redirect_destination reference other rows in history_visits by id. Walk the chain to reconstruct how a user landed on a page — a shortener, an ad click, an SSO bounce — rather than treating each row as an independent visit. This distinguishes a deliberate navigation from a passive redirect, which matters for intent.

load_successful and friends

load_successful = 0 flags a visit that never completed — a typo, a dead link, a blocked host. Failed loads still prove the URL was requested, which is often the point. Pair this with http_non_get to spot form submissions and other non-GET activity.

Acquisition notes

  • Full Disk Access is required for a user-mode tool to read Safari's data; otherwise image the volume or use a FileVault-aware acquisition.
  • Capture the -wal sidecar for the most recent visits — they may not be in the main file yet.
  • BrowserForensics parses History.db directly in the browser (URL, title, visit_count, visit_time), all client-side, so the database never leaves the analyst's machine.
  • Cross-reference with cookies for the same hosts; see Safari binarycookies format.

Further reading