-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #285 from vejja/sri
- Loading branch information
Showing
19 changed files
with
440 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ on: | |
- '**-rc.**' | ||
- 'renovate/**' | ||
pull_request: | ||
workflow_dispatch: | ||
|
||
jobs: | ||
ci: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
docs/content/1.documentation/4.utils/3.subresource-integrity.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
# Subresource Integrity | ||
|
||
:badge[Enabled]{type="success"} Ensure that your application bundle has not been manipulated. | ||
|
||
--- | ||
|
||
:ellipsis{right=0px width=75% blur=150px} | ||
|
||
Subresource Integrity (SRI) is a security feature that enables the browser to verify that the static assets that your application is loading have not been altered. | ||
|
||
Nuxt Security automatically computes the integrity hash of each static asset (scripts, stylesheets, etc.) that are bundled in your Nuxt Application, and then inserts this value in the resulting HTML file. | ||
|
||
|
||
::alert{type="info"} | ||
ℹ Read more about Subresource Integrity [here](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity). | ||
:: | ||
|
||
## Options | ||
|
||
This feature is enabled globally by default. You can customize it like following: | ||
|
||
```js{}[nuxt.config.ts] | ||
export default defineNuxtConfig({ | ||
// Global | ||
security: { | ||
sri: true | ||
} | ||
}) | ||
``` | ||
|
||
You can disable the feature globally by setting `sri: false`. | ||
|
||
## Usage | ||
|
||
Subresource Integrity is used for two important security features of your application: | ||
|
||
**1. SRI ensures that the assets that _you_ included in your build have not been altered.** | ||
|
||
When you build your Nuxt application and deliver it to your users, a significant number of critical components are included in your final bundle. | ||
|
||
These components are mostly scripts containing Javascript code (files such as `/_nuxt/entry.b8aef440d.js`), stylesheets, etc. An attacker may try to compromise your application by modifying these files. | ||
|
||
Nuxt Security calculates the hash of each of these files _at build time_, therefore guaranteeing that the files that are loaded by your users are exactly the ones that you included in your bundle. | ||
|
||
Arguably, if you host your static assets yourself, the risk that these files are modified by a malicious actor without your authorization can be rated as low. | ||
|
||
However: | ||
|
||
- If you host your application on a public CDN, that CDN could become the target of an attack. | ||
- Even if you host your application on a private hosting service, you should be aware that most hosting providers use elaborate caching strategies to accelerate the delivery of your files (e.g. via edge CDN replication). | ||
- In any case, your own account (or the account of one of the members of your organization) might become compromised. | ||
|
||
For these reasons, most modern web applications rely on SRI to reduce their attack surface. | ||
|
||
::alert{type="success"} | ||
SRI is supported by all modern browsers: [caniuse](https://caniuse.com/subresource-integrity) | ||
:: | ||
|
||
**2. SRI is a critical component of Content Security Policy (CSP) in SSG mode.** | ||
|
||
For more information on the relationship between Subresource Integrity and a Strict CSP, please read our [Advanced Section on Integrity Hashes for CSP](/documentation/advanced/strict-csp/#ssg-mode) | ||
|
||
If you use CSP on a statically-generated application, you will need to enable SRI by setting `sri: true`. | ||
|
||
::alert{type="warning"} | ||
Subresource Integrity hashes can only be inserted on `<script>` and `<link>` elements generated as part of the server bundle. | ||
<br> | ||
To protect your Nuxt application after client-side hydration, you must deploy a Strict CSP. | ||
:: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import type { H3Event } from 'h3' | ||
import { extname } from 'pathe' | ||
import { useStorage } from '#imports' | ||
import * as cheerio from 'cheerio' | ||
|
||
export default defineNitroPlugin((nitroApp) => { | ||
nitroApp.hooks.hook('render:html', async (html, { event }) => { | ||
const prerendering = isPrerendering(event) | ||
|
||
// Retrieve the sriHases that we computed at build time | ||
// | ||
// - If we are in a pre-rendering step of nuxi generate | ||
// Then the /integrity directory does not exist in server assets | ||
// But it is still in the .nuxt build directory | ||
// | ||
// - Conversely, if we are in a standalone SSR server pre-built by nuxi build | ||
// Then we don't have a .nuxt build directory anymore | ||
// But we did save the /integrity directory into the server assets | ||
|
||
const storageBase = prerendering ? 'build' : 'assets' | ||
const sriHashes: Record<string, string> = await useStorage(storageBase).getItem('integrity:sriHashes.json') || {} | ||
|
||
// Scan all relevant sections of the NuxtRenderHtmlContext | ||
// Note: integrity can only be set on scripts and on links with rel preload, modulepreload and stylesheet | ||
// However the SRI standard provides that other elements may be added to that list in the future | ||
for (const section of ['body', 'bodyAppend', 'bodyPrepend', 'head']) { | ||
const htmlRecords = html as unknown as Record<string, string[]> | ||
|
||
htmlRecords[section] = htmlRecords[section].map(element => { | ||
const $ = cheerio.load(element, null, false) | ||
// Add integrity to all relevant script tags | ||
$('script').each((i, script) => { | ||
const scriptAttrs = $(script).attr() | ||
const src = scriptAttrs?.src | ||
const integrity = scriptAttrs?.integrity | ||
// Only add integrity to external scripts that do not already have one | ||
if (src && !integrity) { | ||
// Get the integrity hash from our static database | ||
const hash = sriHashes[src] | ||
// Set the integrity hash in HTML if found | ||
if (hash) { | ||
$(script).attr('integrity', hash) | ||
} | ||
} | ||
}) | ||
// Add integrity to all relevant link tags | ||
$('link').each((i, link) => { | ||
const linkAttrs = $(link).attr() | ||
const href = linkAttrs?.href | ||
const integrity = linkAttrs?.integrity | ||
// Only add integrity to resources that do not already have one | ||
if (href && !integrity) { | ||
// Get the integrity hash from our static database | ||
const hash = sriHashes[href] | ||
// Set the integrity hash in HTML if found | ||
if (hash) { | ||
$(link).attr('integrity', hash) | ||
} | ||
} | ||
}) | ||
return $.html() | ||
}) | ||
} | ||
}) | ||
|
||
/** | ||
* Detect if page is being pre-rendered | ||
* @param event H3Event | ||
* @returns boolean | ||
*/ | ||
function isPrerendering(event: H3Event): boolean { | ||
const nitroPrerenderHeader = 'x-nitro-prerender' | ||
|
||
// Page is not prerendered | ||
if (!event.node.req.headers[nitroPrerenderHeader]) { | ||
return false | ||
} | ||
|
||
// File is not HTML | ||
if (!['', '.html'].includes(extname(event.node.req.headers[nitroPrerenderHeader] as string))) { | ||
return false | ||
} | ||
|
||
return true | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { createHash } from 'node:crypto' | ||
import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises' | ||
import type { Nitro } from 'nitropack' | ||
import { join } from 'pathe' | ||
|
||
|
||
export default async function (nitro: Nitro) { | ||
const hashAlgorithm = 'sha384' | ||
const sriHashes: Record<string, string> = {} | ||
|
||
// Will be later necessary to construct url | ||
const { cdnURL: appCdnUrl = '', baseURL: appBaseUrl } = nitro.options.runtimeConfig.app | ||
|
||
|
||
// Go through all public assets folder by folder | ||
const publicAssets = nitro.options.publicAssets | ||
for (const publicAsset of publicAssets) { | ||
const { dir, baseURL = '' } = publicAsset | ||
|
||
// Node 16 compatibility maintained | ||
// Node 18.17+ supports recursive option on readdir | ||
// const entries = await readdir(dir, { withFileTypes: true, recursive: true }) | ||
const entries = await readdir(dir, { withFileTypes: true }) | ||
for (const entry of entries) { | ||
if (entry.isFile()) { | ||
|
||
// Node 16 compatibility maintained | ||
// Node 18.17+ supports entry.path on DirEnt | ||
// const fullPath = join(entry.path, entry.name) | ||
const fullPath = join(dir, entry.name) | ||
const fileContent = await readFile(fullPath) | ||
const hash = generateHash(fileContent, hashAlgorithm) | ||
// construct the url as it will appear in the head template | ||
const relativeUrl = join(baseURL, entry.name) | ||
let url: string | ||
if (appCdnUrl) { | ||
// If the cdnURL option was set, the url will be in the form https://... | ||
url = new URL(relativeUrl, appCdnUrl).href | ||
} else { | ||
// If not, the url will be in a relative form: /_nuxt/... | ||
url = join('/', appBaseUrl, relativeUrl) | ||
} | ||
sriHashes[url] = hash | ||
} | ||
} | ||
} | ||
|
||
// Save hashes in a /integrity directory within the .nuxt build for later use with SSG | ||
const buildDir = nitro.options.buildDir | ||
const integrityDir = join(buildDir, 'integrity') | ||
await mkdir(integrityDir) | ||
const hashFilePath = join(integrityDir, 'sriHashes.json') | ||
await writeFile(hashFilePath, JSON.stringify(sriHashes)) | ||
|
||
// Mount the /integrity directory into server assets for later use with SSR | ||
nitro.options.serverAssets.push({ dir: integrityDir, baseName: 'integrity' }) | ||
|
||
} | ||
|
||
function generateHash (content: Buffer, hashAlgorithm: string) { | ||
const hash = createHash(hashAlgorithm) | ||
hash.update(content) | ||
return `${hashAlgorithm}-${hash.digest('base64')}` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
imports.autoImport=true |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { defineNuxtConfig } from 'nuxt/config' | ||
|
||
export default defineNuxtConfig({ | ||
|
||
modules: ['../../../src/module'], | ||
|
||
// Per route configuration | ||
routeRules: { | ||
'/public': { | ||
prerender: true, | ||
} | ||
}, | ||
|
||
// Global configuration | ||
security: { | ||
rateLimiter: false, | ||
sri: true | ||
}, | ||
|
||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"private": true, | ||
"name": "basic", | ||
"type": "module" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<template> | ||
<div> | ||
Includes an image the ~/assets folder | ||
<img src="~/assets/snyk.png"> | ||
</div> | ||
</template> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<template> | ||
<div> | ||
Includes 2 manually entered integrity hashes | ||
</div> | ||
</template> | ||
<script setup> | ||
useHead({ | ||
link: [ | ||
{rel:'stylesheet', href:'https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css', integrity: 'sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN' } | ||
], | ||
script: [ | ||
{ src: 'https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js', integrity:'sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL', crossorigin: "" } | ||
] | ||
}) | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<template> | ||
<div> | ||
{{ data }} | ||
<NuxtLink to="/about">Go to about page</NuxtLink> | ||
</div> | ||
</template> | ||
|
||
<script setup> | ||
const { data } = await useAsyncData(() => 'Home') | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<template> | ||
<div> | ||
Includes an icon from public folder and an image from public folder | ||
<img src="/preview.png"> | ||
</div> | ||
</template> | ||
<script setup> | ||
useHead({ | ||
link: [ | ||
{ rel: 'icon', href: '/icon.png' }, | ||
{ rel: 'preload', as: 'image', href: '/preview.png' } | ||
] | ||
}) | ||
</script> |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.