Skip to content

Commit

Permalink
Merge pull request #547 from Baroshem/vejja/issue541
Browse files Browse the repository at this point in the history
feat(core): crypto compatibility for Workers
  • Loading branch information
vejja authored Nov 12, 2024
2 parents 4e2a603 + 88a46a4 commit f3706ab
Show file tree
Hide file tree
Showing 7 changed files with 33 additions and 21 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
node: [18]
node: [20]

steps:
- uses: actions/setup-node@v3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,17 @@ Cross-Origin-Opener-Policy: same-origin
The `crossOriginOpenerPolicy` header can be configured with following values.

```ts
crossOriginOpenerPolicy: 'unsafe-none' | 'same-origin-allow-popups' | 'same-origin' | false
crossOriginOpenerPolicy: 'same-origin' | 'same-origin-allow-popups' | 'unsafe-none' | false
```

### `unsafe-none`
### `same-origin`

This is the default value. Allows the document to be added to its opener's browsing context group unless the opener itself has a COOP of same-origin or same-origin-allow-popups.
This is the default value. Isolates the browsing context exclusively to same-origin documents. Cross-origin documents are not loaded in the same browsing context.

### `same-origin-allow-popups`

Retains references to newly opened windows or tabs that either don't set COOP or that opt out of isolation by setting a COOP of unsafe-none.

### `same-origin`
### `unsafe-none`

Isolates the browsing context exclusively to same-origin documents. Cross-origin documents are not loaded in the same browsing context.
Allows the document to be added to its opener's browsing context group unless the opener itself has a COOP of same-origin or same-origin-allow-popups.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"version": "2.0.0",
"license": "MIT",
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"homepage": "https://nuxt-security.vercel.app",
"description": "🛡️ Security Module for Nuxt based on HTTP Headers and Middleware",
"repository": {
Expand Down
4 changes: 2 additions & 2 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ function reorderNitroPlugins(nuxt: Nuxt) {


async function hashBundledAssets(nitro: Nitro) {
const hashAlgorithm = 'sha384'
const hashAlgorithm = 'SHA-384'
const sriHashes: Record<string, string> = {}

// Will be later necessary to construct url
Expand All @@ -326,7 +326,7 @@ async function hashBundledAssets(nitro: Nitro) {
// const fullPath = join(entry.path, entry.name)
const path = join(dir, entry.name)
const content = await readFile(path)
const hash = generateHash(content, hashAlgorithm)
const hash = await generateHash(content, hashAlgorithm)
// construct the url as it will appear in the head template
const fullPath = join(baseURL, entry.name)
let url: string
Expand Down
14 changes: 8 additions & 6 deletions src/runtime/nitro/plugins/30-cspSsgHashes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default defineNitroPlugin((nitroApp) => {
return
}

nitroApp.hooks.hook('render:html', (html, { event }) => {
nitroApp.hooks.hook('render:html', async(html, { event }) => {
// Exit if no CSP defined
const rules = resolveSecurityRules(event)
if (!rules.enabled || !rules.headers || !rules.headers.contentSecurityPolicy) {
Expand All @@ -34,7 +34,7 @@ export default defineNitroPlugin((nitroApp) => {
}
const scriptHashes = event.context.security!.hashes.script
const styleHashes = event.context.security!.hashes.style
const hashAlgorithm = 'sha256'
const hashAlgorithm = 'SHA-256'

// Parse HTML if SSG is enabled for this route
if (rules.ssg) {
Expand All @@ -43,12 +43,13 @@ export default defineNitroPlugin((nitroApp) => {
// Scan all relevant sections of the NuxtRenderHtmlContext
const sections = ['body', 'bodyAppend', 'bodyPrepend', 'head'] as Section[]
for (const section of sections) {
html[section].forEach(element => {
for (const element of html[section]) {
if (hashScripts) {
// Parse all script tags
const inlineScriptMatches = element.matchAll(INLINE_SCRIPT_RE)
for (const [, scriptText] of inlineScriptMatches) {
scriptHashes.add(`'${generateHash(scriptText, hashAlgorithm)}'`)
const hash = await generateHash(scriptText, hashAlgorithm)
scriptHashes.add(`'${hash}'`)
}
const externalScriptMatches = element.matchAll(SCRIPT_RE)
for (const [, integrity] of externalScriptMatches) {
Expand All @@ -60,7 +61,8 @@ export default defineNitroPlugin((nitroApp) => {
if (hashStyles) {
const styleMatches = element.matchAll(STYLE_RE)
for (const [, styleText] of styleMatches) {
styleHashes.add(`'${generateHash(styleText, hashAlgorithm)}'`)
const hash = await generateHash(styleText, hashAlgorithm)
styleHashes.add(`'${hash}'`)
}
}

Expand Down Expand Up @@ -94,7 +96,7 @@ export default defineNitroPlugin((nitroApp) => {
}
}
}
})
}
}
}
})
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/nitro/plugins/40-cspSsrNonce.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { defineNitroPlugin } from '#imports'
import { randomBytes } from 'node:crypto'
import { resolveSecurityRules } from '../context'

const LINK_RE = /<link([^>]*?>)/gi
Expand Down Expand Up @@ -28,7 +27,9 @@ export default defineNitroPlugin((nitroApp) => {

const rules = resolveSecurityRules(event)
if (rules.enabled && rules.nonce && !import.meta.prerender) {
const nonce = randomBytes(16).toString('base64')
const array = new Uint8Array(18);
crypto.getRandomValues(array)
const nonce = btoa(String.fromCharCode(...array))
event.context.security!.nonce = nonce
}
})
Expand Down
16 changes: 11 additions & 5 deletions src/utils/hash.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { createHash } from 'node:crypto';

export function generateHash(content: Buffer | string, hashAlgorithm: string) {
const hash = createHash(hashAlgorithm);
hash.update(content);
return `${hashAlgorithm}-${hash.digest('base64')}`;
export async function generateHash(content: Buffer | string, hashAlgorithm: 'SHA-256' | 'SHA-384' | 'SHA-512') {
let buffer: Uint8Array
if (typeof content === 'string') {
buffer = new TextEncoder().encode(content);
} else {
buffer = new Uint8Array(content);
}
const hashBuffer = await crypto.subtle.digest(hashAlgorithm, buffer);
const base64 = btoa(String.fromCharCode(...new Uint8Array(hashBuffer)));
const prefix = hashAlgorithm.replace('-', '').toLowerCase()
return `${prefix}-${base64}`;
}

0 comments on commit f3706ab

Please sign in to comment.