From 8d66400362cb44c8f29725dc6e68bd52c526e3d5 Mon Sep 17 00:00:00 2001 From: UB Date: Mon, 29 Jun 2026 20:34:52 +0530 Subject: [PATCH] fix(sbom): percent-encode vcs_url qualifier in generated purls (#9670) Both SBOM generators build a git package's purl by sticking the raw `node.resolved` straight into the `vcs_url` qualifier, so any `#` or `&` in that resolved URL escapes the qualifier value. A git dep resolving to e.g. `https://github.com/foo/bar.git?a=b&c=d#1234` produces `pkg:npm/...?vcs_url=https://github.com/foo/bar.git?a=b&c=d#1234`, where a purl parser reads `c=d` as a separate qualifier and `1234` as the subpath. Wrapping `node.resolved` in `encodeURIComponent` at both sites keeps it a single qualifier value; the existing git-url snapshots and two new assertions cover it. (cherry picked from commit 024e6d98fd0262c87c3207387d324b7a9274aed8) --- lib/utils/sbom-cyclonedx.js | 2 +- lib/utils/sbom-spdx.js | 2 +- .../test/lib/utils/sbom-cyclonedx.js.test.cjs | 2 +- tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs | 2 +- test/lib/utils/sbom-cyclonedx.js | 11 +++++++++++ test/lib/utils/sbom-spdx.js | 11 +++++++++++ 6 files changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/utils/sbom-cyclonedx.js b/lib/utils/sbom-cyclonedx.js index fe368e968baaa..bd741718bef0d 100644 --- a/lib/utils/sbom-cyclonedx.js +++ b/lib/utils/sbom-cyclonedx.js @@ -76,7 +76,7 @@ const toCyclonedxItem = (node, { packageType }) => { // Calculate purl from package spec let spec = npa(node.pkgid) spec = (spec.type === 'alias') ? spec.subSpec : spec - const purl = npa.toPurl(spec) + (isGitNode(node) ? `?vcs_url=${node.resolved}` : '') + const purl = npa.toPurl(spec) + (isGitNode(node) ? `?vcs_url=${encodeURIComponent(node.resolved)}` : '') if (node.package) { const toNormalize = new PackageJson() diff --git a/lib/utils/sbom-spdx.js b/lib/utils/sbom-spdx.js index 8ea75c688bc86..88797fd03effb 100644 --- a/lib/utils/sbom-spdx.js +++ b/lib/utils/sbom-spdx.js @@ -109,7 +109,7 @@ const toSpdxItem = (node, { packageType }) => { // Calculate purl from package spec let spec = npa(node.pkgid) spec = (spec.type === 'alias') ? spec.subSpec : spec - const purl = npa.toPurl(spec) + (isGitNode(node) ? `?vcs_url=${node.resolved}` : '') + const purl = npa.toPurl(spec) + (isGitNode(node) ? `?vcs_url=${encodeURIComponent(node.resolved)}` : '') /* For workspace nodes, use the location from their linkNode */ let location = node.location diff --git a/tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs b/tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs index 124478bc82993..6963299566d53 100644 --- a/tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs +++ b/tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs @@ -417,7 +417,7 @@ exports[`test/lib/utils/sbom-cyclonedx.js TAP single node - from git url > must "version": "1.0.0", "scope": "required", "author": "Author", - "purl": "pkg:npm/root@1.0.0?vcs_url=https://github.com/foo/bar#1234", + "purl": "pkg:npm/root@1.0.0?vcs_url=https%3A%2F%2Fgithub.com%2Ffoo%2Fbar%231234", "properties": [], "externalReferences": [ { diff --git a/tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs b/tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs index 6adb6d26de143..eda01bcaa4ed6 100644 --- a/tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs +++ b/tap-snapshots/test/lib/utils/sbom-spdx.js.test.cjs @@ -414,7 +414,7 @@ exports[`test/lib/utils/sbom-spdx.js TAP single node - from git url > must match { "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", - "referenceLocator": "pkg:npm/root@1.0.0?vcs_url=https://github.com/foo/bar#1234" + "referenceLocator": "pkg:npm/root@1.0.0?vcs_url=https%3A%2F%2Fgithub.com%2Ffoo%2Fbar%231234" } ] } diff --git a/test/lib/utils/sbom-cyclonedx.js b/test/lib/utils/sbom-cyclonedx.js index ea569d41c57d8..f3105e3cd4879 100644 --- a/test/lib/utils/sbom-cyclonedx.js +++ b/test/lib/utils/sbom-cyclonedx.js @@ -257,6 +257,17 @@ t.test('single node - from git url', t => { t.end() }) +t.test('git url with special chars is encoded into the vcs_url qualifier', t => { + const node = { ...root, type: 'git', resolved: 'https://github.com/foo/bar.git?a=b&c=d#1234' } + const res = cyclonedxOutput({ npm, nodes: [node] }) + const { purl } = res.metadata.component + // everything after vcs_url= must be a single percent-encoded value, so the + // committish/query can't leak out as an extra purl qualifier or subpath + t.equal(purl, 'pkg:npm/root@1.0.0?vcs_url=https%3A%2F%2Fgithub.com%2Ffoo%2Fbar.git%3Fa%3Db%26c%3Dd%231234') + t.notMatch(purl.split('vcs_url=')[1], /[#&]/) + t.end() +}) + t.test('single node - no package info', t => { const node = { ...root, package: undefined } const res = cyclonedxOutput({ npm, nodes: [node] }) diff --git a/test/lib/utils/sbom-spdx.js b/test/lib/utils/sbom-spdx.js index d2599b0824510..1e21c945ca75d 100644 --- a/test/lib/utils/sbom-spdx.js +++ b/test/lib/utils/sbom-spdx.js @@ -223,6 +223,17 @@ t.test('single node - from git url', t => { t.end() }) +t.test('git url with special chars is encoded into the vcs_url qualifier', t => { + const node = { ...root, type: 'git', resolved: 'https://github.com/foo/bar.git?a=b&c=d#1234' } + const res = spdxOutput({ npm, nodes: [node] }) + const purl = res.packages + .find(p => p.SPDXID === 'SPDXRef-Package-root-1.0.0') + .externalRefs.find(r => r.referenceType === 'purl').referenceLocator + t.equal(purl, 'pkg:npm/root@1.0.0?vcs_url=https%3A%2F%2Fgithub.com%2Ffoo%2Fbar.git%3Fa%3Db%26c%3Dd%231234') + t.notMatch(purl.split('vcs_url=')[1], /[#&]/) + t.end() +}) + t.test('single node - linked', t => { const node = { ...root, isLink: true, target: { edgesOut: [] } } const res = spdxOutput({ npm, nodes: [node] })