Summary
osv-scanner scan --licenses <allowlist> crashes with an unrecoverable panic (index out of range) when scanning packages whose license field is empty. An empty license string reaches spdx.Satisfies("", allowlist), the tokenizer produces an empty token list, and tokens.next() dereferences ts.tokens[0] without a bounds check.
This is reachable from valid parser output, not malformed input: the APK extractor emits Licenses: []string{record["L"]} and, when an Alpine package record omits the L: field, that yields []string{""}. The license enricher preserves it and it flows into the SPDX evaluator. With no recover() anywhere in the scan path, the panic aborts the whole scan.
Affected code
internal/spdx/satisfies.go:72-77:
// next returns the next token in the list of tokens, removing it from the list in the process
func (ts *tokens) next() string {
token := ts.tokens[0]
ts.tokens = ts.tokens[1:]
return token
}
No bounds check exists before ts.tokens[0], unlike peek() (:63-69) which does guard with len(ts.tokens) == 0.
Call chain: Satisfies("", ...) -> parse (:157) -> parseOr -> parseAnd -> parseExpression (:216) -> nextAndIsNextNextValid (:92) -> next (:73) -> panic.
Reproduction
At HEAD d87a911 (go 1.26+):
go test ./internal/spdx -run TestPocSatisfiesEmptyLicense -v
Observed output (poc_satisfies_test.go calling spdx.Satisfies("", []string{"MIT"})):
panic: runtime error: index out of range [0] with length 0
github.com/google/osv-scanner/v2/internal/spdx.(*tokens).next(...)
C:/.../internal/spdx/satisfies.go:73
End-to-end: scan an Alpine repository/container image containing a package whose lib/apk/db/installed record omits the L: field with osv-scanner scan --licenses MIT <target>. The empty license reaches spdx.Satisfies("") and the process terminates.
Impact
- Complete denial of the scan whenever
--licenses is enabled and any scanned package lacks a license field.
- CI pipelines fail consistently until the package or flag is removed.
- Remaining packages in the scan are never evaluated.
Root cause / suggested fix
tokenise can legitimately produce zero tokens (empty input). next() must not index ts.tokens[0] unconditionally - return an error (or EOF token) when the stream is exhausted.
- Skip empty license strings before evaluating (e.g. treat
"" as no license / not satisfied rather than crashing).
- Add regression tests for empty license expressions.
- Consider a top-level
recover() around scan execution so one malformed package record cannot terminate the whole process.
Summary
osv-scanner scan --licenses <allowlist>crashes with an unrecoverable panic (index out of range) when scanning packages whose license field is empty. An empty license string reachesspdx.Satisfies("", allowlist), the tokenizer produces an empty token list, andtokens.next()dereferencests.tokens[0]without a bounds check.This is reachable from valid parser output, not malformed input: the APK extractor emits
Licenses: []string{record["L"]}and, when an Alpine package record omits theL:field, that yields[]string{""}. The license enricher preserves it and it flows into the SPDX evaluator. With norecover()anywhere in the scan path, the panic aborts the whole scan.Affected code
internal/spdx/satisfies.go:72-77:No bounds check exists before
ts.tokens[0], unlikepeek()(:63-69) which does guard withlen(ts.tokens) == 0.Call chain:
Satisfies("", ...)->parse(:157) ->parseOr->parseAnd->parseExpression(:216) ->nextAndIsNextNextValid(:92) ->next(:73) -> panic.Reproduction
At HEAD
d87a911(go 1.26+):go test ./internal/spdx -run TestPocSatisfiesEmptyLicense -vObserved output (
poc_satisfies_test.gocallingspdx.Satisfies("", []string{"MIT"})):End-to-end: scan an Alpine repository/container image containing a package whose
lib/apk/db/installedrecord omits theL:field withosv-scanner scan --licenses MIT <target>. The empty license reachesspdx.Satisfies("")and the process terminates.Impact
--licensesis enabled and any scanned package lacks a license field.Root cause / suggested fix
tokenisecan legitimately produce zero tokens (empty input).next()must not indexts.tokens[0]unconditionally - return an error (or EOF token) when the stream is exhausted.""as no license / not satisfied rather than crashing).recover()around scan execution so one malformed package record cannot terminate the whole process.