Skip to content

Commit

Permalink
SSG pre-rendered headers
Browse files Browse the repository at this point in the history
- are now available in SSR
- are also avaiable for Nitro preset deployments
  • Loading branch information
vejja committed Apr 28, 2024
1 parent 2128967 commit a24e9e2
Show file tree
Hide file tree
Showing 30 changed files with 1,837 additions and 1,270 deletions.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,15 @@
"xss": "^1.0.14"
},
"devDependencies": {
"@nuxt/eslint-config": "^0.2.0",
"@nuxt/module-builder": "^0.5.2",
"@nuxt/eslint-config": "^0.3.10",
"@nuxt/module-builder": "^0.6.0",
"@nuxt/schema": "^3.11.2",
"@nuxt/test-utils": "^3.12.0",
"@types/node": "^18.18.1",
"eslint": "^8.50.0",
"nuxt": "^3.11.2",
"vitest": "^1.3.1"
"vitest": "^1.3.1",
"typescript": "^5.4.5"
},
"stackblitz": {
"installDependencies": false,
Expand Down
73 changes: 47 additions & 26 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineNuxtModule, addServerHandler, installModule, addVitePlugin, addServerPlugin, createResolver, addImportsDir, addServerImportsDir } from '@nuxt/kit'
import { defineNuxtModule, addServerHandler, installModule, addVitePlugin, addServerPlugin, createResolver, addImportsDir, addServerImportsDir, useNitro } from '@nuxt/kit'
import { defu } from 'defu'
import type { Nuxt } from '@nuxt/schema'
import viteRemove from 'unplugin-remove/vite'
Expand All @@ -7,7 +7,7 @@ import type { ModuleOptions, NuxtSecurityRouteRules } from './types/index'
import type { BasicAuth } from './types/middlewares'
import { defaultSecurityConfig } from './defaultConfig'
import type { CheerioAPI } from 'cheerio'
import { hashBundledAssets } from './runtime/utils/hashes'
import { hashBundledAssets } from './utils'

declare module 'nuxt/schema' {
interface NuxtOptions {
Expand Down Expand Up @@ -51,9 +51,14 @@ declare module 'nitropack' {
type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
declare module 'h3' {
interface H3EventContext {
security: {
routeRules?: Record<string, NuxtSecurityRouteRules>;
security?: {
route?: string;
rules?: NuxtSecurityRouteRules;
nonce?: string;
hashes?: {
script: Set<string>;
style: Set<string>;
};
cheerios?: Record<Section, CheerioAPI[]>;
}
}
Expand Down Expand Up @@ -112,35 +117,32 @@ export default defineNuxtModule<ModuleOptions>({
// Register nitro plugin to manage security rules at the level of each route
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/00-routeRules'))

// Register nitro plugin to add nonce
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/10-nonce'))

// Register nitro plugin to hide X-Powered-By header
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/20-hidePoweredBy'))

// Register nitro plugin to enable Security Headers
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/30-securityHeaders'))

// Pre-process HTML into DOM tree
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/40-preprocessHtml'))
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/10-preprocessHtml'))

// Register nitro plugin to enable Subresource Integrity
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/50-subresourceIntegrity'))
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/20-subresourceIntegrity'))

// Register nitro plugin to enable CSP Hashes for SSG
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/60-cspSsgHashes'))

// Register nitro plugin to enable CSP Headers presets for SSG
// TEMPORARILY DISABLED AS NUXT 3.9.3 PREVENTS IMPORTING @NUXT/KIT IN NITRO PLUGINS
/*
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/70-cspSsgPresets'))
*/
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/30-cspSsgHashes'))

// Nitro plugin to enable CSP Nonce for SSR
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/80-cspSsrNonce'))
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/40-cspSsrNonce'))

// Register nitro plugin to update CSP with actual nonce or hashes
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/50-updateCsp'))

// Recombine HTML from DOM tree
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/90-recombineHtml'))
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/60-recombineHtml'))

// Register nitro plugin to insert Security Headers in response
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/70-securityHeaders'))

// Register nitro plugin to hide X-Powered-By header
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/80-hidePoweredBy'))

// Register nitro plugin to save and retrieve prerendered headers in SSG mode
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/90-prerenderedHeaders'))

// Register hook that will reorder nitro plugins to be applied last
reorderNitroPlugins(nuxt)
Expand Down Expand Up @@ -197,6 +199,25 @@ export default defineNuxtModule<ModuleOptions>({

// Calculates SRI hashes at build time
nuxt.hook('nitro:build:before', hashBundledAssets)

// When the prerendering is done, we need to add the prerendered headers to the server assets
nuxt.hook('nitro:init', nitro => {
nitro.hooks.hook('prerender:done', async() => {
nitro.options.serverAssets.push({
baseName: 'nuxt-security',
dir: createResolver(nuxt.options.buildDir).resolve('./nuxt-security')
})

// In some Nitro presets (e.g. Vercel), the header rules are generated for the static server
// By default we update the nitro headers route rules with their calculated value to support this possibility
const prerenderedHeaders = await nitro.storage.getItem<any>('build:nuxt-security:headers.json')
const n = useNitro()
n.options.routeRules = defuReplaceArray(
prerenderedHeaders,
n.options.routeRules
)
})
})
}
})

Expand All @@ -212,13 +233,13 @@ function registerRateLimiterStorage(nuxt: Nuxt, securityOptions: ModuleOptions)
)
const { name, options } = driver
config.storage = defu(
config.storage,
{
'#rate-limiter-storage': {
driver: name,
options
}
}
},
config.storage
)
})
}
Expand Down
51 changes: 51 additions & 0 deletions src/runtime/nitro/context/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { NuxtSecurityRouteRules } from "~/src/types"
import { createRouter, toRouteMatcher } from "radix3"
import type { H3Event } from "h3"
import { defuReplaceArray } from "../utils"

// This is the global singleton that holds all of the application security rules
const nitroAppSecurityOptions: Record<string, NuxtSecurityRouteRules> = {}

/**
* Returns all the security rules of the Nitro application
*/
export function getAppSecurityOptions() {
return nitroAppSecurityOptions
}

/**
* Returns the security rules applicable to a specific request
*/
export function resolveSecurityRules(event: H3Event): NuxtSecurityRouteRules {
if (!event.context.security) {
event.context.security = {}
}
if (!event.context.security.rules) {
const router = createRouter<NuxtSecurityRouteRules>({ routes: structuredClone(nitroAppSecurityOptions) })
const matcher = toRouteMatcher(router)
const matches = matcher.matchAll(event.path.split('?')[0])
const rules: NuxtSecurityRouteRules = defuReplaceArray({}, ...matches.reverse())
event.context.security.rules = rules
}
return event.context.security.rules
}

/**
* Returns the security route that was matched for a specific request
*/
export function resolveSecurityRoute(event: H3Event) {
if (!event.context.security) {
event.context.security = {}
}
if (!event.context.security.route) {
const routeNames = Object.fromEntries(Object.entries(nitroAppSecurityOptions).map(([name]) => [name, { name }]))
const router = createRouter<{ name: string }>({ routes: routeNames})
const match = router.lookup(event.path.split('?')[0])
const route = match?.name ?? ''
event.context.security.route = route
}
return event.context.security.route
}



38 changes: 18 additions & 20 deletions src/runtime/nitro/plugins/00-routeRules.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
import { defineNitroPlugin, useRuntimeConfig } from "#imports"
import { NuxtSecurityRouteRules } from "../../../types"
import { defineNitroPlugin, useRuntimeConfig, useStorage } from "#imports"

Check warning on line 1 in src/runtime/nitro/plugins/00-routeRules.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest, 18)

'useStorage' is defined but never used
import { defuReplaceArray } from "../utils"
import { OptionKey, SecurityHeaders } from "../../../types/headers"
import { getKeyFromName, headerObjectFromString } from "../../utils/headers"
import { getAppSecurityOptions } from '../context'

export default defineNitroPlugin((nitroApp) => {

/**
* This plugin merges all security options into the global security context
*/
export default defineNitroPlugin(async(nitroApp) => {
const appSecurityOptions = getAppSecurityOptions()
const runtimeConfig = useRuntimeConfig()
const securityRouteRules: Record<string, NuxtSecurityRouteRules> = {}

// First insert standard route rules headers
for (const route in runtimeConfig.nitro.routeRules) {
const rule = runtimeConfig.nitro.routeRules[route]
const { headers } = rule
const securityHeaders = standardToSecurity(headers)
if (securityHeaders) {
securityRouteRules[route] = { headers: securityHeaders }
appSecurityOptions[route] = { headers: securityHeaders }
}
}


// Then insert global security config
const securityOptions = runtimeConfig.security
securityRouteRules['/**'] = defuReplaceArray(
appSecurityOptions['/**'] = defuReplaceArray(
securityOptions,
securityRouteRules['/**']
appSecurityOptions['/**']
)

// Then insert route specific security headers
Expand All @@ -34,30 +34,28 @@ export default defineNitroPlugin((nitroApp) => {
if (security) {
const { headers } = security
const securityHeaders = backwardsCompatibleSecurity(headers)
securityRouteRules[route] = defuReplaceArray(
appSecurityOptions[route] = defuReplaceArray(
{ headers: securityHeaders },
security,
securityRouteRules[route],
appSecurityOptions[route],
)
}
}

// TO DO : DEPRECATE IN FAVOR OF NUXT-SECURITY:ROUTERULES HOOK
nitroApp.hooks.hook('nuxt-security:headers', ({ route, headers }) => {
securityRouteRules[route] = defuReplaceArray(
appSecurityOptions[route] = defuReplaceArray(
{ headers },
securityRouteRules[route]
appSecurityOptions[route]
)
})
nitroApp.hooks.callHook('nuxt-security:ready')

// NEW HOOK HAS ABILITY TO CONFIGURE ALL SECURITY OPTIONS FOR EACH ROUTE
nitroApp.hooks.callHook('nuxt-security:routeRules', securityRouteRules)


nitroApp.hooks.hook('request', async(event) => {
event.context.security = { routeRules: securityRouteRules }
nitroApp.hooks.hook('nuxt-security:ready', async() => {
await nitroApp.hooks.callHook('nuxt-security:routeRules', appSecurityOptions)
})

await nitroApp.hooks.callHook('nuxt-security:ready')
})

/**
Expand Down
18 changes: 0 additions & 18 deletions src/runtime/nitro/plugins/10-nonce.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { defineNitroPlugin } from '#imports'
import * as cheerio from 'cheerio/lib/slim'
import { resolveSecurityRules } from '../utils'

import { resolveSecurityRules } from '../context'

/**
* This plugin parses the HTML of the NuxtRenderHtmlContext and stores the Cheerio instances in the event context.
*/
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('render:html', (html, { event }) => {

Expand All @@ -25,6 +27,6 @@ export default defineNitroPlugin((nitroApp) => {
}, false)
})
}
event.context.security.cheerios = cheerios
event.context.security!.cheerios = cheerios
})
})
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useStorage, defineNitroPlugin } from '#imports'
import { resolveSecurityRules } from '../utils'

import { resolveSecurityRules } from '../context'

/**
* This plugin adds Subresource Integrity (SRI) hashes to script and link tags in the HTML.
*/
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('render:html', async(html, { event }) => {
// Exit if SRI not enabled for this route
Expand Down Expand Up @@ -29,7 +31,7 @@ export default defineNitroPlugin((nitroApp) => {
// However the SRI standard provides that other elements may be added to that list in the future
type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
const sections = ['body', 'bodyAppend', 'bodyPrepend', 'head'] as Section[]
const cheerios = event.context.security.cheerios!
const cheerios = event.context.security!.cheerios!
for (const section of sections) {
cheerios[section].forEach($ => {
// Add integrity to all relevant script tags
Expand Down
Loading

0 comments on commit a24e9e2

Please sign in to comment.