Skip to content

Commit 55043d3

Browse files
committed
feat: support Subresource Integrity via integrity option
1 parent 7b39bed commit 55043d3

File tree

9 files changed

+152
-5
lines changed

9 files changed

+152
-5
lines changed

docs/config/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,17 @@ module.exports = {
173173

174174
See also: [CROS setting attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes)
175175

176+
### integrity
177+
178+
- Type: `boolean`
179+
- Default: `false`
180+
181+
Set to `true` to enable [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) (SRI) on `<link rel="stylesheet">` and `<script>` tags in generated HTML. If you are hosting your built files on a CDN, it is a good idea to enable this for additional security.
182+
183+
Note that this only affects tags injected by `html-webpack-plugin` - tags directly added in the source template (`public/index.html`) are not affected.
184+
185+
Also, when SRI is enabled, preload resource hints are disabled due to a [bug in Chrome](https://bugs.chromium.org/p/chromium/issues/detail?id=677022) which causes the resources to be downloaded twice.
186+
176187
### configureWebpack
177188

178189
- Type: `Object | Function`

docs/zh/config/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,17 @@ module.exports = {
171171

172172
更多细节可查阅: [CROS setting attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes)
173173

174+
### integrity
175+
176+
- Type: `boolean`
177+
- Default: `false`
178+
179+
在生成的 HTML 中的 `<link rel="stylesheet">``<script>` 标签上启用 [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) (SRI)。如果你构建后的文件是部署在 CDN 上的,启用该选项可以提供额外的安全性。
180+
181+
需要注意的是该选项仅影响由 `html-webpack-plugin` 在构建时注入的标签 - 直接写在模版 (`public/index.html`) 中的标签不受影响。
182+
183+
另外,当启用 SRI 时,preload resource hints 会被禁用,因为 [Chrome 的一个 bug](https://bugs.chromium.org/p/chromium/issues/detail?id=677022) 会导致文件被下载两次。
184+
174185
### configureWebpack
175186

176187
- Type: `Object | Function`

packages/@vue/cli-service/__tests__/build.spec.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ test('build', async () => {
3030
// should preload css
3131
expect(index).toMatch(/<link [^>]+app[^>]+\.css rel=preload as=style>/)
3232

33+
// should inject scripts
34+
expect(index).toMatch(/<script src=\/js\/chunk-vendors\.\w{8}\.js>/)
35+
expect(index).toMatch(/<script src=\/js\/app\.\w{8}\.js>/)
36+
// should inject css
37+
expect(index).toMatch(/<link href=\/css\/app\.\w{8}\.css rel=stylesheet>/)
38+
3339
// should reference favicon with correct base URL
3440
expect(index).toMatch(/<link rel=icon href=\/favicon.ico>/)
3541

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
jest.setTimeout(30000)
2+
3+
const path = require('path')
4+
const portfinder = require('portfinder')
5+
const { createServer } = require('http-server')
6+
const { defaultPreset } = require('@vue/cli/lib/options')
7+
const create = require('@vue/cli-test-utils/createTestProject')
8+
const launchPuppeteer = require('@vue/cli-test-utils/launchPuppeteer')
9+
10+
let server, browser, page
11+
test('build', async () => {
12+
const project = await create('e2e-build-cors', defaultPreset)
13+
14+
await project.write('vue.config.js', `
15+
module.exports = {
16+
crossorigin: '',
17+
integrity: true
18+
}
19+
`)
20+
21+
const { stdout } = await project.run('vue-cli-service build')
22+
expect(stdout).toMatch('Build complete.')
23+
24+
const index = await project.read('dist/index.html')
25+
26+
// preload disabled due to chrome bug
27+
// https://bugs.chromium.org/p/chromium/issues/detail?id=677022
28+
// expect(index).toMatch(/<link [^>]+js\/app[^>]+\.js rel=preload as=script crossorigin>/)
29+
// expect(index).toMatch(/<link [^>]+js\/chunk-vendors[^>]+\.js rel=preload as=script crossorigin>/)
30+
// expect(index).toMatch(/<link [^>]+app[^>]+\.css rel=preload as=style crossorigin>/)
31+
32+
// should apply crossorigin and add integrity to scripts and css
33+
expect(index).toMatch(/<script src=\/js\/chunk-vendors\.\w{8}\.js crossorigin integrity=sha384-.{64}>/)
34+
expect(index).toMatch(/<script src=\/js\/app\.\w{8}\.js crossorigin integrity=sha384-.{64}>/)
35+
expect(index).toMatch(/<link href=\/css\/app\.\w{8}\.css rel=stylesheet crossorigin integrity=sha384-.{64}>/)
36+
37+
// verify integrity is correct by actually running it
38+
const port = await portfinder.getPortPromise()
39+
server = createServer({ root: path.join(project.dir, 'dist') })
40+
41+
await new Promise((resolve, reject) => {
42+
server.listen(port, err => {
43+
if (err) return reject(err)
44+
resolve()
45+
})
46+
})
47+
48+
const launched = await launchPuppeteer(`http://localhost:${port}/`)
49+
browser = launched.browser
50+
page = launched.page
51+
52+
const h1Text = await page.evaluate(() => {
53+
return document.querySelector('h1').textContent
54+
})
55+
56+
expect(h1Text).toMatch('Welcome to Your Vue.js App')
57+
})
58+
59+
afterAll(async () => {
60+
if (browser) {
61+
await browser.close()
62+
}
63+
if (server) {
64+
server.close()
65+
}
66+
})

packages/@vue/cli-service/lib/config/app.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,12 +251,13 @@ module.exports = (api, options) => {
251251
}
252252

253253
// CORS and Subresource Integrity
254-
if (options.crossorigin != null || options.integreity) {
254+
if (options.crossorigin != null || options.integrity) {
255255
webpackConfig
256256
.plugin('cors')
257257
.use(require('../webpack/CorsPlugin'), [{
258258
crossorigin: options.crossorigin,
259-
integreity: options.integreity
259+
integrity: options.integrity,
260+
baseUrl: options.baseUrl
260261
}])
261262
}
262263

packages/@vue/cli-service/lib/options.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const schema = createSchema(joi => joi.object({
1313
devServer: joi.object(),
1414
pages: joi.object(),
1515
crossorigin: joi.string().valid(['', 'anonymous', 'use-credentials']),
16+
integrity: joi.boolean(),
1617

1718
// css
1819
css: joi.object({

packages/@vue/cli-service/lib/webpack/CorsPlugin.js

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,66 @@
11
module.exports = class CorsPlugin {
2-
constructor ({ crossorigin, integrity }) {
3-
this.crossorigin = crossorigin || (integrity ? '' : undefined)
2+
constructor ({ baseUrl, crossorigin, integrity }) {
3+
this.crossorigin = crossorigin
44
this.integrity = integrity
5+
this.baseUrl = baseUrl
56
}
67

78
apply (compiler) {
89
const ID = `vue-cli-cors-plugin`
910
compiler.hooks.compilation.tap(ID, compilation => {
11+
const ssri = require('ssri')
12+
13+
const computeHash = url => {
14+
const filename = url.replace(this.baseUrl, '')
15+
const asset = compilation.assets[filename]
16+
if (asset) {
17+
const src = asset.source()
18+
const integrity = ssri.fromData(src, {
19+
algorithms: ['sha384']
20+
})
21+
return integrity.toString()
22+
}
23+
}
24+
1025
compilation.hooks.htmlWebpackPluginAlterAssetTags.tap(ID, data => {
26+
const tags = [...data.head, ...data.body]
1127
if (this.crossorigin != null) {
12-
[...data.head, ...data.body].forEach(tag => {
28+
tags.forEach(tag => {
1329
if (tag.tagName === 'script' || tag.tagName === 'link') {
1430
tag.attributes.crossorigin = this.crossorigin
1531
}
1632
})
1733
}
34+
if (this.integrity) {
35+
tags.forEach(tag => {
36+
if (tag.tagName === 'script') {
37+
const hash = computeHash(tag.attributes.src)
38+
if (hash) {
39+
tag.attributes.integrity = hash
40+
}
41+
} else if (tag.tagName === 'link' && tag.attributes.rel === 'stylesheet') {
42+
const hash = computeHash(tag.attributes.href)
43+
if (hash) {
44+
tag.attributes.integrity = hash
45+
}
46+
}
47+
})
48+
49+
// when using SRI, Chrome somehow cannot reuse
50+
// the preloaded resource, and causes the files to be downloaded twice.
51+
// this is a Chrome bug (https://bugs.chromium.org/p/chromium/issues/detail?id=677022)
52+
// for now we disable preload if SRI is used.
53+
data.head = data.head.filter(tag => {
54+
return !(
55+
tag.tagName === 'link' &&
56+
tag.attributes.rel === 'preload'
57+
)
58+
})
59+
}
60+
})
61+
62+
compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tap(ID, data => {
63+
data.html = data.html.replace(/\scrossorigin=""/g, ' crossorigin')
1864
})
1965
})
2066
}

packages/@vue/cli-service/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"semver": "^5.5.0",
5858
"slash": "^2.0.0",
5959
"source-map-url": "^0.4.0",
60+
"ssri": "^6.0.0",
6061
"string.prototype.padend": "^3.0.0",
6162
"thread-loader": "^1.1.5",
6263
"uglifyjs-webpack-plugin": "^1.2.7",

yarn.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11270,6 +11270,10 @@ ssri@^5.2.4:
1127011270
dependencies:
1127111271
safe-buffer "^5.1.1"
1127211272

11273+
ssri@^6.0.0:
11274+
version "6.0.0"
11275+
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.0.tgz#fc21bfc90e03275ac3e23d5a42e38b8a1cbc130d"
11276+
1127311277
stable@~0.1.6:
1127411278
version "0.1.8"
1127511279
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"

0 commit comments

Comments
 (0)