Skip to content

getNextFixVersion panics on an advisory with an unparsable fixed version, crashing result output #2936

Description

@ashvinctrl

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions