Safari
Safari Local Storage
Overview
Safari's LocalStorage implementation (part of the Web Storage API) allows websites to store key-value pairs persistently in the browser. Each web origin (scheme + domain + port) gets its own SQLite database file. This data persists across browser sessions and can contain user preferences, application state, cached content, authentication tokens, and other application-specific data.
LocalStorage is forensically significant because web applications increasingly store substantial data client-side. Email clients, document editors, social media platforms, and other web applications may store messages, drafts, user settings, and cached content in LocalStorage. This data can persist even after browsing history and cookies have been cleared.
File Locations
| Path | macOS Version | Format |
|---|---|---|
~/Library/WebKit/WebsiteData/LocalStorage/*.localstorage | macOS 11.0 (Big Sur) and later | SQLite |
~/Library/Safari/LocalStorage/*.localstorage | macOS 10.12-10.15 | SQLite |
Each origin gets a separate .localstorage file. The directory may contain dozens or hundreds of files depending on the user's browsing habits.
Filename Convention
LocalStorage files use a naming convention that encodes the web origin:
{scheme}_{domain}_{index}.localstorage
Examples:
| Filename | Origin |
|---|---|
https_github.com_0.localstorage | https://github.com |
https_app.example.com_0.localstorage | https://app.example.com |
http_localhost_0.localstorage | http://localhost |
The scheme and domain are separated by an underscore. The trailing _0 is an index number (typically 0). Dots in domain names are preserved; only the scheme separator uses an underscore.
Database Schema
Each .localstorage file is a SQLite database with a single table.
ItemTable
| Column | Type | Description |
|---|---|---|
key | TEXT | The localStorage key name |
value | BLOB | The stored value (typically UTF-8 or UTF-16 encoded text) |
The schema is minimal -- just key-value pairs with no timestamps, metadata, or row IDs beyond SQLite's implicit rowid.
Query
SELECT key, value FROM ItemTable;
Key Fields for Analysis
key: The localStorage key name set by the web application. Common patterns include:- User preference keys (e.g.,
theme,language,darkMode) - Authentication-related keys (e.g.,
auth_token,user_id,session) - Application state (e.g.,
draft_message,last_viewed,cart_items) - Analytics and tracking (e.g.,
_ga,amplitude_id)
- User preference keys (e.g.,
value: The stored data. Values can range from simple strings to large JSON objects. Web applications may store serialised application state, cached API responses, or user-generated content.- Origin (from filename): The web origin identifies which website stored the data. This provides crucial context for interpreting the key-value pairs.
Timestamps
LocalStorage databases do not contain internal timestamps. To determine when data was written:
- Use the file modification time of the
.localstoragefile (reflects the last write to any key in that origin's storage) - Correlate with browsing history entries for the same origin
- Check FSEvents for write events to the LocalStorage directory
- Some web applications store their own timestamps within the stored values
Analysis Notes
- Sensitive data exposure: Web applications frequently store sensitive information in LocalStorage, including authentication tokens, user IDs, draft messages, and personal preferences. This data may persist after the user logs out of the web application.
- Application state recovery: Single-page applications (SPAs) often store significant application state in LocalStorage. Email clients, project management tools, and messaging apps may cache messages, contacts, or documents locally.
- Cleared history bypass: LocalStorage is not always cleared when a user clears their browsing history or cookies. It may persist through standard browser cleanup operations, making it a more durable evidence source.
- JSON data: Many values are JSON-encoded objects. Parse these to extract structured data.
- Value truncation: Values can be very large (the Web Storage spec allows up to 5-10 MB per origin). Forensic tools typically preview only the first portion and record the total size.
- Origin enumeration: The list of
.localstoragefiles itself reveals which web applications the user has interacted with, even without examining the contents. - Encrypted or obfuscated values: Some web applications encrypt or obfuscate their LocalStorage values. Base64-encoded strings and encrypted blobs may require application-specific knowledge to decode.
Version Differences
| Version | Change |
|---|---|
| macOS 10.12-10.15 | LocalStorage at ~/Library/Safari/LocalStorage/ |
| macOS 11.0 (Big Sur)+ | LocalStorage moved to ~/Library/WebKit/WebsiteData/LocalStorage/ |
The SQLite schema (ItemTable with key and value columns) has remained unchanged across all versions.
Tool Support
macfor
The browser.safari plugin enumerates all .localstorage files in the LocalStorage directory, validates each as a SQLite database, and parses the ItemTable. It emits local_storage records with fields including origin (extracted from the filename), key, value_preview (truncated to 200 characters with ... suffix for longer values), value_size (in bytes), and source_file. Raw database files are also preserved in the evidence container.
LocalStorage collection can be disabled with the SkipLocalStorage option.
Manual Analysis
# List all LocalStorage databases and their origins
ls ~/Library/WebKit/WebsiteData/LocalStorage/*.localstorage
# Examine a specific origin's data
sqlite3 -readonly "https_github.com_0.localstorage" \
"SELECT key, length(value) AS size FROM ItemTable ORDER BY size DESC;"
# Extract all key-value pairs for an origin
sqlite3 -readonly "https_github.com_0.localstorage" \
"SELECT key, substr(value, 1, 100) AS preview FROM ItemTable;"
# Search across all origins for a specific key
for db in ~/Library/WebKit/WebsiteData/LocalStorage/*.localstorage; do
result=$(sqlite3 -readonly "$db" \
"SELECT key, substr(value,1,80) FROM ItemTable WHERE key LIKE '%token%'" 2>/dev/null)
if [ -n "$result" ]; then
echo "=== $(basename "$db") ==="
echo "$result"
fi
done
Python Analysis
import sqlite3
import os
import json
ls_dir = os.path.expanduser(
"~/Library/WebKit/WebsiteData/LocalStorage"
)
for filename in os.listdir(ls_dir):
if not filename.endswith(".localstorage"):
continue
filepath = os.path.join(ls_dir, filename)
# Extract origin from filename
name = filename.rsplit("_", 1)[0] # Remove _0.localstorage
parts = name.split("_", 1)
origin = f"{parts[0]}://{parts[1]}" if len(parts) == 2 else name
conn = sqlite3.connect(f"file:{filepath}?mode=ro", uri=True)
rows = conn.execute("SELECT key, value FROM ItemTable").fetchall()
conn.close()
if rows:
print(f"\n{origin} ({len(rows)} keys)")
for key, value in rows:
val_str = value.decode("utf-8", errors="replace") if isinstance(value, bytes) else str(value)
# Try to parse JSON values
try:
parsed = json.loads(val_str)
val_str = json.dumps(parsed, indent=2)[:200]
except (json.JSONDecodeError, TypeError):
val_str = val_str[:200]
print(f" {key}: {val_str}")
References
- Web Storage API Specification (W3C)
- MDN Web Docs: Window.localStorage
- SANS FOR518: Mac and iOS Forensic Analysis