Skip to content

Commit 806e8c8

Browse files
committed
fix: Do not blow up if the opts object is mutated
Pacote has a use case where the integrity value may not be known at the outset, but is later established, either via the dist.integrity in a packument, or by the x-local-hash header value when make-fetch-happen loads a response from the cache. In these cases, we have already started an integrity stream at the beginning of the request, and don't get the expected integrity until _after_ the integrity stream is created, resulting in a spurious EINTEGRITY error. This patch makes ssri responsive to (and resilient against) updates to the integrity and size options after the stream has started.
1 parent cea474f commit 806e8c8

File tree

2 files changed

+94
-26
lines changed

2 files changed

+94
-26
lines changed

index.js

+34-26
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,40 @@ const SsriOpts = figgyPudding({
2323
strict: { default: false }
2424
})
2525

26+
const getOptString = options => !options || !options.length ? ''
27+
: `?${options.join('?')}`
28+
29+
const _onEnd = Symbol('_onEnd')
30+
const _getOptions = Symbol('_getOptions')
2631
class IntegrityStream extends MiniPass {
2732
constructor (opts) {
2833
super()
2934
this.size = 0
3035
this.opts = opts
31-
// For verification
32-
this.sri = opts.integrity && parse(opts.integrity, opts)
33-
this.goodSri = this.sri && Object.keys(this.sri).length
34-
this.algorithm = this.goodSri && this.sri.pickAlgorithm(opts)
35-
this.digests = this.goodSri && this.sri[this.algorithm]
36-
// Calculating stream
36+
37+
// may be overridden later, but set now for class consistency
38+
this[_getOptions]()
39+
40+
// options used for calculating stream. can't be changed.
3741
this.algorithms = Array.from(
3842
new Set(opts.algorithms.concat(this.algorithm ? [this.algorithm] : []))
3943
)
4044
this.hashes = this.algorithms.map(crypto.createHash)
41-
this.onEnd = this.onEnd.bind(this)
45+
}
46+
47+
[_getOptions] () {
48+
const opts = this.opts
49+
// For verification
50+
this.sri = opts.integrity && parse(opts.integrity, opts)
51+
this.expectedSize = opts.size
52+
this.goodSri = this.sri ? Object.keys(this.sri).length || null : null
53+
this.algorithm = this.goodSri ? this.sri.pickAlgorithm(opts) : null
54+
this.digests = this.goodSri ? this.sri[this.algorithm] : null
55+
this.optString = getOptString(opts.options)
4256
}
4357

4458
emit (ev, data) {
45-
if (ev === 'end') this.onEnd()
59+
if (ev === 'end') this[_onEnd]()
4660
return super.emit(ev, data)
4761
}
4862

@@ -52,23 +66,23 @@ class IntegrityStream extends MiniPass {
5266
return super.write(data)
5367
}
5468

55-
onEnd () {
56-
const optString = (this.opts.options && this.opts.options.length)
57-
? `?${this.opts.options.join('?')}`
58-
: ''
69+
[_onEnd] () {
70+
if (!this.goodSri) {
71+
this[_getOptions]()
72+
}
5973
const newSri = parse(this.hashes.map((h, i) => {
60-
return `${this.algorithms[i]}-${h.digest('base64')}${optString}`
74+
return `${this.algorithms[i]}-${h.digest('base64')}${this.optString}`
6175
}).join(' '), this.opts)
6276
// Integrity verification mode
6377
const match = this.goodSri && newSri.match(this.sri, this.opts)
64-
if (typeof this.opts.size === 'number' && this.size !== this.opts.size) {
65-
const err = new Error(`stream size mismatch when checking ${this.sri}.\n Wanted: ${this.opts.size}\n Found: ${this.size}`)
78+
if (typeof this.expectedSize === 'number' && this.size !== this.expectedSize) {
79+
const err = new Error(`stream size mismatch when checking ${this.sri}.\n Wanted: ${this.expectedSize}\n Found: ${this.size}`)
6680
err.code = 'EBADSIZE'
6781
err.found = this.size
68-
err.expected = this.opts.size
82+
err.expected = this.expectedSize
6983
err.sri = this.sri
7084
this.emit('error', err)
71-
} else if (this.opts.integrity && !match) {
85+
} else if (this.sri && !match) {
7286
const err = new Error(`${this.sri} integrity checksum failed when using ${this.algorithm}: wanted ${this.digests} but got ${newSri}. (${this.size} bytes)`)
7387
err.code = 'EINTEGRITY'
7488
err.found = newSri
@@ -260,9 +274,7 @@ function stringify (obj, opts) {
260274
module.exports.fromHex = fromHex
261275
function fromHex (hexDigest, algorithm, opts) {
262276
opts = SsriOpts(opts)
263-
const optString = opts.options && opts.options.length
264-
? `?${opts.options.join('?')}`
265-
: ''
277+
const optString = getOptString(opts.options)
266278
return parse(
267279
`${algorithm}-${
268280
Buffer.from(hexDigest, 'hex').toString('base64')
@@ -274,9 +286,7 @@ module.exports.fromData = fromData
274286
function fromData (data, opts) {
275287
opts = SsriOpts(opts)
276288
const algorithms = opts.algorithms
277-
const optString = opts.options && opts.options.length
278-
? `?${opts.options.join('?')}`
279-
: ''
289+
const optString = getOptString(opts.options)
280290
return algorithms.reduce((acc, algo) => {
281291
const digest = crypto.createHash(algo).update(data).digest('base64')
282292
const hash = new Hash(
@@ -375,9 +385,7 @@ module.exports.create = createIntegrity
375385
function createIntegrity (opts) {
376386
opts = SsriOpts(opts)
377387
const algorithms = opts.algorithms
378-
const optString = opts.options.length
379-
? `?${opts.options.join('?')}`
380-
: ''
388+
const optString = getOptString(opts.options)
381389

382390
const hashes = algorithms.map(crypto.createHash)
383391

test/mutable-opts-resilience.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
const ssri = require('../')
2+
const t = require('tap')
3+
4+
const data = 'hello world'
5+
const expectIntegrity = ssri.fromData(data, { algorithms: ['sha512'] })
6+
const expectSize = data.length
7+
8+
t.test('support adding bad integrity later', t => {
9+
const opts = {}
10+
const stream = ssri.integrityStream(opts)
11+
opts.integrity = ssri.parse('sha512-deepbeets')
12+
return t.rejects(stream.end(data).collect(), {
13+
code: 'EINTEGRITY'
14+
})
15+
})
16+
17+
t.test('support adding bad integrity string later', t => {
18+
const opts = {}
19+
const stream = ssri.integrityStream(opts)
20+
opts.integrity = 'sha512-deepbeets'
21+
return t.rejects(stream.end(data).collect(), {
22+
code: 'EINTEGRITY'
23+
})
24+
})
25+
26+
t.test('support adding bad size later', t => {
27+
const opts = {}
28+
const stream = ssri.integrityStream(opts)
29+
opts.size = 2
30+
return t.rejects(stream.end(data).collect(), {
31+
code: 'EBADSIZE'
32+
})
33+
})
34+
35+
t.test('support adding good integrity later', t => {
36+
const opts = {}
37+
const stream = ssri.integrityStream(opts)
38+
opts.integrity = expectIntegrity
39+
return stream.end(data).on('verified', match => {
40+
t.same(match, expectIntegrity.sha512[0])
41+
}).collect()
42+
})
43+
44+
t.test('support adding good integrity string later', t => {
45+
const opts = {}
46+
const stream = ssri.integrityStream(opts)
47+
opts.integrity = String(expectIntegrity)
48+
return stream.end(data).on('verified', match => {
49+
t.same(match, expectIntegrity.sha512[0])
50+
}).collect()
51+
})
52+
53+
t.test('support adding good size later', t => {
54+
const opts = {}
55+
const stream = ssri.integrityStream(opts)
56+
opts.size = expectSize
57+
return stream.end(data).on('size', size => {
58+
t.same(size, expectSize)
59+
}).collect()
60+
})

0 commit comments

Comments
 (0)