Repository navigation
getNextFixVersion panics on an advisory with an unparsable fixed version, crashing result output #2936
Description
Summary
internal/output/output_result.go builds the scan result for every matched
package. For each vulnerability it calls getNextFixVersion, which parses the
fixed string of every affected range event with semantic.MustParse
(output_result.go:590). MustParse panics when the ecosystem's version parser
returns an error, so a single advisory whose fixed value is unparsable aborts
the entire scan after matching has already succeeded. Because every output
format (table, JSON, SARIF, HTML) routes through BuildResults, all of them
crash.
This is a distinct site from #2867 (match path, ecosystem-name MustParse) and
#2837 (match path, version parse). Neither touched the output/result-building
path.
Affected code
internal/output/output_result.go:590:
order, _ = semantic.MustParse(affectedEvent.GetFixed(), ecosystemPrefix).CompareStr(minFixVersion)semantic.Parse returns ErrUnsupportedEcosystem for ecosystems with no
version parser, and ErrInvalidVersion for a malformed version in ecosystems
whose parser validates input (CRAN, Debian, Alpine, Ubuntu, Hackage, ...).
MustParse turns either error into a panic. The guard at :584 does not
prevent this: vp.CompareStr(GetFixed()) returns (0, err) on an unparsable
fixed, the error is discarded, so order == 0, the order > 0 skip does not
fire, and execution reaches the panicking MustParse.
Reproduction (public CLI, offline DB)
Reproduces end-to-end through the documented --offline-vulnerabilities / --local-db-path feature — no internal calls. mypkg@1.0.0 (CRAN) matches an
advisory by its versions list, and result building then parses the range's
malformed fixed value and panics. Real advisories routinely carry both a
versions list and ranges.
WORK="$(mktemp -d)"
mkdir -p "$WORK/target"
cat > "$WORK/target/renv.lock" <<'EOF'
{ "R": { "Version": "4.3.0" },
"Packages": { "mypkg": { "Package": "mypkg", "Version": "1.0.0", "Repository": "CRAN" } } }
EOF
python3 - "$WORK" <<'PY'
import json,os,sys,zipfile
work=sys.argv[1]
adv={"id":"CB-CRAN-REPRO","affected":[
{"package":{"ecosystem":"CRAN","name":"mypkg"},
"versions":["1.0.0"],
"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"notaversion"}]}]}]}
zp=os.path.join(work,"db","osv-scanner","CRAN","all.zip")
os.makedirs(os.path.dirname(zp),exist_ok=True)
zipfile.ZipFile(zp,"w",zipfile.ZIP_DEFLATED).writestr("CB-CRAN-REPRO.json",json.dumps(adv))
PY
go run ./cmd/osv-scanner scan source \
--offline-vulnerabilities --local-db-path "$WORK/db" "$WORK/target"Actual behavior
panic: invalid version: 'notaversion' is not allowed
semantic.MustParse semantic/parse.go:36
output.getNextFixVersion internal/output/output_result.go:590
output.updateVuln internal/output/output_result.go:541
output.processPackage internal/output/output_result.go:462
output.BuildResults internal/output/output_result.go:202
output.PrintTableResults internal/output/table.go:30
Expected behavior
An unparsable fixed version is skipped (that event simply cannot be offered as
a fix); the scan completes and reports the matched vulnerability with no
available fix.
Why it is reachable without an attacker
The per-ecosystem DB zip stores the full advisory record, and the vendored
osv-schema / semantic parsers are pinned per release while osv.dev's data
evolves. Any advisory that carries a fixed string this build cannot parse —
future syntax, an ecosystem whose parser validates input, or a self-maintained
local DB — crashes result building on a package that otherwise matched. Broad
blast radius for a tool that runs in CI.
Suggested fix
Use the non-panicking semantic.Parse and skip the event on error, mirroring
the "skip, don't panic on unsupported ecosystems/versions" approach already
taken in #2837/#2867. The default (parsable) path is unchanged.
I have a fix and reproducing tests ready and would be happy to open a PR once
this is assigned.
Related, separate observation (not part of this fix): internal/output/table.go:334
uses osvecosystem.MustParse(eco.Name) on the scanned package's ecosystem in the
table renderer. Reachability depends on whether an unrecognized ecosystem can
reach output (e.g. via SBOM input); worth a separate look but not covered here.
Activity
G-Rath commentedon Jul 22, 2026
Do you have a reproduction using a real published advisory, rather than one you've crafted?
ashvinctrl commentedon Jul 22, 2026
No, there's no real published advisory that triggers it. I ran every
affected[].ranges[].fixedvalue throughsemantic.Parsefor its own ecosystem across all the validating ecosystems (Alpine, CRAN, Debian, Ubuntu, Hackage, Wolfi, Chainguard — ~193k records). Nothing errors on the pathgetNextFixVersionactually takes. The only rejecting value is a git commit hash in CVE-2024-38998, and that's in a GIT-range entry with an empty ecosystem, which line 579 filters out before the parse — so it's not reachable.So this is a latent / forward-compatibility crash, not something current osv.dev data hits — same situation as #2867. It's deterministic with a self-hosted
--local-db-pathDB (repro in the issue), and it'll fire if afixedvalue the pinned parser can't handle ever lands in a scannable ecosystem. I'm happy to send the one-lineMustParse->Parse-and-skip fix on that basis, but if you'd rather only track output-path panics that live data actually reaches, feel free to close.hamodywe commentedon Aug 16, 2026
Could this be assigned to me? Per CONTRIBUTING I'd rather ask before opening anything — the fix is written and verified locally, and I'm happy to drop it if you'd rather not take this.
On the question of a real advisory: I agree with the earlier answer that none currently triggers it, and I don't think that settles it either way. Two things make it look worth hardening regardless:
getNextFixVersionalready degrades gracefully for an unparsable installed version — it returnsN/Ainstead of failing. Thefixedvalue comes from the same third-party data and is the one place in the function that panics instead.The change is small:
CompareStralready returns an error on an unparsable version and it was being discarded, which is why the existingorder > 0guard doesn't stop execution reachingMustParse. Usingsemantic.Parseand skipping the event when it fails keeps a valid fix that appears alongside an unparsable one, so nothing that works today changes.I verified it the way you'd want: with the change reverted the new test fails with
panic: invalid version: 'notaversion' is not allowed, and with it applied the wholeinternal/outputpackage is green.ashvinctrl commentedon Aug 16, 2026
@hamodywe go ahead, it's yours. No point in two of us sending the same patch.
One thing worth carrying over so you don't have to redo it. I ran every
affected[].ranges[].fixedvalue through the realsemantic.Parsefor its own ecosystem across the validating ones (Alpine, CRAN, Debian, Ubuntu, Hackage, Wolfi, Chainguard), about 193k records, and nothing reaches the panic. The only value that rejects is the git commit hash in CVE-2024-38998, and that sits in a GIT-range entry with an empty ecosystem, which the check at line 579 filters out beforegetNextFixVersionever sees it.So this is forward-compat hardening rather than a live crash, same shape as #2867. Worth saying that plainly in the PR so @G-Rath can weigh it for what it actually is.
github-actions commentedon Oct 15, 2026
This issue has not had any activity for 60 days and will be automatically closed in two weeks
See https://github.com/google/osv-scanner/blob/main/CONTRIBUTING.md for how to contribute a PR if you're interested in helping out.