Skip to content

Commit ab37bc1

Browse files
authored
fix: prevent path duplication in attestation URL for registries with … (#452)
fix: prevent path duplication in attestation URL for registries with path components When a custom registry URL includes a path (e.g. https://example.com/javascript), the attestation URL was incorrectly constructed by concatenating the full registry URL with the full pathname from the attestation URL, causing the path to be duplicated (e.g. /javascript/javascript/-/npm/v1/attestations/...). Use the URL constructor to correctly resolve the pathname against the registry origin, matching the existing pattern in lib/remote.js. ## References Fixes #450
1 parent 8b8ea3b commit ab37bc1

2 files changed

Lines changed: 57 additions & 1 deletion

File tree

lib/registry.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ class RegistryFetcher extends Fetcher {
229229
if (this.opts.verifyAttestations) {
230230
// Always fetch attestations from the current registry host
231231
const attestationsPath = new URL(dist.attestations.url).pathname
232-
const attestationsUrl = removeTrailingSlashes(this.registry) + attestationsPath
232+
const attestationsUrl = new URL(attestationsPath, this.registry).href
233233
const res = await fetch(attestationsUrl, {
234234
...this.opts,
235235
// disable integrity check for attestations json payload, we check the

test/registry.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,62 @@ t.test('verifyAttestations valid attestations', async t => {
397397
t.ok(mani._integrity)
398398
})
399399

400+
t.test('verifyAttestations with registry path does not duplicate path', async t => {
401+
const customRegistry = 'https://registry.example.com/custom'
402+
403+
tnock(t, 'https://registry.example.com')
404+
.get('/custom/sigstore')
405+
.reply(200, {
406+
_id: 'sigstore',
407+
_rev: 'deadbeef',
408+
name: 'sigstore',
409+
'dist-tags': { latest: '0.4.0' },
410+
versions: {
411+
'0.4.0': {
412+
name: 'sigstore',
413+
version: '0.4.0',
414+
dist: {
415+
// eslint-disable-next-line max-len
416+
integrity: 'sha512-KCwMX6k20mQyFkNYG2XT3lwK9u1P36wS9YURFd85zCXPrwrSLZCEh7/vMBFNYcJXRiBtGDS+T4/RZZF493zABA==',
417+
// eslint-disable-next-line max-len
418+
attestations: { url: 'https://registry.example.com/custom/-/npm/v1/attestations/sigstore@0.4.0', provenance: { predicateType: 'https://slsa.dev/provenance/v0.2' } },
419+
},
420+
},
421+
},
422+
})
423+
424+
const fixture = fs.readFileSync(
425+
path.join(__dirname, 'fixtures', 'sigstore/valid-attestations.json'),
426+
'utf8'
427+
)
428+
429+
// This should fetch /custom/-/npm/v1/attestations/sigstore@0.4.0
430+
// NOT /custom/custom/-/npm/v1/attestations/sigstore@0.4.0
431+
tnock(t, 'https://registry.example.com')
432+
.get('/custom/-/npm/v1/attestations/sigstore@0.4.0')
433+
.reply(200, JSON.parse(fixture))
434+
435+
const f = new MockedRegistryFetcher('sigstore@0.4.0', {
436+
registry: customRegistry,
437+
cache,
438+
verifyAttestations: true,
439+
[`//registry.example.com/custom:_keys`]: [{
440+
expires: null,
441+
keyid: 'SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA',
442+
keytype: 'ecdsa-sha2-nistp256',
443+
scheme: 'ecdsa-sha2-nistp256',
444+
// eslint-disable-next-line max-len
445+
key: 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==',
446+
// eslint-disable-next-line max-len
447+
pemkey: '-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==\n-----END PUBLIC KEY-----',
448+
}],
449+
})
450+
451+
const mani = await f.manifest()
452+
t.ok(mani._attestations)
453+
t.ok(mani._integrity)
454+
})
455+
400456
t.test('verifyAttestations when registry returns no attestations', async t => {
401457
tnock(t, 'https://registry.npmjs.org')
402458
.get('/sigstore')

0 commit comments

Comments
 (0)