feat: support Subresource Integrity via integrity option

This commit is contained in:
Evan You
2018-08-09 01:22:46 -04:00
parent 7b39bed188
commit 55043d310e
9 changed files with 152 additions and 5 deletions

View File

@@ -173,6 +173,17 @@ module.exports = {
See also: [CROS setting attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes)
### integrity
- Type: `boolean`
- Default: `false`
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.
Note that this only affects tags injected by `html-webpack-plugin` - tags directly added in the source template (`public/index.html`) are not affected.
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.
### configureWebpack
- Type: `Object | Function`

View File

@@ -171,6 +171,17 @@ module.exports = {
更多细节可查阅: [CROS setting attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes)
### integrity
- Type: `boolean`
- Default: `false`
在生成的 HTML 中的 `<link rel="stylesheet">` 和 `<script>` 标签上启用 [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) (SRI)。如果你构建后的文件是部署在 CDN 上的,启用该选项可以提供额外的安全性。
需要注意的是该选项仅影响由 `html-webpack-plugin` 在构建时注入的标签 - 直接写在模版 (`public/index.html`) 中的标签不受影响。
另外,当启用 SRI 时preload resource hints 会被禁用,因为 [Chrome 的一个 bug](https://bugs.chromium.org/p/chromium/issues/detail?id=677022) 会导致文件被下载两次。
### configureWebpack
- Type: `Object | Function`

View File

@@ -30,6 +30,12 @@ test('build', async () => {
// should preload css
expect(index).toMatch(/<link [^>]+app[^>]+\.css rel=preload as=style>/)
// should inject scripts
expect(index).toMatch(/<script src=\/js\/chunk-vendors\.\w{8}\.js>/)
expect(index).toMatch(/<script src=\/js\/app\.\w{8}\.js>/)
// should inject css
expect(index).toMatch(/<link href=\/css\/app\.\w{8}\.css rel=stylesheet>/)
// should reference favicon with correct base URL
expect(index).toMatch(/<link rel=icon href=\/favicon.ico>/)

View File

@@ -0,0 +1,66 @@
jest.setTimeout(30000)
const path = require('path')
const portfinder = require('portfinder')
const { createServer } = require('http-server')
const { defaultPreset } = require('@vue/cli/lib/options')
const create = require('@vue/cli-test-utils/createTestProject')
const launchPuppeteer = require('@vue/cli-test-utils/launchPuppeteer')
let server, browser, page
test('build', async () => {
const project = await create('e2e-build-cors', defaultPreset)
await project.write('vue.config.js', `
module.exports = {
crossorigin: '',
integrity: true
}
`)
const { stdout } = await project.run('vue-cli-service build')
expect(stdout).toMatch('Build complete.')
const index = await project.read('dist/index.html')
// preload disabled due to chrome bug
// https://bugs.chromium.org/p/chromium/issues/detail?id=677022
// expect(index).toMatch(/<link [^>]+js\/app[^>]+\.js rel=preload as=script crossorigin>/)
// expect(index).toMatch(/<link [^>]+js\/chunk-vendors[^>]+\.js rel=preload as=script crossorigin>/)
// expect(index).toMatch(/<link [^>]+app[^>]+\.css rel=preload as=style crossorigin>/)
// should apply crossorigin and add integrity to scripts and css
expect(index).toMatch(/<script src=\/js\/chunk-vendors\.\w{8}\.js crossorigin integrity=sha384-.{64}>/)
expect(index).toMatch(/<script src=\/js\/app\.\w{8}\.js crossorigin integrity=sha384-.{64}>/)
expect(index).toMatch(/<link href=\/css\/app\.\w{8}\.css rel=stylesheet crossorigin integrity=sha384-.{64}>/)
// verify integrity is correct by actually running it
const port = await portfinder.getPortPromise()
server = createServer({ root: path.join(project.dir, 'dist') })
await new Promise((resolve, reject) => {
server.listen(port, err => {
if (err) return reject(err)
resolve()
})
})
const launched = await launchPuppeteer(`http://localhost:${port}/`)
browser = launched.browser
page = launched.page
const h1Text = await page.evaluate(() => {
return document.querySelector('h1').textContent
})
expect(h1Text).toMatch('Welcome to Your Vue.js App')
})
afterAll(async () => {
if (browser) {
await browser.close()
}
if (server) {
server.close()
}
})

View File

@@ -251,12 +251,13 @@ module.exports = (api, options) => {
}
// CORS and Subresource Integrity
if (options.crossorigin != null || options.integreity) {
if (options.crossorigin != null || options.integrity) {
webpackConfig
.plugin('cors')
.use(require('../webpack/CorsPlugin'), [{
crossorigin: options.crossorigin,
integreity: options.integreity
integrity: options.integrity,
baseUrl: options.baseUrl
}])
}

View File

@@ -13,6 +13,7 @@ const schema = createSchema(joi => joi.object({
devServer: joi.object(),
pages: joi.object(),
crossorigin: joi.string().valid(['', 'anonymous', 'use-credentials']),
integrity: joi.boolean(),
// css
css: joi.object({

View File

@@ -1,20 +1,66 @@
module.exports = class CorsPlugin {
constructor ({ crossorigin, integrity }) {
this.crossorigin = crossorigin || (integrity ? '' : undefined)
constructor ({ baseUrl, crossorigin, integrity }) {
this.crossorigin = crossorigin
this.integrity = integrity
this.baseUrl = baseUrl
}
apply (compiler) {
const ID = `vue-cli-cors-plugin`
compiler.hooks.compilation.tap(ID, compilation => {
const ssri = require('ssri')
const computeHash = url => {
const filename = url.replace(this.baseUrl, '')
const asset = compilation.assets[filename]
if (asset) {
const src = asset.source()
const integrity = ssri.fromData(src, {
algorithms: ['sha384']
})
return integrity.toString()
}
}
compilation.hooks.htmlWebpackPluginAlterAssetTags.tap(ID, data => {
const tags = [...data.head, ...data.body]
if (this.crossorigin != null) {
[...data.head, ...data.body].forEach(tag => {
tags.forEach(tag => {
if (tag.tagName === 'script' || tag.tagName === 'link') {
tag.attributes.crossorigin = this.crossorigin
}
})
}
if (this.integrity) {
tags.forEach(tag => {
if (tag.tagName === 'script') {
const hash = computeHash(tag.attributes.src)
if (hash) {
tag.attributes.integrity = hash
}
} else if (tag.tagName === 'link' && tag.attributes.rel === 'stylesheet') {
const hash = computeHash(tag.attributes.href)
if (hash) {
tag.attributes.integrity = hash
}
}
})
// when using SRI, Chrome somehow cannot reuse
// the preloaded resource, and causes the files to be downloaded twice.
// this is a Chrome bug (https://bugs.chromium.org/p/chromium/issues/detail?id=677022)
// for now we disable preload if SRI is used.
data.head = data.head.filter(tag => {
return !(
tag.tagName === 'link' &&
tag.attributes.rel === 'preload'
)
})
}
})
compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tap(ID, data => {
data.html = data.html.replace(/\scrossorigin=""/g, ' crossorigin')
})
})
}

View File

@@ -57,6 +57,7 @@
"semver": "^5.5.0",
"slash": "^2.0.0",
"source-map-url": "^0.4.0",
"ssri": "^6.0.0",
"string.prototype.padend": "^3.0.0",
"thread-loader": "^1.1.5",
"uglifyjs-webpack-plugin": "^1.2.7",

View File

@@ -11270,6 +11270,10 @@ ssri@^5.2.4:
dependencies:
safe-buffer "^5.1.1"
ssri@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.0.tgz#fc21bfc90e03275ac3e23d5a42e38b8a1cbc130d"
stable@~0.1.6:
version "0.1.8"
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"