Chrome download history forensics
2026-06-21 · 4 min
Download records answer a question history alone cannot: not just which
URL the browser touched, but which bytes landed on disk and where. The
downloads and downloads_url_chains tables live inside the History
SQLite database — so the artifact you already pulled for visit analysis
also carries a complete record of saved files, blocked files, and the
redirect chain behind each one.
Where the tables live
The downloads tables are not a separate file. They sit in the same
History SQLite database as urls and visits, for both Chrome and
Edge:
%LOCALAPPDATA%\Google\Chrome\User Data\Default\History
%LOCALAPPDATA%\Microsoft\Edge\User Data\Default\History
That means one acquisition of History gives you visits and downloads
in the same place — see
Chrome History database forensics
for the broader schema and
Chrome History file locations
for per-OS paths.
The downloads table
One row per saved file. The columns that carry investigative weight:
| Column | Meaning |
|---|---|
id | Local download id (joins downloads_url_chains) |
guid | Stable cross-session identifier |
current_path | Where the file is now on disk |
target_path | Intended final path (often a .crdownload rename target) |
tab_url | Page the download was initiated from |
tab_referrer_url | Referrer of that page |
start_time / end_time | When the transfer began and finished |
received_bytes / total_bytes | Bytes saved vs. expected size |
state | in_progress / complete / cancelled / interrupted |
danger_type | Safe Browsing verdict (see below) |
interrupt_reason | Why an incomplete download stopped |
opened | Whether the user opened the file from the browser |
last_access_time | Last time the file was accessed via the browser |
mime_type / original_mime_type | Served vs. sniffed content type |
current_path and target_path diverging is normal — a partial download
sits at a temporary name and only takes target_path on completion. A
complete row where the two still differ, or where the file no longer
exists on disk, is worth a note.
The redirect chain
downloads_url_chains records the full hop sequence that produced the
file:
| Column | Meaning |
|---|---|
id | Joins downloads.id |
chain_index | Hop order, 0 = the URL the user clicked |
url | The URL at that hop |
Order by chain_index for one id and you get click URL → redirects →
final fetch URL. This is the column that unmasks a shortener or tracking
redirect: the user clicked bit.ly/..., but the last hop is the real
host that served the payload. For malware triage, the final hop is the
indicator you want, not tab_url.
Reading state, danger and interrupt
state is the headline, but the two qualifier columns explain why a
download ended the way it did:
danger_typeis the Safe Browsing classification — values such as dangerous file, dangerous URL, uncommon content, potentially unwanted, or "not dangerous". A non-benigndanger_typepaired withcompletemeans the user clicked through a warning and kept the file.interrupt_reasonexplains aninterruptedorcancelledrow — network failure, disk full, file blocked by policy, or a Safe Browsing block. A blocked-by-securityinterrupt_reasonis evidence the browser stopped the file, not the user.
Together with opened, these let you separate downloaded, blocked,
and downloaded-and-run.
Timestamps
Every time column (start_time, end_time, last_access_time) is a
WebKit/Chrome timestamp: microseconds since 1601-01-01 UTC, not Unix
epoch. Convert before correlating, or downloads will land 369 years off
your other artifacts. See
browser timestamp formats for the
conversion.
What the tool surfaces
BrowserForensics parses the downloads tables straight from History and
shows, per file: the saved-to path, the source URL, byte counts, and the
start/end times — all decoded from WebKit time. Those rows feed into the
unified timeline alongside visits and cookies, so a download lines up
against the page that launched it and the cookies set in the same window.
See build a browser activity timeline
for how the streams merge.
Pitfalls
- A cleared download list does not always vacuum the rows immediately — check the WAL before concluding the history is empty.
target_pathis the browser's intent, not proof the file is still there; verify against the filesystem.received_bytes<total_byteson acompleterow usually meanstotal_byteswas never known (chunked transfer), not a truncated file.- Edge shares the exact schema; the only difference is the profile path.