Scan Safari profile databases for history and browser state (#846)

* fix(ios): scan Safari profile databases for history and browser state

Safari profiles (iOS 17 and later) keep their own databases under
Library/Safari/Profiles/<UUID>/, but SafariHistory and SafariBrowserState
only ever looked at the default profile's Library/Safari/History.db and
Library/Safari/BrowserState.db.

On a device where browsing happens inside a profile, MVT silently skipped
that history and still reported no detections, so an indicator only ever
visited within a profile went unnoticed.

Both modules now also match Library/Safari/Profiles/*/ in backups and in
filesystem dumps. No helper changes were needed: the Manifest.db lookup
already translates "*" into a SQL LIKE wildcard, and the filesystem lookup
already globs.

Found while examining an encrypted iOS 26.5.2 backup that contained 14
per-profile History.db files under
AppDomain-com.apple.mobilesafari::Library/Safari/Profiles/<UUID>/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(ios): scope Safari redirects to history database

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Janik Besendorf <janik@besendorf.org>
This commit is contained in:
Volodymyr Styran
2026-07-27 15:06:44 +02:00
committed by GitHub
co-authored by Claude Opus 5 Janik Besendorf
parent a6c3a805d8
commit 84df51c518
5 changed files with 271 additions and 23 deletions
+18
View File
@@ -4,6 +4,7 @@
# https://license.mvt.re/1.1/
import os
import sqlite3
from pathlib import Path
@@ -37,6 +38,23 @@ def get_indicator_file():
print("PYTEST env", os.getenv("PYTEST_CURRENT_TEST"))
def add_backup_manifest_entry(backup_path, file_id, domain, relative_path):
"""
Register an extra file in a test backup's Manifest.db
"""
conn = sqlite3.connect(os.path.join(backup_path, "Manifest.db"))
conn.execute(
"INSERT INTO Files (fileID, domain, relativePath, flags, file) "
"VALUES (?, ?, ?, 1, ?);",
(file_id, domain, relative_path, b""),
)
conn.commit()
# MVT opens backup databases with immutable=1, which ignores any -wal file,
# so the new row has to be checkpointed into Manifest.db itself.
conn.execute("PRAGMA journal_mode=DELETE;")
conn.close()
def delete_tmp_db_files(file_path):
"""
Remove Sqlite temporary files that appear on some platforms