From 1622864f9e263a8c0c1551a37ec4ecd65da35f30 Mon Sep 17 00:00:00 2001 From: Arpit Jain Date: Fri, 24 Jul 2026 01:47:16 +0900 Subject: [PATCH] guard against panic on malformed SPDX license expression spdxexp.ExtractLicenses can panic (nil pointer dereference) on some malformed expressions, for example one with a dangling opening parenthesis like "MIT AND (". SBOM license fields are decoded straight into pkg.License.SPDXExpression without revalidation, so a single malformed value in an input SBOM crashes the whole scan. Wrap the call in a small helper that recovers from the panic and returns an error, letting the existing error path fall back to treating the value as a non-SPDX license. syft guards its own call to the same parser for this reason (anchore/syft#1837). Adds a test that drives ConvertSyftLicenses with several malformed expressions and asserts it no longer panics. Signed-off-by: Arpit Jain --- grant/license.go | 21 ++++++++++++++++++++- grant/license_test.go | 44 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 grant/license_test.go diff --git a/grant/license.go b/grant/license.go index ead30c5..b4dd49b 100644 --- a/grant/license.go +++ b/grant/license.go @@ -1,6 +1,7 @@ package grant import ( + "fmt" "strings" "github.com/github/go-spdx/v2/spdxexp" @@ -75,8 +76,26 @@ func ConvertSyftLicenses(set syftPkg.LicenseSet) (licenses []License) { return licenses } +// safeExtractLicenses wraps spdxexp.ExtractLicenses so a malformed SPDX +// expression cannot take down the whole scan. The upstream parser +// (github.com/github/go-spdx) panics on some inputs, for example an expression +// with a dangling opening parenthesis like "MIT AND (". SBOM license fields +// flow through here unvalidated, so a single malformed value in an SBOM would +// otherwise crash grant. syft guards its own call to this parser for +// the same reason (see anchore/syft#1837); grant does the same here and lets the +// existing error path fall back to treating the value as a non-SPDX license. +func safeExtractLicenses(expression string) (extracted []string, err error) { + defer func() { + if r := recover(); r != nil { + extracted = nil + err = fmt.Errorf("recovered from panic parsing SPDX expression %q: %v", expression, r) + } + }() + return spdxexp.ExtractLicenses(expression) +} + func handleSPDXLicense(license syftPkg.License, licenses []License, licenseLocations []string, checked map[string]bool) []License { - extractedLicenses, err := spdxexp.ExtractLicenses(license.SPDXExpression) + extractedLicenses, err := safeExtractLicenses(license.SPDXExpression) if err != nil { // log.Errorf("unable to extract licenses from SPDX expression: %s", license.SPDXExpression) return addNonSPDXLicense(licenses, license, licenseLocations) diff --git a/grant/license_test.go b/grant/license_test.go new file mode 100644 index 0000000..949f6d5 --- /dev/null +++ b/grant/license_test.go @@ -0,0 +1,44 @@ +package grant + +import ( + "testing" + + syftPkg "github.com/anchore/syft/syft/pkg" +) + +// TestConvertSyftLicenses_MalformedSPDXExpressionDoesNotPanic feeds a license +// whose SPDXExpression is a malformed expression with a dangling opening +// parenthesis. This is the shape that reaches handleSPDXLicense when grant +// decodes an SBOM whose license field is malformed. The upstream SPDX parser +// panics on this input, so without the guard in handleSPDXLicense the whole scan +// would crash. The expression should instead fall back to a non-SPDX license. +func TestConvertSyftLicenses_MalformedSPDXExpressionDoesNotPanic(t *testing.T) { + malformed := []string{ + "MIT AND (", + "(", + "(((((", + "MIT OR (", + } + + for _, expr := range malformed { + expr := expr + t.Run(expr, func(t *testing.T) { + set := syftPkg.NewLicenseSet(syftPkg.License{Value: expr, SPDXExpression: expr}) + + // must not panic + got := ConvertSyftLicenses(set) + + if len(got) != 1 { + t.Fatalf("expected 1 license, got %d", len(got)) + } + // the malformed expression should be carried as a plain name, not + // treated as a valid SPDX expression + if got[0].IsSPDX() { + t.Fatalf("expected malformed expression %q to fall back to a non-SPDX license, got SPDX license %q", expr, got[0].SPDXExpression) + } + if got[0].Name != expr { + t.Fatalf("expected fallback license name %q, got %q", expr, got[0].Name) + } + }) + } +}