You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
spdx.Satisfies parses SPDX license expressions with an unbounded recursive descent. A license string with deeply nested brackets recurses until the goroutine stack overflows, which is a Go fatal error that recover cannot catch, so the whole scan process aborts.
License strings are untrusted input: they come from the metadata of packages in the scanned target. When --licenses is enabled, every package license is passed to spdx.Satisfies at pkg/osvscanner/vulnerability_result.go:123.
This is distinct from #2968. That issue is an empty-input index out of range in tokens.next(); its proposed fix guards next() against an empty token stream. That guard does not bound recursion depth, so it does not address this crash. The two are separate defects in the same parser, one shallow (zero tokens) and one deep (many tokens).
Affected code
internal/spdx/satisfies.go. The recursion runs parseOr -> parseAnd -> parseExpression, and parseExpression calls back into parseOr on every ( (:215-237 at HEAD 567f3ea):
allowed["("] includes "(", so an arbitrarily long run of ( is accepted and each one adds a stack frame. There is no bound on nesting depth anywhere in the descent.
The closing brackets are not required to trigger it; an opening run alone overflows. On this machine the threshold sits between 1,000,000 nested brackets (survives) and 1,500,000 (overflows), so roughly a 3 MB license string. fatal error: stack overflow is not recoverable, so the process exits regardless of any recover on the scan path.
Impact
Denial of the scan when --licenses is enabled and any scanned package carries a sufficiently nested license expression.
The trigger is package metadata from the scanned target, so a single crafted or malformed package aborts the run and no remaining packages are evaluated.
Severity is bounded by the input size required (a multi-megabyte license string), so this is defense-in-depth for a parser that consumes untrusted input, rather than a trivially small trigger. It is still a reachable, unrecoverable crash from untrusted data in a security scanner, which is the reason to bound it.
Suggested fix
Bound the nesting depth in the recursive descent and return a normal parse error past the limit, so a malformed or hostile license expression is rejected like any other invalid input instead of crashing the process. Real SPDX expressions nest only a few levels, so a generous ceiling (for example 1000) rejects nothing legitimate. A regression test drives a deeply nested expression and asserts an error rather than a crash.
CWE-674 (Uncontrolled Recursion).
I have this change and its test ready locally and can open a PR once assigned.
Summary
spdx.Satisfiesparses SPDX license expressions with an unbounded recursive descent. A license string with deeply nested brackets recurses until the goroutine stack overflows, which is a Go fatal error thatrecovercannot catch, so the whole scan process aborts.License strings are untrusted input: they come from the metadata of packages in the scanned target. When
--licensesis enabled, every package license is passed tospdx.Satisfiesatpkg/osvscanner/vulnerability_result.go:123.This is distinct from #2968. That issue is an empty-input
index out of rangeintokens.next(); its proposed fix guardsnext()against an empty token stream. That guard does not bound recursion depth, so it does not address this crash. The two are separate defects in the same parser, one shallow (zero tokens) and one deep (many tokens).Affected code
internal/spdx/satisfies.go. The recursion runsparseOr -> parseAnd -> parseExpression, andparseExpressioncalls back intoparseOron every((:215-237at HEAD567f3ea):allowed["("]includes"(", so an arbitrarily long run of(is accepted and each one adds a stack frame. There is no bound on nesting depth anywhere in the descent.Reproduction
At HEAD
567f3ea, go 1.26:Observed:
The closing brackets are not required to trigger it; an opening run alone overflows. On this machine the threshold sits between 1,000,000 nested brackets (survives) and 1,500,000 (overflows), so roughly a 3 MB license string.
fatal error: stack overflowis not recoverable, so the process exits regardless of anyrecoveron the scan path.Impact
--licensesis enabled and any scanned package carries a sufficiently nested license expression.--licensescode path as osv-scanner panics on empty license field (index out of range) when --licenses is enabled #2968, so both crash the scanner from an untrusted license field, by different mechanisms.Severity is bounded by the input size required (a multi-megabyte license string), so this is defense-in-depth for a parser that consumes untrusted input, rather than a trivially small trigger. It is still a reachable, unrecoverable crash from untrusted data in a security scanner, which is the reason to bound it.
Suggested fix
Bound the nesting depth in the recursive descent and return a normal parse error past the limit, so a malformed or hostile license expression is rejected like any other invalid input instead of crashing the process. Real SPDX expressions nest only a few levels, so a generous ceiling (for example 1000) rejects nothing legitimate. A regression test drives a deeply nested expression and asserts an error rather than a crash.
CWE-674 (Uncontrolled Recursion).
I have this change and its test ready locally and can open a PR once assigned.
Environment
567f3ea