Skip to content

Commit 3f0b64c

Browse files
committed
feat(strict-mode): strict SRI support
BREAKING CHANGE: functions that accepted an optional `sep` argument now expect `opts.sep`.
1 parent 2dc11d1 commit 3f0b64c

File tree

2 files changed

+120
-60
lines changed

2 files changed

+120
-60
lines changed

README.md

+42-25
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ ssri.checkData(fs.readFileSync('./my-file'), parsed) // => true
5858
* Optional use of reserved `option-expression` syntax.
5959
* Multiple entries for the same algorithm.
6060
* Object-based integrity string manipulation.
61+
* Optional strict parsing that follows the spec as closely as possible.
6162

6263
### Contributing
6364

@@ -69,10 +70,12 @@ jump in if you'd like to, or even ask us questions if something isn't clear.
6970

7071
### API
7172

72-
#### <a name="parse"></a> `> ssri.parse(integrityString) -> Integrity`
73+
#### <a name="parse"></a> `> ssri.parse(sri, [opts]) -> Integrity`
7374

74-
Parses an `integrity` string into an `Integrity` data structure. The resulting
75-
object has this shape:
75+
Parses `sri` into an `Integrity` data structure. `sri` can be an integrity
76+
string, an `IntegrityMetadata`-like with `digest` and `algorithm` fields and an
77+
optional `options` field, or an `Integrity`-like object. The resulting object
78+
will be an `Integrity` instance that has this shape:
7679

7780
```javascript
7881
{
@@ -84,20 +87,31 @@ object has this shape:
8487
}
8588
```
8689

90+
If `opts.strict` is truthy, the resulting object will be filtered such that
91+
it strictly follows the Subresource Integrity spec, throwing away any entries
92+
with any invalid components. This also means a restricted set of algorithms
93+
will be used -- the spec limits them to `sha256`, `sha384`, and `sha512`.
94+
95+
Strict mode is recommended if the integrity strings are intended for use in
96+
browsers, or in other situations where strict adherence to the spec is needed.
97+
8798
##### Example
8899

89100
```javascript
90101
ssri.parse('sha512-9KhgCRIx/AmzC8xqYJTZRrnO8OW2Pxyl2DIMZSBOr0oDvtEFyht3xpp71j/r/pAe1DM+JI/A+line3jUBgzQ7A==?foo') // -> Integrity
91102
```
92103

93-
#### <a name="integrity-concat"></a> `> Integrity#concat(otherIntegrity) -> Integrity`
104+
#### <a name="integrity-concat"></a> `> Integrity#concat(otherIntegrity, [opts]) -> Integrity`
94105

95106
Concatenates an `Integrity` object with another IntegrityLike, or a string
96107
representing integrity metadata.
97108

98109
This is functionally equivalent to concatenating the string format of both
99110
integrity arguments, and calling [`ssri.parse`](#ssri-parse) on the new string.
100111

112+
If `opts.strict` is true, the new `Integrity` will be created using strict
113+
parsing rules. See [`ssri.parse`](#parse).
114+
101115
##### Example
102116

103117
```javascript
@@ -113,14 +127,17 @@ const mobileIntegrity = ssri.fromData(fs.readFileSync('./index.mobile.js'))
113127
desktopIntegrity.concat(mobileIntegrity)
114128
```
115129

116-
#### <a name="integrity-to-string"></a> `> Integrity#toString([sep=' ']) -> String`
130+
#### <a name="integrity-to-string"></a> `> Integrity#toString([opts]) -> String`
117131

118132
Returns the string representation of an `Integrity` object. All metadata entries
119-
will be concatenated in the string by `sep`.
133+
will be concatenated in the string by `opts.sep`, which defaults to `' '`.
120134

121135
If you want to serialize an object that didn't from from an `ssri` function,
122136
use [`ssri.serialize()`](#serialize).
123137

138+
If `opts.strict` is true, the integrity string will be created using strict
139+
parsing rules. See [`ssri.parse`](#parse).
140+
124141
##### Example
125142

126143
```javascript
@@ -129,25 +146,19 @@ const integrity = 'sha512-9KhgCRIx/AmzC8xqYJTZRrnO8OW2Pxyl2DIMZSBOr0oDvtEFyht3xp
129146
ssri.parse(integrity).toString() === integrity
130147
```
131148

132-
#### <a name="serialize"></a> `> ssri.serialize(integrityObj, [sep=' ']) -> String`
149+
#### <a name="serialize"></a> `> ssri.serialize(sri, [opts]) -> String`
133150

134151
This function is identical to [`Integrity#toString()`](#integrity-to-string),
135-
except it can be used on _any_ object resembling the shape of either an
136-
`Integrity` or an `IntegrityMedatada` object.
137-
138-
If `IntegrityLike` has both `.algorithm` and `.digest` properties, it will be
139-
serialized as a single integrity entry. That is, `<algorithm>-<digest>`, along
140-
with `?<options.join('?')>` if the object has an `options` property.
152+
except it can be used on _any_ object that [`parse`](#parse) can handle -- that
153+
is, a string, an `IntegrityMetadata`-like, or an `Integrity`-like.
141154

142-
Otherwise, the `IntegrityLike` will be treated as a full `Integrity` object,
143-
where every key on the object will be interpreted as an algorithm, and each
144-
value should be an array of metadata objects (with `algorithm` and `digest`
145-
properties) corresponding to that key.
146-
147-
The `sep` option defines the string to use when joining multiple entries
155+
The `opts.sep` option defines the string to use when joining multiple entries
148156
together. To be spec-compliant, this _must_ be whitespace. The default is a
149157
single space (`' '`).
150158

159+
If `opts.strict` is true, the integrity string will be created using strict
160+
parsing rules. See [`ssri.parse`](#parse).
161+
151162
##### Example
152163

153164
```javascript
@@ -190,6 +201,9 @@ strings that will be added to all generated integrity metadata generated by
190201
specified semantics besides being `?`-separated. Use at your own risk, and
191202
probably avoid if your integrity strings are meant to be used with browsers.
192203

204+
If `opts.strict` is true, the integrity object will be created using strict
205+
parsing rules. See [`ssri.parse`](#parse).
206+
193207
##### Example
194208

195209
```javascript
@@ -214,6 +228,9 @@ part of [`ssri.fromData`](#from-data).
214228
Additionally, `opts.Promise` may be passed in to inject a Promise library of
215229
choice. By default, ssri will use Node's built-in Promises.
216230

231+
If `opts.strict` is true, the integrity object will be created using strict
232+
parsing rules. See [`ssri.parse`](#parse).
233+
217234
##### Example
218235

219236
```javascript
@@ -227,8 +244,8 @@ ssri.fromStream(fs.createReadStream('index.js'), {
227244
#### <a name="check-data"></a> `> ssri.checkData(data, sri, [opts]) -> Algorithm|false`
228245

229246
Verifies `data` integrity against an `sri` argument. `data` may be either a
230-
`String` or a `Buffer`, and `sri` can be any `Integrity`-like, or a `String`
231-
that [`ssri.parse`](#parse) can turn into one.
247+
`String` or a `Buffer`, and `sri` can be any subresource integrity
248+
representation that [`ssri.parse`](#parse) can handle.
232249

233250
If verification succeeds, `checkData` will return the name of the algorithm that
234251
was used for verification (a truthy value). Otherwise, it will return `false`.
@@ -252,8 +269,8 @@ ssri.checkData(data, 'sha1-BaDDigEST') // -> false
252269
#### <a name="check-stream"></a> `> ssri.checkStream(stream, sri, [opts]) -> Promise<Algorithm>`
253270

254271
Verifies the contents of `stream` against an `sri` argument. `stream` will be
255-
consumed in its entirety by this process. `sri` can be any `Integrity`-like, or
256-
a `String` that [`ssri.parse`](#parse) can turn into one.
272+
consumed in its entirety by this process. `sri` can be any subresource integrity
273+
representation that [`ssri.parse`](#parse) can handle.
257274

258275
`checkStream` will return a Promise that either resolves to the string name of
259276
the algorithm that verification was done with, or, if the verification fails or
@@ -293,8 +310,8 @@ ssri.checkStream(
293310
#### <a name="create-checker-stream"></a> `> createCheckerStream(sri, [opts]) -> CheckerStream`
294311

295312
Returns a `Through` stream that data can be piped through in order to check it
296-
against `sri`. `sri` can be any `Integrity`-like, or a `String` that
297-
[`ssri.parse`](#parse) can turn into one.
313+
against `sri`. `sri` can be any subresource integrity representation that
314+
[`ssri.parse`](#parse) can handle.
298315

299316
If verification fails, the returned stream will error with an `EBADCHECKSUM`
300317
error code.

index.js

+78-35
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,101 @@
33
const crypto = require('crypto')
44
const Transform = require('stream').Transform
55

6-
const SRI_REGEX = /([^-]+)-([^?]+)([?\S*]*)/
6+
const SPEC_ALGORITHMS = ['sha256', 'sha384', 'sha512']
7+
8+
const BASE64_REGEX = /[a-z0-9+/]+(?:=?=?)/i
9+
const SRI_REGEX = /^([^-]+)-([^?]+)([?\S*]*)$/
10+
const STRICT_SRI_REGEX = /^([^-]+)-([A-Za-z0-9+/]+(?:=?=?))([?\x21-\x7E]*)$/
11+
const VCHAR_REGEX = /[\x21-\x7E]+/
712

813
class IntegrityMetadata {
9-
constructor (metadata) {
10-
this.source = metadata
14+
constructor (metadata, opts) {
15+
const strict = !!(opts && opts.strict)
16+
this.source = metadata.trim()
1117
// 3.1. Integrity metadata
1218
// https://w3c.github.io/webappsec-subresource-integrity/#integrity-metadata-description
13-
const match = metadata.match(SRI_REGEX)
19+
const match = this.source.match(
20+
strict
21+
? STRICT_SRI_REGEX
22+
: SRI_REGEX
23+
)
1424
if (!match) { return }
25+
if (strict && !SPEC_ALGORITHMS.some(a => a === match[1])) { return }
1526
this.algorithm = match[1]
1627
this.digest = match[2]
1728

1829
const rawOpts = match[3]
1930
this.options = rawOpts ? rawOpts.slice(1).split('?') : []
2031
}
21-
toString () {
22-
const opts = this.options && this.options.length
32+
toString (opts) {
33+
if (opts && opts.strict) {
34+
// Strict mode enforces the standard as close to the foot of the
35+
// letter as it can.
36+
if (!(
37+
// The spec has very restricted productions for algorithms.
38+
// https://www.w3.org/TR/CSP2/#source-list-syntax
39+
SPEC_ALGORITHMS.some(x => x === this.algorithm) &&
40+
// Usually, if someone insists on using a "different" base64, we
41+
// leave it as-is, since there's multiple standards, and the
42+
// specified is not a URL-safe variant.
43+
// https://www.w3.org/TR/CSP2/#base64_value
44+
this.digest.match(BASE64_REGEX) &&
45+
// Option syntax is strictly visual chars.
46+
// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-option-expression
47+
// https://tools.ietf.org/html/rfc5234#appendix-B.1
48+
(this.options || []).every(opt => opt.match(VCHAR_REGEX))
49+
)) {
50+
return ''
51+
}
52+
}
53+
const options = this.options && this.options.length
2354
? `?${this.options.join('?')}`
2455
: ''
25-
return `${this.algorithm}-${this.digest}${opts}`
56+
return `${this.algorithm}-${this.digest}${options}`
2657
}
2758
}
2859

2960
class Integrity {
30-
toString (sep) {
31-
sep = sep || ' '
61+
toString (opts) {
62+
opts = opts || {}
63+
let sep = opts.sep || ' '
64+
if (opts.strict) {
65+
// Entries must be separated by whitespace, according to spec.
66+
sep = sep.replace(/\S+/g, ' ')
67+
}
3268
return Object.keys(this).map(k => {
3369
return this[k].map(meta => {
34-
return IntegrityMetadata.prototype.toString.call(meta)
35-
})
36-
}).join(sep)
70+
return IntegrityMetadata.prototype.toString.call(meta, opts)
71+
}).filter(x => x.length).join(sep)
72+
}).filter(x => x.length).join(sep)
3773
}
38-
concat (integrity) {
74+
concat (integrity, opts) {
3975
const other = typeof integrity === 'string'
4076
? integrity
4177
: serialize(integrity)
42-
return parse(`${this.toString()} ${other}`)
78+
return parse(`${this.toString()} ${other}`, opts)
4379
}
4480
}
4581

4682
module.exports.parse = parse
47-
function parse (integrity) {
83+
function parse (sri, opts) {
84+
opts = opts || {}
85+
if (typeof sri === 'string') {
86+
return _parse(sri, opts)
87+
} else if (sri.algorithm && sri.digest) {
88+
const fullSri = new Integrity()
89+
fullSri[sri.algorithm] = [sri]
90+
return _parse(serialize(fullSri, opts), opts)
91+
} else {
92+
return _parse(serialize(sri, opts), opts)
93+
}
94+
}
95+
96+
function _parse (integrity, opts) {
4897
// 3.4.3. Parse metadata
4998
// https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
5099
return integrity.trim().split(/\s+/).reduce((acc, string) => {
51-
const metadata = new IntegrityMetadata(string)
100+
const metadata = new IntegrityMetadata(string, opts)
52101
if (metadata.algorithm && metadata.digest) {
53102
const algo = metadata.algorithm
54103
if (!acc[algo]) { acc[algo] = [] }
@@ -60,11 +109,11 @@ function parse (integrity) {
60109

61110
module.exports.serialize = serialize
62111
module.exports.unparse = serialize
63-
function serialize (obj, sep) {
112+
function serialize (obj, opts) {
64113
if (obj.algorithm && obj.digest) {
65-
return IntegrityMetadata.prototype.toString.call(obj)
114+
return IntegrityMetadata.prototype.toString.call(obj, opts)
66115
} else {
67-
return Integrity.prototype.toString.call(obj, sep)
116+
return Integrity.prototype.toString.call(obj, opts)
68117
}
69118
}
70119

@@ -77,7 +126,10 @@ function fromData (data, opts) {
77126
: ''
78127
return algorithms.reduce((acc, algo) => {
79128
const digest = crypto.createHash(algo).update(data).digest('base64')
80-
const meta = new IntegrityMetadata(`${algo}-${digest}${optString}`)
129+
const meta = new IntegrityMetadata(
130+
`${algo}-${digest}${optString}`,
131+
opts
132+
)
81133
if (meta.algorithm && meta.digest) {
82134
const algo = meta.algorithm
83135
if (!acc[algo]) { acc[algo] = [] }
@@ -103,7 +155,10 @@ function fromStream (stream, opts) {
103155
resolve(algorithms.reduce((acc, algo, i) => {
104156
const hash = hashes[i]
105157
const digest = hash.digest('base64')
106-
const meta = new IntegrityMetadata(`${algo}-${digest}${optString}`)
158+
const meta = new IntegrityMetadata(
159+
`${algo}-${digest}${optString}`,
160+
opts
161+
)
107162
if (meta.algorithm && meta.digest) {
108163
const algo = meta.algorithm
109164
if (!acc[algo]) { acc[algo] = [] }
@@ -118,13 +173,7 @@ function fromStream (stream, opts) {
118173
module.exports.checkData = checkData
119174
function checkData (data, sri, opts) {
120175
opts = opts || {}
121-
if (typeof sri === 'string') {
122-
sri = parse(sri)
123-
} else if (sri.algorithm && sri.digest) {
124-
const fullSri = new Integrity()
125-
fullSri[sri.algorithm] = [sri]
126-
sri = fullSri
127-
}
176+
sri = parse(sri, opts)
128177
const pickAlgorithm = opts.pickAlgorithm || getPrioritizedHash
129178
const algorithm = Object.keys(sri).reduce((acc, algo) => {
130179
return pickAlgorithm(acc, algo) || acc
@@ -152,13 +201,7 @@ function checkStream (stream, sri, opts) {
152201
module.exports.createCheckerStream = createCheckerStream
153202
function createCheckerStream (sri, opts) {
154203
opts = opts || {}
155-
if (typeof sri === 'string') {
156-
sri = parse(sri)
157-
} else if (sri.algorithm && sri.digest) {
158-
const fullSri = new Integrity()
159-
fullSri[sri.algorithm] = [sri]
160-
sri = fullSri
161-
}
204+
sri = parse(sri, opts)
162205
const pickAlgorithm = opts.pickAlgorithm || getPrioritizedHash
163206
const algorithm = Object.keys(sri).reduce((acc, algo) => {
164207
return pickAlgorithm(acc, algo) || acc

0 commit comments

Comments
 (0)