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.
Summary
internal/output/output_result.gobuilds the scan result for every matchedpackage. For each vulnerability it calls
getNextFixVersion, which parses thefixedstring of every affected range event withsemantic.MustParse(
output_result.go:590).MustParsepanics when the ecosystem's version parserreturns an error, so a single advisory whose
fixedvalue is unparsable abortsthe entire scan after matching has already succeeded. Because every output
format (table, JSON, SARIF, HTML) routes through
BuildResults, all of themcrash.
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:semantic.ParsereturnsErrUnsupportedEcosystemfor ecosystems with noversion parser, and
ErrInvalidVersionfor a malformed version in ecosystemswhose parser validates input (CRAN, Debian, Alpine, Ubuntu, Hackage, ...).
MustParseturns either error into a panic. The guard at:584does notprevent this:
vp.CompareStr(GetFixed())returns(0, err)on an unparsablefixed, the error is discarded, soorder == 0, theorder > 0skip does notfire, and execution reaches the panicking
MustParse.Reproduction (public CLI, offline DB)
Reproduces end-to-end through the documented
--offline-vulnerabilities / --local-db-pathfeature — no internal calls.mypkg@1.0.0(CRAN) matches anadvisory by its
versionslist, and result building then parses the range'smalformed
fixedvalue and panics. Real advisories routinely carry both aversionslist andranges.Actual behavior
Expected behavior
An unparsable
fixedversion is skipped (that event simply cannot be offered asa 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/semanticparsers are pinned per release while osv.dev's dataevolves. Any advisory that carries a
fixedstring 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.Parseand skip the event on error, mirroringthe "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:334uses
osvecosystem.MustParse(eco.Name)on the scanned package's ecosystem in thetable renderer. Reachability depends on whether an unrecognized ecosystem can
reach output (e.g. via SBOM input); worth a separate look but not covered here.