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

PathmacOS VersionFormat
~/Library/WebKit/WebsiteData/LocalStorage/*.localstoragemacOS 11.0 (Big Sur) and laterSQLite
~/Library/Safari/LocalStorage/*.localstoragemacOS 10.12-10.15SQLite

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:

FilenameOrigin
https_github.com_0.localstoragehttps://github.com
https_app.example.com_0.localstoragehttps://app.example.com
http_localhost_0.localstoragehttp://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

ColumnTypeDescription
keyTEXTThe localStorage key name
valueBLOBThe 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)
  • 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 .localstorage file (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 .localstorage files 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

VersionChange
macOS 10.12-10.15LocalStorage 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

Previous
Extensions