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
| File | Path | Format |
|---|---|---|
| Downloads record | ~/Library/Safari/Downloads.plist | Binary 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
| Key | Type | Description |
|---|---|---|
DownloadEntryURL | String | Source URL of the download |
DownloadEntryPath | String | Full local filesystem path where the file was saved |
DownloadEntryDateAddedKey | Date | Timestamp when the download was initiated |
DownloadEntryDateFinishedKey | Date | Timestamp when the download completed (absent or zero if incomplete) |
DownloadEntryProgressTotalToLoad | Integer | Expected total file size in bytes |
DownloadEntryProgressBytesSoFar | Integer | Bytes downloaded so far |
DownloadEntryIdentifier | String | UUID uniquely identifying this download entry |
DownloadEntryRemoveWhenDoneKey | Boolean | Whether 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.DownloadEntryProgressTotalToLoadvsDownloadEntryProgressBytesSoFar: IfBytesSoFar < 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:
| Condition | State |
|---|---|
BytesSoFar >= TotalToLoad and TotalToLoad > 0 | Complete |
BytesSoFar < TotalToLoad or TotalToLoad = 0 | Incomplete |
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
DownloadEntryPathwith 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.plistis 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 supplementDownloads.plistdata.
Version Differences
| Version | Change |
|---|---|
| Safari 10 and earlier | Root 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
- Apple Property List Documentation
- SANS FOR518: Mac and iOS Forensic Analysis
- macOS Quarantine Extended Attributes