If your Rust project relies on Dependabot alone, it has been missing most new Rust advisories since August. Of the 117 vulnerability advisories RustSec added from August to mid-October 2026, 115 weren't in GitHub's reviewed advisory database on 17 October, and Dependabot only alerts on reviewed advisories. The gap runs the other way too: 364 crates.io advisories GitHub reviewed this year have no RustSec entry, so cargo audit and cargo deny don't report them.
This guide is for Rust teams choosing how to scan Cargo.lock in CI. It covers where each tool gets its data, what the two databases held for 2026, a real sandbox escape that only one of them listed, and a script that shows which source reports each advisory in your lockfile.
The short answer
cargo audit and cargo deny read the RustSec Advisory Database. Dependabot reads GitHub's reviewed advisories. In 2026 the two have drifted apart: on 17 October, 185 of the 310 RustSec vulnerability advisories added this year had no GitHub reviewed entry, including almost every one since August, and 364 of the 472 crates.io advisories GitHub reviewed this year had no RustSec entry. Run a RustSec-based check and Dependabot, or use a scanner that queries OSV.dev, which carries both. Don't read a quiet Dependabot tab as a clean Rust dependency tree.
Where each tool gets its data
| Tool | Advisory source |
|---|---|
cargo audit | RustSec Advisory Database |
cargo deny check advisories | RustSec by default (db-urls), configurable |
| Dependabot alerts | GitHub Advisory Database, reviewed advisories only |
| OSV-Scanner and other OSV.dev clients | OSV.dev, which includes both RustSec and GitHub reviewed advisories |
GitHub lists RustSec as one of its sources, and RustSec entries often carry a GHSA alias, so it's natural to assume the two stay in sync. GitHub's own documentation explains why they can't fully: advisories arrive in its database as unreviewed, and "Dependabot doesn't create Dependabot alerts for unreviewed advisories". Until a GitHub curator reviews an imported advisory, Dependabot users don't see it.
What the two databases held in 2026
OSV publishes a dump of every crates.io advisory from both sources. RustSec entries carry the disclosure date rather than the day they were added, so the script below takes the add date from the RustSec repository's git history instead. It matches entries across databases by ID, GHSA alias or CVE, and ignores RustSec's informational notices for unmaintained or unsound crates. Counting by the date each entry was added is why our 2026 total is 310: counted by the date printed in each advisory, it would be 255, because RustSec added some older advisories this year:
#!/usr/bin/env python3
"""Compare RustSec and GitHub's reviewed advisory database for crates.io advisories.
Inputs:
crates.zip OSV's crates.io dump:
https://osv-vulnerabilities.storage.googleapis.com/crates.io/all.zip
added.txt when each RUSTSEC id first appeared in rustsec/advisory-db, from:
git log --no-renames --diff-filter=A --format='C %cI' --name-only -- crates
Usage: rustsec_vs_github.py crates.zip added.txt [YEAR]
"""
import json
import statistics
import sys
import zipfile
from datetime import datetime
zpath, apath = sys.argv[1], sys.argv[2]
year = int(sys.argv[3]) if len(sys.argv) > 3 else 2026
z = zipfile.ZipFile(zpath)
recs = {d["id"]: d for d in (json.loads(z.read(n)) for n in z.namelist())}
added, when = {}, None
for line in open(apath): # git log prints newest first, so the last date seen is the first add
line = line.strip()
if line.startswith("C "):
when = line[2:]
elif line.endswith(".md"):
added[line.rsplit("/", 1)[-1][:-3]] = when
def ts(s):
return datetime.fromisoformat(s.replace("Z", "+00:00"))
def informational(d):
return any((a.get("database_specific") or {}).get("informational") for a in d.get("affected", []))
def rustsec_seen(d): # when cargo audit / cargo deny could first see it
return ts(added[d["id"]]) if d["id"] in added else ts(d["published"])
def github_seen(d): # when Dependabot could first see it
return ts((d.get("database_specific") or {}).get("github_reviewed_at") or d["published"])
def keys(d):
return {d["id"], *d.get("aliases", [])}
rustsec = [d for i, d in recs.items() if i.startswith("RUSTSEC-") and not informational(d)]
ghsa = [d for i, d in recs.items() if i.startswith("GHSA-")]
by_key = {}
for g in ghsa:
for k in keys(g):
by_key.setdefault(k, g)
pairs, rs_only = [], []
for r in rustsec:
g = next((by_key[k] for k in keys(r) if k in by_key), None)
(pairs.append((r, g)) if g else rs_only.append(r))
matched = {g["id"] for _, g in pairs}
in_year = lambda dt: dt.year == year
pair_of = {r["id"]: g for r, g in pairs}
rs_year = [r for r in rustsec if in_year(rustsec_seen(r))]
gh_year = [g for g in ghsa if in_year(github_seen(g)) and not g.get("withdrawn")]
rs_matched = [r for r in rs_year if r["id"] in pair_of]
gh_unmatched = [g for g in gh_year if g["id"] not in matched]
print(f"{year}: RustSec vulnerability advisories added: {len(rs_year)}")
print(f" with a GitHub reviewed entry: {len(rs_matched)}; RustSec only: {len(rs_year) - len(rs_matched)}")
print(f"{year}: crates.io advisories added to GitHub's reviewed database: {len(gh_year)}")
print(f" with a RustSec entry: {len(gh_year) - len(gh_unmatched)}; GitHub only: {len(gh_unmatched)}")
lag = sorted((github_seen(pair_of[r["id"]]) - rustsec_seen(r)).total_seconds() / 86400 for r in rs_matched)
rs_first = [x for x in lag if x > 1]
gh_first = [-x for x in lag if x < -1]
print(f"Timing for the {len(lag)} RustSec {year} advisories GitHub also has:")
print(f" RustSec first by more than a day: {len(rs_first)}; GitHub first: {len(gh_first)}; "
f"within a day: {len(lag) - len(rs_first) - len(gh_first)}")
for name, xs in (("GitHub after RustSec", rs_first), ("RustSec after GitHub", gh_first)):
if xs:
print(f" {name}: median {statistics.median(xs):.0f} days, over 30 days {sum(x > 30 for x in xs)}, "
f"over 90 days {sum(x > 90 for x in xs)}")
$ curl -O https://osv-vulnerabilities.storage.googleapis.com/crates.io/all.zip && mv all.zip crates.zip
$ git clone https://github.com/rustsec/advisory-db
$ git -C advisory-db log --no-renames --diff-filter=A --format='C %cI' --name-only -- crates > added.txt
$ python3 rustsec_vs_github.py crates.zip added.txt 2026
2026: RustSec vulnerability advisories added: 310
with a GitHub reviewed entry: 125; RustSec only: 185
2026: crates.io advisories added to GitHub's reviewed database: 472
with a RustSec entry: 108; GitHub only: 364
Timing for the 125 RustSec 2026 advisories GitHub also has:
RustSec first by more than a day: 49; GitHub first: 30; within a day: 46
GitHub after RustSec: median 6 days, over 30 days 16, over 90 days 1
RustSec after GitHub: median 16 days, over 30 days 11, over 90 days 5
Where the two overlap, the gap is usually short. RustSec went first more often, and when it did, GitHub followed after a median of six days. The problem is the advisories that never crossed over. By month:
| Month (2026) | RustSec advisories added | Not in GitHub's reviewed database on 17 October |
|---|---|---|
| January | 10 | 1 |
| February | 28 | 7 |
| March | 68 | 38 |
| April | 29 | 6 |
| May | 17 | 0 |
| June | 23 | 5 |
| July | 18 | 13 |
| August | 32 | 31 |
| September | 32 | 31 |
| October (to the 17th) | 53 | 53 |
Until July, most RustSec advisories reached GitHub's reviewed database. From August on, almost none did. We can't see GitHub's queue, so we can't say why. But 74 of the 185 RustSec-only entries already carry a GHSA ID, usually because the maintainers published a repository advisory on GitHub. The three we looked up returned a 404 from GitHub's global advisory API.
A sandbox escape only one database had
On 14 October, Wasmtime published GHSA-m9f7-9w4p-gv38, a high-severity filesystem sandbox escape in wasmtime-wasi on macOS and FreeBSD. A guest with write access to any directory in its sandbox could move outside directories into it and read or write them. It's fixed in 49.0.3, 48.0.6 and 36.0.18. RustSec added it the same day as RUSTSEC-2026-0352. On 17 October GitHub's global advisory API returned 404 for that GHSA ID, and the advisory has no CVE yet.
So a team on wasmtime-wasi 48.0.1 that runs cargo audit saw it within hours. A team relying only on Dependabot saw nothing. The reverse happened earlier this year: Wasmtime's CVE-2026-58494, fixed on 24 June, reached RustSec on 29 June but GitHub's reviewed database only on 16 October.
The other direction: GitHub-only advisories
The 364 GitHub-only advisories cluster in a few projects that publish their own repository advisories on GitHub. The largest groups in 2026 are surrealdb (55), coreutils (44), russh (15), deno (14), zebrad (13), deepseek-tui (13) and rustfs (12). Several are applications more than libraries. But plenty of them, russh and the gix crates among them, are dependencies other projects pull in. Those findings reach Dependabot and not cargo audit.
Check which source reports what
This script reads a Cargo.lock, queries OSV.dev for every registry crate and version, and groups each finding by which database carries it. It skips RustSec's informational notices and malware records. It uses only the Python standard library:
#!/usr/bin/env python3
"""List known advisories for a Cargo.lock and which database each comes from.
Queries OSV (api.osv.dev), which carries both RustSec and GitHub's reviewed
advisories, then groups each finding by source:
RustSec only -> cargo audit / cargo deny report it, Dependabot doesn't
GitHub only -> Dependabot reports it, cargo audit / cargo deny don't
both
Usage: cargo_lock_sources.py path/to/Cargo.lock
"""
import json
import re
import sys
import tomllib
import urllib.request
def query(batch):
req = urllib.request.Request("https://api.osv.dev/v1/querybatch", json.dumps({"queries": batch}).encode(),
{"Content-Type": "application/json"})
return json.load(urllib.request.urlopen(req, timeout=60))["results"]
def details(vid):
with urllib.request.urlopen(f"https://api.osv.dev/v1/vulns/{vid}", timeout=30) as r:
return json.load(r)
lock = tomllib.loads(open(sys.argv[1]).read())
pkgs = sorted({(p["name"], p["version"]) for p in lock.get("package", [])
if p.get("source", "").startswith("registry+")})
hits = {}
for i in range(0, len(pkgs), 500):
chunk = pkgs[i:i + 500]
res = query([{"package": {"ecosystem": "crates.io", "name": n}, "version": v} for n, v in chunk])
for (n, v), r in zip(chunk, res):
for vuln in r.get("vulns", []):
hits.setdefault((n, v), set()).add(vuln["id"])
groups = {"RustSec only": [], "GitHub only": [], "both": []}
for (n, v), ids in sorted(hits.items()):
seen = set()
for vid in sorted(ids):
if vid in seen or vid.startswith("MAL-"):
continue
d = details(vid)
if any((a.get("database_specific") or {}).get("informational") for a in d.get("affected", [])):
continue # unmaintained/unsound notices are not vulnerabilities
same = {vid, *d.get("aliases", [])} & ids
seen |= same
has_rs = any(x.startswith("RUSTSEC-") for x in same)
has_gh = any(x.startswith("GHSA-") for x in same)
label = "both" if has_rs and has_gh else ("RustSec only" if has_rs else "GitHub only")
ref = ", ".join(sorted(same))
groups[label].append(f"{n} {v}: {ref}: {d.get('summary', '')[:70]}")
print(f"{len(pkgs)} registry crates checked")
for label, rows in groups.items():
print(f"{label}: {len(rows)}")
for row in rows:
print(" " + row)
On a small lockfile built from crate versions common in real projects:
$ python3 cargo_lock_sources.py Cargo.lock
8 registry crates checked
RustSec only: 15
quinn-proto 0.11.17: RUSTSEC-2026-0338: Remote peer can panic quinn-proto with zero-length STREAM frames durin
quinn-proto 0.11.17: RUSTSEC-2026-0340: Peer minimum ACK delay can panic quinn-proto when ACK_FREQUENCY transm
quinn-proto 0.11.17: RUSTSEC-2026-0343: Outgoing datagram buffer accounting can panic the sender in quinn-prot
rustls 0.23.43: RUSTSEC-2026-0346: `Debug` impls for client session values expose TLS secrets
rustls 0.23.43: RUSTSEC-2026-0348: Brotli certificate decompression can allocate up to 1 GiB before authe
rustls 0.23.43: RUSTSEC-2026-0349: Unauthenticated ServerHello can downgrade later connections to classic
wasmtime-wasi 48.0.1: RUSTSEC-2026-0314: Guest can panic host through filesystem datetime overflow
wasmtime-wasi 48.0.1: RUSTSEC-2026-0321: WASI preview 0 implementation of `poll_oneoff` circumvents fuel consum
wasmtime-wasi 48.0.1: RUSTSEC-2026-0322: Excessive allocated memory on the host when guests don't have stdio
wasmtime-wasi 48.0.1: RUSTSEC-2026-0323: fd_readdir copies uninitialized struct padding into guest memory
wasmtime-wasi 48.0.1: RUSTSEC-2026-0324: Guest can panic host through filesystem timestamp before the epoch on
wasmtime-wasi 48.0.1: RUSTSEC-2026-0352: Filesystem sandbox escape when renaming symlinks with trailing slashes
zip 1.1.4: RUSTSEC-2026-0367: Insecure Preservation of SUID/SGID Bits during ZIP Extraction Leading
zip 1.1.4: RUSTSEC-2026-0368: zip: `ZipArchive::extract` creates symbolic links whose attacker-contr
zip 1.1.4: RUSTSEC-2026-0369: ZipStreamReader::extract() and ZipArchive::extract() apply Unix permis
GitHub only: 2
gix-fs 0.18.2: GHSA-f89h-2fjh-2r9q: gix-fs: Symlink prefix-reuse allows worktree escape during checkout
opentelemetry_sdk 0.29.0: GHSA-w9wp-h8wv-79jx: opentelemetry_sdk has unbounded memory allocation in W3C Baggage propa
both: 4
rustls 0.23.43: GHSA-2mjx-qc3c-rqvc, RUSTSEC-2026-0285: Rustls: TLS 1.3 handshake messages incorrectly accepted across encrypt
rustls-webpki 0.101.7: GHSA-82j2-j2ch-gfr8, RUSTSEC-2026-0104: rustls-webpki: Denial of service via panic on malformed CRL BIT STRING
rustls-webpki 0.101.7: GHSA-965h-392x-2mh5, RUSTSEC-2026-0098: webpki: Name constraints for URI names were incorrectly accepted
rustls-webpki 0.101.7: GHSA-xgp8-3hg3-c2mh, RUSTSEC-2026-0099: webpki: Name constraints were accepted for certificates asserting a wi
Dependabot would raise the four in "both" and the two GitHub-only ones. cargo audit would raise the four in "both" and the 15 RustSec-only ones, including the Wasmtime sandbox escape. Neither raises all 21.
We also ran it against the lockfiles of three large open-source Rust workspaces on 17 October. One had 44 open findings, all RustSec-only. One had 17 RustSec-only, 5 GitHub-only and 10 in both. The third had a single GitHub-only finding. A lockfile entry isn't proof the vulnerable code is reachable, and some findings, like the rsa timing issue, have no fix to move to. But in the two larger workspaces, most of what remained was something Dependabot doesn't report.
What to do
- Run a RustSec check in CI if you rely on Dependabot.
cargo deny check advisoriesorcargo auditon every pull request, and on a schedule so new advisories surface on unchanged code. - Keep Dependabot or another GitHub-sourced check if you run
cargo audit, for the advisories RustSec doesn't carry. - Or use one OSV.dev-based scanner for both. OSV-Scanner reads
Cargo.lockand queries OSV.dev, where both databases meet. Our script above shows which database each result came from. - Watch repository advisories for critical dependencies. For a runtime like Wasmtime, a security advisory on GitHub can be the first and, for days, the only public record of a fix.
- Treat an empty alerts tab as a statement about one database. Before calling a Rust tree clean, check which source the tool read.
We've written before about using cargo audit with the RustSec database and how cargo audit and cargo deny compare. This year's data changes one recommendation: for Rust, one advisory source is no longer enough.
The fair view
Both databases are doing hard work, and much of it is done by a few people. RustSec is volunteer-run and lists advisories quickly. GitHub's reviewed database adds severity scores, CWE tags and version ranges that tools rely on, and reviewing takes time. The 2025 numbers looked very different: 77 of 78 RustSec advisories added that year had a GitHub reviewed entry by 17 October. The backlog since August may clear quickly.
These counts are one snapshot, taken on 17 October from the OSV dump of 16 October, and matching by alias will miss a few pairs that use different identifiers. But a gap measured in months, on a sandbox escape, is a reason not to depend on one feed. The cheapest fix is a second scanner in CI.