Safari

Safari Downloads

Overview

Safari maintains a record of all downloads performed through the browser in a property list file. This artifact is valuable for identifying files that were downloaded from the internet, including the source URL, local save path, file size, download timestamps, and completion status. Even if the downloaded file has been deleted from disk, the download record may persist in this plist until the user clears it or Safari prunes old entries.

Download records are particularly useful for investigating malware delivery, data exfiltration staging, and identifying files of interest that the user obtained from the web.

File Locations

FilePathFormat
Downloads record~/Library/Safari/Downloads.plistBinary Property List

This file may not exist if the user has never downloaded anything through Safari or has cleared all download records.

File Format

Downloads.plist is a binary property list. Modern Safari versions (Safari 11+) use a dictionary structure with a DownloadHistory key, while older versions used a plain array at the root level.

Modern Format (Safari 11+)

Root (Dictionary)
  DownloadHistory (Array)
    [0] (Dictionary)
      DownloadEntryURL (String)
      DownloadEntryPath (String)
      DownloadEntryDateAddedKey (Date)
      DownloadEntryDateFinishedKey (Date)
      DownloadEntryProgressTotalToLoad (Integer)
      DownloadEntryProgressBytesSoFar (Integer)
      DownloadEntryIdentifier (String)
      DownloadEntryRemoveWhenDoneKey (Boolean)
    [1] (Dictionary)
      ...

Legacy Format (Older Safari)

Root (Array)
  [0] (Dictionary)
    DownloadEntryURL (String)
    DownloadEntryPath (String)
    ...

Plist Keys

KeyTypeDescription
DownloadEntryURLStringSource URL of the download
DownloadEntryPathStringFull local filesystem path where the file was saved
DownloadEntryDateAddedKeyDateTimestamp when the download was initiated
DownloadEntryDateFinishedKeyDateTimestamp when the download completed (absent or zero if incomplete)
DownloadEntryProgressTotalToLoadIntegerExpected total file size in bytes
DownloadEntryProgressBytesSoFarIntegerBytes downloaded so far
DownloadEntryIdentifierStringUUID uniquely identifying this download entry
DownloadEntryRemoveWhenDoneKeyBooleanWhether Safari should auto-remove this entry after completion

Key Fields for Analysis

  • DownloadEntryURL: The full source URL. Reveals the server, path, and often the original filename. May include query parameters such as authentication tokens or tracking IDs.
  • DownloadEntryPath: The local save location. This tells you where on disk the file was placed. The filename component is extracted as the downloaded file name. Common save directories include ~/Downloads/, but users may have changed the destination.
  • DownloadEntryDateAddedKey / DownloadEntryDateFinishedKey: Together these establish the download window. A missing or zero finish time indicates the download was interrupted or is still in progress.
  • DownloadEntryProgressTotalToLoad vs DownloadEntryProgressBytesSoFar: If BytesSoFar < TotalToLoad, the download did not complete. This can indicate network interruption, user cancellation, or download blocking.
  • DownloadEntryIdentifier: A UUID for forensic correlation. Can be used to link download records across artifacts or time periods.

Download State Determination

The download state can be inferred from the byte counts:

ConditionState
BytesSoFar >= TotalToLoad and TotalToLoad > 0Complete
BytesSoFar < TotalToLoad or TotalToLoad = 0Incomplete

Timestamps

Timestamps in Downloads.plist are stored as standard property list <date> values in ISO 8601 format (e.g., 2026-01-23T10:00:00Z). Unlike History.db, these are not Core Data timestamps -- the plist library handles the conversion automatically.

When accessed programmatically via Apple's property list APIs or compatible libraries, these values decode directly to datetime objects.

Analysis Notes

  • Deleted file recovery: The download record persists even after the downloaded file is deleted from the filesystem. Cross-reference DownloadEntryPath with filesystem metadata to determine if the file still exists.
  • Malware delivery: Download records often reveal the initial infection vector. Look for executable types (.dmg, .pkg, .app, .zip, .sh) downloaded from suspicious domains.
  • Renamed files: Compare the filename in the URL with the filename in the local path. Differences may indicate user renaming or Safari's automatic conflict resolution (appending numbers).
  • Incomplete downloads: Failed or interrupted downloads may indicate content filtering, network issues, or user cancellation. The byte counts provide detail on how much was transferred.
  • Cleared history: If Downloads.plist is missing or empty but filesystem evidence shows recently downloaded files, the user may have cleared their download history.
  • Quarantine attributes: Downloaded files on macOS are tagged with extended attributes (com.apple.quarantine) that record the source URL and download time independently. These can corroborate or supplement Downloads.plist data.

Version Differences

VersionChange
Safari 10 and earlierRoot element is a plain array of download entries
Safari 11+ (macOS 10.13+)Root element is a dictionary with DownloadHistory array

Both formats contain the same per-entry keys. The macfor collector handles both formats automatically.

Tool Support

macfor

The browser.safari plugin reads Downloads.plist, handles both legacy (array) and modern (dictionary) formats, and emits structured browser_download records with fields including url, local_path, file_name, total_bytes, received_bytes, start_time, end_time, state (complete/incomplete), identifier, and remove_when_done. The raw plist file is also preserved in the evidence container.

Manual Analysis

# Convert binary plist to XML for inspection
plutil -convert xml1 -o - ~/Library/Safari/Downloads.plist

# View with Python
python3 -c "
import plistlib
with open('Downloads.plist', 'rb') as f:
    data = plistlib.load(f)
    # Modern format
    if isinstance(data, dict) and 'DownloadHistory' in data:
        entries = data['DownloadHistory']
    else:
        entries = data
    for entry in entries:
        print(entry.get('DownloadEntryURL', 'N/A'))
        print(f\"  -> {entry.get('DownloadEntryPath', 'N/A')}\")
        print(f\"  Date: {entry.get('DownloadEntryDateAddedKey', 'N/A')}\")
        print()
"

References

Previous
History