Skip to content

Commit

Permalink
Merge pull request #532 from Baroshem/chore/2.1.0
Browse files Browse the repository at this point in the history
Chore/2.1.0
  • Loading branch information
Baroshem authored Nov 14, 2024
2 parents c2d8728 + 7659ada commit 84f13ee
Show file tree
Hide file tree
Showing 14 changed files with 323 additions and 128 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 @@ -26,7 +26,7 @@ interface ModuleOptions {
enabled: boolean;
csrf: CsrfOptions | false;
nonce: boolean;
removeLoggers: RemoveOptions | false;
removeLoggers: boolean | RemoveOptions; // RemoveOptions is being deprecated, please use `true` instead
ssg: Ssg | false;
sri: boolean;
}
Expand Down Expand Up @@ -112,12 +112,7 @@ security: {
enabled: true,
csrf: false,
nonce: true,
removeLoggers: {
external: [],
consoleType: ['log', 'debug'],
include: [/\.[jt]sx?$/, /\.vue\??/],
exclude: [/node_modules/, /\.git/]
},
removeLoggers: true,
ssg: {
meta: true,
hashScripts: true,
Expand Down
2 changes: 2 additions & 0 deletions docs/content/1.documentation/2.headers/1.csp.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ contentSecurityPolicy: {
'frame-ancestors'?: ("'self'" | "'none'" | string)[] | false;
'report-uri'?: string[] | false;
'report-to'?: string | false;
'require-trusted-types-for'?: string | false;
'trusted-types'?: string[] | string | false;
'upgrade-insecure-requests'?: boolean;
} | false
```
Expand Down
82 changes: 49 additions & 33 deletions docs/content/1.documentation/4.utils/2.remove-console-loggers.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,58 @@ By default, your application will allow log all activity in the browser when you
ℹ Read more about it [here](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html#data-to-exclude).
::

Fortunately, `nuxt-security` module removes both `log` and `debug` console outputs by default so your application is not leaking this information.
Fortunately, the Nuxt Security module removes all `console` outputs by default so your application is not leaking this information.
Nuxt Security also removes all `debugger` statements from your code.

This functionality is delivered by the amazing Vite Plugin by [Talljack](https://github.com/Talljack) that you can check out [here](https://github.com/Talljack/unplugin-remove).
## Options

This feature is enabled globally default.

You can disable the feature by setting `removeLoggers: false`:

```js{}[nuxt.config.ts]
export default defineNuxtConfig({
modules: ['nuxt-security'],
security: {
removeLoggers: false
}
})
```

## Alternative method - deprecated

By default when you set `removeLoggers: true`, Nuxt Security uses the native Vite features to remove statements.

In addition, Nuxt Security also supports an alternative method for removing console outputs, via the amazing `unplugin-remove` Vite Plugin by [Talljack](https://github.com/Talljack) that you can check out [here](https://github.com/Talljack/unplugin-remove).

::alert{type="warning"}
ℹ The `unplugin-remove` method is being deprecated and will be removed in a future release.
Please note that `unplugin-remove` will not remove `debugger` statements from your code.
::

If you want to use the `unplugin-remove` plugin method, pass an object to the `removeLoggers` configuration instead of passing `true`.

```js{}[nuxt.config.ts]
export default defineNuxtConfig({
modules: ['nuxt-security'],
security: {
removeLoggers: {
external: [],
consoleType: ['log', 'debug'],
include: [/\.[jt]sx?$/, /\.vue\??/],
exclude: [/node_modules/, /\.git/]
}
}
})
```

The `removeLoggers` object can be configured with following values.

```ts
import type { FilterPattern } from '@rollup/pluginutils'
export interface Options {
// https://github.com/Talljack/unplugin-remove/blob/main/src/types.ts
type RemoveOptions {
/**
* don't remove console.log and debugger these module
*
Expand Down Expand Up @@ -47,32 +92,3 @@ export interface Options {
exclude?: FilterPattern
}
```

If you would like to add some custom functionality to it, you can do so by doing the following:

```js{}[nuxt.config.ts]
export default defineNuxtConfig({
modules: ['nuxt-security'],
security: {
removeLoggers: {
external: [],
consoleType: ['log', 'debug'],
include: [/\.[jt]sx?$/, /\.vue\??/],
exclude: [/node_modules/, /\.git/]
}
}
})
```

However, if you prefer not to have this, you can always disable this functionality from the module configuration (which is not recommended but possible) like the following:

```js{}[nuxt.config.ts]
export default defineNuxtConfig({
modules: ['nuxt-security'],
security: {
removeLoggers: false
}
})
```
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
{
"name": "nuxt-security",
"version": "2.0.0",
"version": "2.1.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 Expand Up @@ -54,7 +57,7 @@
"@nuxt/kit": "^3.11.2",
"basic-auth": "^2.0.1",
"defu": "^6.1.1",
"nuxt-csurf": "^1.5.1",
"nuxt-csurf": "^1.6.5",
"pathe": "^1.0.0",
"unplugin-remove": "^1.0.3",
"xss": "^1.0.14"
Expand Down
10 changes: 2 additions & 8 deletions src/defaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,7 @@ export const defaultSecurityConfig = (serverlUrl: string, strict: boolean) => {
enabled: true,
csrf: false,
nonce: true,
// https://github.com/Talljack/unplugin-remove/blob/main/src/types.ts
removeLoggers: {
external: [],
consoleType: ['log', 'debug'],
include: [/\.[jt]sx?$/, /\.vue\??/],
exclude: [/node_modules/, /\.git/]
},
removeLoggers: true,
ssg: {
meta: true,
hashScripts: true,
Expand All @@ -96,7 +90,7 @@ export const defaultSecurityConfig = (serverlUrl: string, strict: boolean) => {
}

if (strict) {
defaultConfig.headers.crossOriginEmbedderPolicy = 'require-corp'
defaultConfig.headers.crossOriginEmbedderPolicy = process.env.NODE_ENV === 'development' ? 'unsafe-none' : 'require-corp'
defaultConfig.headers.contentSecurityPolicy = {
'base-uri': ["'none'"],
'default-src' : ["'none'"],
Expand Down
53 changes: 42 additions & 11 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineNuxtModule, addServerHandler, installModule, addVitePlugin, addServerPlugin, createResolver, addImportsDir, useNitro, addServerImports } from '@nuxt/kit'
import { existsSync } from 'node:fs'
import { readFile, readdir } from 'node:fs/promises'
import { join } from 'pathe'
import { join, isAbsolute } from 'pathe'
import { defu } from 'defu'
import viteRemove from 'unplugin-remove/vite'
import { getHeadersApplicableToAllResources } from './utils/headers'
Expand Down Expand Up @@ -58,9 +58,38 @@ export default defineNuxtModule<ModuleOptions>({
// Disable module when `enabled` is set to `false`
if (!securityOptions.enabled) { return }

// Register Vite transform plugin to remove loggers
// Register transform plugin to remove loggers
if (securityOptions.removeLoggers) {
addVitePlugin(viteRemove(securityOptions.removeLoggers))
if (securityOptions.removeLoggers !== true) {
// Uses the legacy unplugin-remove plugin method
// This method is deprecated and will be removed in the future
addVitePlugin(viteRemove(securityOptions.removeLoggers))

} else {
// Uses the native method by Vite
// Vite can use either esbuild or terser
if (nuxt.options.vite.build?.minify === 'terser') {
// In case of terser, set the drop_console and drop_debugger options
nuxt.options.vite.build = defu(
{
terserOptions: { compress: { drop_console: true, drop_debugger: true } }
},
nuxt.options.vite.build
)
} else {
// In the default case, make sure minification by esbuild is turned on and set the drop option
nuxt.options.vite.build = defu(
{ minify: true },
nuxt.options.vite.build
)
nuxt.options.vite.esbuild = defu(
{
drop: ['console', 'debugger'] as ('console' | 'debugger')[],
},
nuxt.options.vite.esbuild
)
}
}
}

// Copy security headers that apply to all resources into standard route rules
Expand Down Expand Up @@ -273,13 +302,12 @@ 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
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) {
Expand All @@ -296,24 +324,27 @@ async function hashBundledAssets(nitro: Nitro) {
// 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)
const path = join(dir, entry.name)
const content = await readFile(path)
const hash = await generateHash(content, hashAlgorithm)
// construct the url as it will appear in the head template
const relativeUrl = join(baseURL, entry.name)
const fullPath = 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
const relativePath = isAbsolute(fullPath) ? fullPath.slice(1) : fullPath
const abdsoluteCdnUrl = appCdnUrl.endsWith('/') ? appCdnUrl : appCdnUrl + '/'
url = new URL(relativePath, abdsoluteCdnUrl).href
} else {
// If not, the url will be in a relative form: /_nuxt/...
url = join('/', appBaseUrl, relativeUrl)
url = join('/', appBaseUrl, fullPath)
}
sriHashes[url] = hash
}
}
}
}


return sriHashes
}
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
2 changes: 2 additions & 0 deletions src/types/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export type ContentSecurityPolicyValue = {
//'navigate-to'?: ("'self'" | "'none'" | "'unsafe-allow-redirects'" | string)[] | string | false;
'report-uri'?: string[] | string | false;
'report-to'?: string | false;
'require-trusted-types-for'?: string | false;
'trusted-types'?: string[] | string | false;
'upgrade-insecure-requests'?: boolean;
};

Expand Down
4 changes: 2 additions & 2 deletions src/types/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ export interface ModuleOptions {
sri: boolean
basicAuth: BasicAuth | false;
csrf: CsrfOptions | boolean;
removeLoggers: RemoveOptions | false;
removeLoggers: RemoveOptions | boolean;
}

export type NuxtSecurityRouteRules = Partial<
Omit<ModuleOptions, 'strict' | 'csrf' | 'basicAuth' | 'rateLimiter' | 'ssg' | 'requestSizeLimiter' >
Omit<ModuleOptions, 'strict' | 'csrf' | 'basicAuth' | 'rateLimiter' | 'ssg' | 'requestSizeLimiter' | 'removeLoggers' >
& { rateLimiter: Omit<RateLimiter, 'driver'> | false }
& { ssg: Omit<Ssg, 'exportToPresets'> | false }
& { requestSizeLimiter: RequestSizeLimiter | false }
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}`;
}
Loading

0 comments on commit 84f13ee

Please sign in to comment.