5 minute read

Tracing Safari Navigation History in the iOS Simulator with Forensic Techniques

Goal

I want to boot an iOS simulator on macOS and find out where a page opened in a new Safari tab navigated to.

Normally you would debug this with Safari devtools, but when a link opens a new browser tab, devtools can’t show you the navigation. To capture it you would need devtools already attached to that Safari tab — and you can’t attach devtools to a tab that doesn’t exist yet.

Tools

Safari devtools is out, but the Xcode developer tools are not. We’ll use xcrun simctl from the command line instead.

xcrun

xcrun is the tool that lets you invoke Xcode developer tools from the command line.

References

simctl

simctl is the command line utility Xcode provides for controlling simulators.

To confirm it works, let’s stream the logs of the running simulator.

xcrun simctl spawn booted log stream --predicate 'process == "MobileSafari"' --style compact

Now let’s list the simulators that are currently running.

xcrun simctl list devices booted

The output looks roughly like this. What matters is the UDID of the instance marked Booted — in the example below, 077928C7-026B-4D15-AC8C-C992B910D0C4. Everything about that simulator instance is looked up by this UDID.

ys-m4pro@youngseonui-MacBookPro Safari % xcrun simctl list devices booted
== Devices ==
-- iOS 26.2 --
-- iOS 26.4 --
-- iOS 26.5 --
    iPhone 17 (077928C7-026B-4D15-AC8C-C992B910D0C4) (Booted)

References

Safari History.db

As of iOS 26.5, iOS Safari stores history and similar data in SQLite files. There’s no official documentation for this, but the forensics pioneers have already dug most of it up. Reading Plaso’s parser is a good place to start.

Source code for plaso.parsers.sqlite_plugins.safari

So where is Safari’s History.db? This is where the simulator instance UDID from above comes in. Head to this directory:

~/Library/Developer/CoreSimulator/Devices/[UDID from xcrun simctl]/data/Library/Safari

There’s all kinds of data in here.

ys-m4pro@youngseonui-MacBookPro Safari % ls -lh
total 11240
-rw-r--r--  1 ys-m4pro  staff   139K  Aug  3 16:32 AutoFillQuirks.plist
-rw-r--r--  1 ys-m4pro  staff   4.0K  Jun  8 11:34 Bookmarks.db
-rw-r--r--  1 ys-m4pro  staff    32K  Aug  3 16:33 Bookmarks.db-shm
-rw-r--r--  1 ys-m4pro  staff   515K  Jun  8 11:36 Bookmarks.db-wal
-rw-r--r--  1 ys-m4pro  staff   4.0K  Jun  8 11:33 BrowserState.db
-rw-r--r--  1 ys-m4pro  staff    32K  Aug  3 16:34 BrowserState.db-shm
-rw-r--r--  1 ys-m4pro  staff    60K  Jun  8 11:33 BrowserState.db-wal
-rw-r--r--@ 1 ys-m4pro  staff    40K  Jun  8 11:34 CloudTabs.db
-rw-r--r--  1 ys-m4pro  staff    32K  Aug  3 17:24 CloudTabs.db-shm
-rw-r--r--  1 ys-m4pro  staff     0B  Jun  8 11:34 CloudTabs.db-wal
-rw-r--r--  1 ys-m4pro  staff     0B  Jun  8 11:34 com.apple.Bookmarks.lock
-rw-r--r--@ 1 ys-m4pro  staff   4.0K  Jun  8 11:34 History.db
-rw-r--r--@ 1 ys-m4pro  staff    32K  Aug  4 09:00 History.db-shm
-rw-r--r--@ 1 ys-m4pro  staff   3.6M  Aug  4 09:24 History.db-wal
-rw-r--r--  1 ys-m4pro  staff   208K  Aug  3 20:39 SafariTabs.db
-rw-r--r--  1 ys-m4pro  staff    32K  Aug  3 16:32 SafariTabs.db-shm
-rw-r--r--  1 ys-m4pro  staff   258K  Aug  4 09:24 SafariTabs.db-wal

The file we want is History.db. If you want to keep a snapshot of the database, save History.db, History.db-wal, and History.db-shm together. The three are one set.

sqlite3

Reading the data is straightforward — just open History.db with sqlite3. Prefer the -readonly option; reading can modify the database.

ys-m4pro@youngseonui-MacBookPro Safari % sqlite3 -readonly History.db
SQLite version 3.51.0 2025-06-12 13:14:41
Enter ".help" for usage hints.

Let’s list the tables inside History.db.

sqlite> .tables
history_client_versions  history_items            history_tombstones
history_event_listeners  history_items_to_tags    history_visits
history_events           history_tags             metadata

You can also inspect the schema of a specific table.

sqlite> .schema history_visits
CREATE TABLE history_visits (id INTEGER PRIMARY KEY AUTOINCREMENT,history_item INTEGER NOT NULL REFERENCES history_items(id) ON DELETE CASCADE,visit_time REAL NOT NULL,title TEXT NULL,load_successful BOOLEAN NOT NULL DEFAULT 1,http_non_get BOOLEAN NOT NULL DEFAULT 0,synthesized BOOLEAN NOT NULL DEFAULT 0,redirect_source INTEGER NULL UNIQUE REFERENCES history_visits(id) ON DELETE CASCADE,redirect_destination INTEGER NULL UNIQUE REFERENCES history_visits(id) ON DELETE CASCADE,origin INTEGER NOT NULL DEFAULT 0,generation INTEGER NOT NULL DEFAULT 0,attributes INTEGER NOT NULL DEFAULT 0,score INTEGER NOT NULL DEFAULT 0);
CREATE INDEX history_visits__last_visit ON history_visits (history_item, visit_time DESC, synthesized ASC);
CREATE INDEX history_visits__origin ON history_visits (origin, generation);

Action

Referring to Source code for plaso.parsers.sqlite_plugins.safari, let’s pull just the 10 most recent navigations. visit_time is in seconds since the Apple reference date (2001-01-01 GMT), so add +978307200 to convert it to Unix epoch.

sqlite> .mode box
sqlite> select v.id,
   ...>        datetime(v.visit_time+978307200,'unixepoch','localtime') t,
   ...>        i.url
   ...> from history_visits v
   ...> join history_items i on i.id = v.history_item
   ...> order by v.visit_time desc, v.id desc
   ...> limit 10;
┌────┬─────────────────────┬──────────────────────────────────────────────────────────────┐
│ id │          t          │                             url                              │
├────┼─────────────────────┼──────────────────────────────────────────────────────────────┤
│ 85 │ 2026-08-04 11:08:20 │ https://m.epic.ai.kr/?utm_source=naver&utm_medium=brandsearc │
│    │                     │ h&utm_campaign=MO&utm_id=search                              │
├────┼─────────────────────┼──────────────────────────────────────────────────────────────┤
│ 84 │ 2026-08-04 11:08:20 │ https://m.epic.ai.kr/?utm_source=naver&utm_medium=brandsearc │
│    │                     │ h&utm_campaign=MO&utm_id=search                              │
├────┼─────────────────────┼──────────────────────────────────────────────────────────────┤
│ 83 │ 2026-08-04 09:24:16 │ https://m.epic.ai.kr/copilot/36b7f134-1581-4c83-90d1-df71482 │
│    │                     │ 0f1b8                                                        │
├────┼─────────────────────┼──────────────────────────────────────────────────────────────┤
│ 82 │ 2026-08-04 09:24:12 │ https://m.epic.ai.kr/copilot                                 │
├────┼─────────────────────┼──────────────────────────────────────────────────────────────┤
│ 81 │ 2026-08-03 17:20:01 │ https://m.epic.ai.kr/copilot/36b7f134-1581-4c83-90d1-df71482 │
│    │                     │ 0f1b8                                                        │
├────┼─────────────────────┼──────────────────────────────────────────────────────────────┤
│ 80 │ 2026-08-03 17:20:00 │ https://m.epic.ai.kr/copilot/conversations-list              │
├────┼─────────────────────┼──────────────────────────────────────────────────────────────┤
│ 79 │ 2026-08-03 17:19:58 │ https://m.epic.ai.kr/copilot                                 │
├────┼─────────────────────┼──────────────────────────────────────────────────────────────┤
│ 78 │ 2026-08-03 16:55:37 │ https://m.epic.ai.kr/?utm_source=naver&utm_medium=brandsearc │
│    │                     │ h&utm_campaign=MO&utm_id=search                              │
├────┼─────────────────────┼──────────────────────────────────────────────────────────────┤
│ 77 │ 2026-08-03 16:55:37 │ https://m.epic.ai.kr/?utm_source=naver&utm_medium=brandsearc │
│    │                     │ h&utm_campaign=MO&utm_id=search                              │
├────┼─────────────────────┼──────────────────────────────────────────────────────────────┤
│ 76 │ 2026-08-03 16:51:55 │ https://ad-creative.gfa.naver.com/widget/preview.html?creati │
│    │                     │ veId=6a703f4a8965ead565d1266f&fullwidth                      │
└────┴─────────────────────┴──────────────────────────────────────────────────────────────┘

The top row is the most recent. Read upward from the bottom and you get the browser’s navigation history leading up to the current page. In the result above, id 76 → 77 is the case where a link opened a new browser tab and navigated.

Caveats

1. Whether a new tab was opened

Unfortunately, History.db keeps no record of whether a new tab was involved. Browser tabs are recorded in SafariTabs.db, but there’s no key that ties it back to History.db.

2. Sometimes you get 2 rows, sometimes 1

Here’s why.

When a SPA initializes, react-router calls replaceState. At that moment:

  • History.db → adds one row (ids 77–78 and 84–85 in the example above)
  • browser history API stack → adds nothing

So the entries on the browser history API stack and the rows in History.db cannot be mapped one to one.

A document navigation or an in-app SPA navigation adds exactly one row to History.db and one entry to the browser history API stack.

Summary

When browser devtools can’t get you there, forensic techniques can.

20260804

EOD

Tags:

Categories:

Updated:

Leave a comment