c6ef857d57
<!-- Thanks for opening a PR! Your contribution is much appreciated. In order to make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change that you're making: --> This serves to add support for [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) hashes for scripts added from the new app directory. This also has support for utilizing nonce values passed from request headers (expected to be generated per request in middleware) in the bootstrapping scripts via the `Content-Security-Policy` header as such: ``` Content-Security-Policy: script-src 'nonce-2726c7f26c' ``` Which results in the inline scripts having a new `nonce` attribute hash added. These features combined support for setting an aggressive Content Security Policy on scripts loaded. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [x] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [x] Make sure the linting passes by running `pnpm lint` - [x] The examples guidelines are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples) Co-authored-by: Steven <steven@ceriously.com>
71 lines
2.4 KiB
TypeScript
71 lines
2.4 KiB
TypeScript
import { webpack, sources } from 'next/dist/compiled/webpack/webpack'
|
|
import crypto from 'crypto'
|
|
import { SUBRESOURCE_INTEGRITY_MANIFEST } from '../../../shared/lib/constants'
|
|
|
|
const PLUGIN_NAME = 'SubresourceIntegrityPlugin'
|
|
|
|
export type SubresourceIntegrityAlgorithm = 'sha256' | 'sha384' | 'sha512'
|
|
|
|
export class SubresourceIntegrityPlugin {
|
|
constructor(private readonly algorithm: SubresourceIntegrityAlgorithm) {}
|
|
|
|
public apply(compiler: webpack.Compiler) {
|
|
compiler.hooks.make.tap(PLUGIN_NAME, (compilation) => {
|
|
compilation.hooks.afterOptimizeAssets.tap(
|
|
{
|
|
name: PLUGIN_NAME,
|
|
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
|
|
},
|
|
(assets) => {
|
|
// Collect all the entrypoint files.
|
|
let files = new Set<string>()
|
|
for (const entrypoint of compilation.entrypoints.values()) {
|
|
const iterator = entrypoint?.getFiles()
|
|
if (!iterator) {
|
|
continue
|
|
}
|
|
|
|
for (const file of iterator) {
|
|
files.add(file)
|
|
}
|
|
}
|
|
|
|
// For each file, deduped, calculate the file hash.
|
|
const hashes: Record<string, string> = {}
|
|
for (const file of files.values()) {
|
|
// Get the buffer for the asset.
|
|
const asset = assets[file]
|
|
if (!asset) {
|
|
throw new Error(`could not get asset: ${file}`)
|
|
}
|
|
|
|
// Get the buffer for the asset.
|
|
const buffer = asset.buffer()
|
|
|
|
// Create the hash for the content.
|
|
const hash = crypto
|
|
.createHash(this.algorithm)
|
|
.update(buffer)
|
|
.digest()
|
|
.toString('base64')
|
|
|
|
hashes[file] = `${this.algorithm}-${hash}`
|
|
}
|
|
|
|
const json = JSON.stringify(hashes, null, 2)
|
|
const file = 'server/' + SUBRESOURCE_INTEGRITY_MANIFEST
|
|
assets[file + '.js'] = new sources.RawSource(
|
|
'self.__SUBRESOURCE_INTEGRITY_MANIFEST=' + json
|
|
// Work around webpack 4 type of RawSource being used
|
|
// TODO: use webpack 5 type by default
|
|
) as unknown as webpack.sources.RawSource
|
|
assets[file + '.json'] = new sources.RawSource(
|
|
json
|
|
// Work around webpack 4 type of RawSource being used
|
|
// TODO: use webpack 5 type by default
|
|
) as unknown as webpack.sources.RawSource
|
|
}
|
|
)
|
|
})
|
|
}
|
|
}
|