subresource-integrity.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. 'use strict'
  2. const assert = require('node:assert')
  3. const { runtimeFeatures } = require('../../util/runtime-features.js')
  4. /**
  5. * @typedef {object} Metadata
  6. * @property {SRIHashAlgorithm} alg - The algorithm used for the hash.
  7. * @property {string} val - The base64-encoded hash value.
  8. */
  9. /**
  10. * @typedef {Metadata[]} MetadataList
  11. */
  12. /**
  13. * @typedef {('sha256' | 'sha384' | 'sha512')} SRIHashAlgorithm
  14. */
  15. /**
  16. * @type {Map<SRIHashAlgorithm, number>}
  17. *
  18. * The valid SRI hash algorithm token set is the ordered set « "sha256",
  19. * "sha384", "sha512" » (corresponding to SHA-256, SHA-384, and SHA-512
  20. * respectively). The ordering of this set is meaningful, with stronger
  21. * algorithms appearing later in the set.
  22. *
  23. * @see https://w3c.github.io/webappsec-subresource-integrity/#valid-sri-hash-algorithm-token-set
  24. */
  25. const validSRIHashAlgorithmTokenSet = new Map([['sha256', 0], ['sha384', 1], ['sha512', 2]])
  26. // https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
  27. /** @type {import('node:crypto')} */
  28. let crypto
  29. if (runtimeFeatures.has('crypto')) {
  30. crypto = require('node:crypto')
  31. const cryptoHashes = crypto.getHashes()
  32. // If no hashes are available, we cannot support SRI.
  33. if (cryptoHashes.length === 0) {
  34. validSRIHashAlgorithmTokenSet.clear()
  35. }
  36. for (const algorithm of validSRIHashAlgorithmTokenSet.keys()) {
  37. // If the algorithm is not supported, remove it from the list.
  38. if (cryptoHashes.includes(algorithm) === false) {
  39. validSRIHashAlgorithmTokenSet.delete(algorithm)
  40. }
  41. }
  42. } else {
  43. // If crypto is not available, we cannot support SRI.
  44. validSRIHashAlgorithmTokenSet.clear()
  45. }
  46. /**
  47. * @typedef GetSRIHashAlgorithmIndex
  48. * @type {(algorithm: SRIHashAlgorithm) => number}
  49. * @param {SRIHashAlgorithm} algorithm
  50. * @returns {number} The index of the algorithm in the valid SRI hash algorithm
  51. * token set.
  52. */
  53. const getSRIHashAlgorithmIndex = /** @type {GetSRIHashAlgorithmIndex} */ (Map.prototype.get.bind(
  54. validSRIHashAlgorithmTokenSet))
  55. /**
  56. * @typedef IsValidSRIHashAlgorithm
  57. * @type {(algorithm: string) => algorithm is SRIHashAlgorithm}
  58. * @param {*} algorithm
  59. * @returns {algorithm is SRIHashAlgorithm}
  60. */
  61. const isValidSRIHashAlgorithm = /** @type {IsValidSRIHashAlgorithm} */ (
  62. Map.prototype.has.bind(validSRIHashAlgorithmTokenSet)
  63. )
  64. /**
  65. * @param {Uint8Array} bytes
  66. * @param {string} metadataList
  67. * @returns {boolean}
  68. *
  69. * @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist
  70. */
  71. const bytesMatch = runtimeFeatures.has('crypto') === false || validSRIHashAlgorithmTokenSet.size === 0
  72. // If node is not built with OpenSSL support, we cannot check
  73. // a request's integrity, so allow it by default (the spec will
  74. // allow requests if an invalid hash is given, as precedence).
  75. ? () => true
  76. : (bytes, metadataList) => {
  77. // 1. Let parsedMetadata be the result of parsing metadataList.
  78. const parsedMetadata = parseMetadata(metadataList)
  79. // 2. If parsedMetadata is empty set, return true.
  80. if (parsedMetadata.length === 0) {
  81. return true
  82. }
  83. // 3. Let metadata be the result of getting the strongest
  84. // metadata from parsedMetadata.
  85. const metadata = getStrongestMetadata(parsedMetadata)
  86. // 4. For each item in metadata:
  87. for (const item of metadata) {
  88. // 1. Let algorithm be the item["alg"].
  89. const algorithm = item.alg
  90. // 2. Let expectedValue be the item["val"].
  91. const expectedValue = item.val
  92. // See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e
  93. // "be liberal with padding". This is annoying, and it's not even in the spec.
  94. // 3. Let actualValue be the result of applying algorithm to bytes .
  95. const actualValue = applyAlgorithmToBytes(algorithm, bytes)
  96. // 4. If actualValue is a case-sensitive match for expectedValue,
  97. // return true.
  98. if (caseSensitiveMatch(actualValue, expectedValue)) {
  99. return true
  100. }
  101. }
  102. // 5. Return false.
  103. return false
  104. }
  105. /**
  106. * @param {MetadataList} metadataList
  107. * @returns {MetadataList} The strongest hash algorithm from the metadata list.
  108. */
  109. function getStrongestMetadata (metadataList) {
  110. // 1. Let result be the empty set and strongest be the empty string.
  111. const result = []
  112. /** @type {Metadata|null} */
  113. let strongest = null
  114. // 2. For each item in set:
  115. for (const item of metadataList) {
  116. // 1. Assert: item["alg"] is a valid SRI hash algorithm token.
  117. assert(isValidSRIHashAlgorithm(item.alg), 'Invalid SRI hash algorithm token')
  118. // 2. If result is the empty set, then:
  119. if (result.length === 0) {
  120. // 1. Append item to result.
  121. result.push(item)
  122. // 2. Set strongest to item.
  123. strongest = item
  124. // 3. Continue.
  125. continue
  126. }
  127. // 3. Let currentAlgorithm be strongest["alg"], and currentAlgorithmIndex be
  128. // the index of currentAlgorithm in the valid SRI hash algorithm token set.
  129. const currentAlgorithm = /** @type {Metadata} */ (strongest).alg
  130. const currentAlgorithmIndex = getSRIHashAlgorithmIndex(currentAlgorithm)
  131. // 4. Let newAlgorithm be the item["alg"], and newAlgorithmIndex be the
  132. // index of newAlgorithm in the valid SRI hash algorithm token set.
  133. const newAlgorithm = item.alg
  134. const newAlgorithmIndex = getSRIHashAlgorithmIndex(newAlgorithm)
  135. // 5. If newAlgorithmIndex is less than currentAlgorithmIndex, then continue.
  136. if (newAlgorithmIndex < currentAlgorithmIndex) {
  137. continue
  138. // 6. Otherwise, if newAlgorithmIndex is greater than
  139. // currentAlgorithmIndex:
  140. } else if (newAlgorithmIndex > currentAlgorithmIndex) {
  141. // 1. Set strongest to item.
  142. strongest = item
  143. // 2. Set result to « item ».
  144. result[0] = item
  145. result.length = 1
  146. // 7. Otherwise, newAlgorithmIndex and currentAlgorithmIndex are the same
  147. // value. Append item to result.
  148. } else {
  149. result.push(item)
  150. }
  151. }
  152. // 3. Return result.
  153. return result
  154. }
  155. /**
  156. * @param {string} metadata
  157. * @returns {MetadataList}
  158. *
  159. * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
  160. */
  161. function parseMetadata (metadata) {
  162. // 1. Let result be the empty set.
  163. /** @type {MetadataList} */
  164. const result = []
  165. // 2. For each item returned by splitting metadata on spaces:
  166. for (const item of metadata.split(' ')) {
  167. // 1. Let expression-and-options be the result of splitting item on U+003F (?).
  168. const expressionAndOptions = item.split('?', 1)
  169. // 2. Let algorithm-expression be expression-and-options[0].
  170. const algorithmExpression = expressionAndOptions[0]
  171. // 3. Let base64-value be the empty string.
  172. let base64Value = ''
  173. // 4. Let algorithm-and-value be the result of splitting algorithm-expression on U+002D (-).
  174. const algorithmAndValue = [algorithmExpression.slice(0, 6), algorithmExpression.slice(7)]
  175. // 5. Let algorithm be algorithm-and-value[0].
  176. const algorithm = algorithmAndValue[0]
  177. // 6. If algorithm is not a valid SRI hash algorithm token, then continue.
  178. if (!isValidSRIHashAlgorithm(algorithm)) {
  179. continue
  180. }
  181. // 7. If algorithm-and-value[1] exists, set base64-value to
  182. // algorithm-and-value[1].
  183. if (algorithmAndValue[1]) {
  184. base64Value = algorithmAndValue[1]
  185. }
  186. // 8. Let metadata be the ordered map
  187. // «["alg" → algorithm, "val" → base64-value]».
  188. const metadata = {
  189. alg: algorithm,
  190. val: base64Value
  191. }
  192. // 9. Append metadata to result.
  193. result.push(metadata)
  194. }
  195. // 3. Return result.
  196. return result
  197. }
  198. /**
  199. * Applies the specified hash algorithm to the given bytes
  200. *
  201. * @typedef {(algorithm: SRIHashAlgorithm, bytes: Uint8Array) => string} ApplyAlgorithmToBytes
  202. * @param {SRIHashAlgorithm} algorithm
  203. * @param {Uint8Array} bytes
  204. * @returns {string}
  205. */
  206. const applyAlgorithmToBytes = (algorithm, bytes) => {
  207. return crypto.hash(algorithm, bytes, 'base64')
  208. }
  209. /**
  210. * Compares two base64 strings, allowing for base64url
  211. * in the second string.
  212. *
  213. * @param {string} actualValue base64 encoded string
  214. * @param {string} expectedValue base64 or base64url encoded string
  215. * @returns {boolean}
  216. */
  217. function caseSensitiveMatch (actualValue, expectedValue) {
  218. // Ignore padding characters from the end of the strings by
  219. // decreasing the length by 1 or 2 if the last characters are `=`.
  220. let actualValueLength = actualValue.length
  221. if (actualValueLength !== 0 && actualValue[actualValueLength - 1] === '=') {
  222. actualValueLength -= 1
  223. }
  224. if (actualValueLength !== 0 && actualValue[actualValueLength - 1] === '=') {
  225. actualValueLength -= 1
  226. }
  227. let expectedValueLength = expectedValue.length
  228. if (expectedValueLength !== 0 && expectedValue[expectedValueLength - 1] === '=') {
  229. expectedValueLength -= 1
  230. }
  231. if (expectedValueLength !== 0 && expectedValue[expectedValueLength - 1] === '=') {
  232. expectedValueLength -= 1
  233. }
  234. if (actualValueLength !== expectedValueLength) {
  235. return false
  236. }
  237. for (let i = 0; i < actualValueLength; ++i) {
  238. if (
  239. actualValue[i] === expectedValue[i] ||
  240. (actualValue[i] === '+' && expectedValue[i] === '-') ||
  241. (actualValue[i] === '/' && expectedValue[i] === '_')
  242. ) {
  243. continue
  244. }
  245. return false
  246. }
  247. return true
  248. }
  249. module.exports = {
  250. applyAlgorithmToBytes,
  251. bytesMatch,
  252. caseSensitiveMatch,
  253. isValidSRIHashAlgorithm,
  254. getStrongestMetadata,
  255. parseMetadata
  256. }