Skip to content

Commit 84f13ee

Browse files
authored
Merge pull request #532 from Baroshem/chore/2.1.0
Chore/2.1.0
2 parents c2d8728 + 7659ada commit 84f13ee

File tree

14 files changed

+323
-128
lines changed

14 files changed

+323
-128
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
strategy:
1717
matrix:
1818
os: [ubuntu-latest]
19-
node: [18]
19+
node: [20]
2020

2121
steps:
2222
- uses: actions/setup-node@v3

docs/content/1.documentation/1.getting-started/2.configuration.md

+2-7
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ interface ModuleOptions {
2626
enabled: boolean;
2727
csrf: CsrfOptions | false;
2828
nonce: boolean;
29-
removeLoggers: RemoveOptions | false;
29+
removeLoggers: boolean | RemoveOptions; // RemoveOptions is being deprecated, please use `true` instead
3030
ssg: Ssg | false;
3131
sri: boolean;
3232
}
@@ -112,12 +112,7 @@ security: {
112112
enabled: true,
113113
csrf: false,
114114
nonce: true,
115-
removeLoggers: {
116-
external: [],
117-
consoleType: ['log', 'debug'],
118-
include: [/\.[jt]sx?$/, /\.vue\??/],
119-
exclude: [/node_modules/, /\.git/]
120-
},
115+
removeLoggers: true,
121116
ssg: {
122117
meta: true,
123118
hashScripts: true,

docs/content/1.documentation/2.headers/1.csp.md

+2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ contentSecurityPolicy: {
7777
'frame-ancestors'?: ("'self'" | "'none'" | string)[] | false;
7878
'report-uri'?: string[] | false;
7979
'report-to'?: string | false;
80+
'require-trusted-types-for'?: string | false;
81+
'trusted-types'?: string[] | string | false;
8082
'upgrade-insecure-requests'?: boolean;
8183
} | false
8284
```

docs/content/1.documentation/4.utils/2.remove-console-loggers.md

+49-33
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,58 @@ By default, your application will allow log all activity in the browser when you
1212
ℹ Read more about it [here](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html#data-to-exclude).
1313
::
1414

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

17-
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).
18+
## Options
19+
20+
This feature is enabled globally default.
21+
22+
You can disable the feature by setting `removeLoggers: false`:
23+
24+
```js{}[nuxt.config.ts]
25+
export default defineNuxtConfig({
26+
modules: ['nuxt-security'],
27+
28+
security: {
29+
removeLoggers: false
30+
}
31+
})
32+
```
33+
34+
## Alternative method - deprecated
35+
36+
By default when you set `removeLoggers: true`, Nuxt Security uses the native Vite features to remove statements.
37+
38+
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).
39+
40+
::alert{type="warning"}
41+
ℹ The `unplugin-remove` method is being deprecated and will be removed in a future release.
42+
Please note that `unplugin-remove` will not remove `debugger` statements from your code.
43+
::
44+
45+
If you want to use the `unplugin-remove` plugin method, pass an object to the `removeLoggers` configuration instead of passing `true`.
46+
47+
```js{}[nuxt.config.ts]
48+
export default defineNuxtConfig({
49+
modules: ['nuxt-security'],
50+
51+
security: {
52+
removeLoggers: {
53+
external: [],
54+
consoleType: ['log', 'debug'],
55+
include: [/\.[jt]sx?$/, /\.vue\??/],
56+
exclude: [/node_modules/, /\.git/]
57+
}
58+
}
59+
})
60+
```
61+
62+
The `removeLoggers` object can be configured with following values.
1863

1964
```ts
20-
import type { FilterPattern } from '@rollup/pluginutils'
21-
export interface Options {
65+
// https://github.com/Talljack/unplugin-remove/blob/main/src/types.ts
66+
type RemoveOptions {
2267
/**
2368
* don't remove console.log and debugger these module
2469
*
@@ -47,32 +92,3 @@ export interface Options {
4792
exclude?: FilterPattern
4893
}
4994
```
50-
51-
If you would like to add some custom functionality to it, you can do so by doing the following:
52-
53-
```js{}[nuxt.config.ts]
54-
export default defineNuxtConfig({
55-
modules: ['nuxt-security'],
56-
57-
security: {
58-
removeLoggers: {
59-
external: [],
60-
consoleType: ['log', 'debug'],
61-
include: [/\.[jt]sx?$/, /\.vue\??/],
62-
exclude: [/node_modules/, /\.git/]
63-
}
64-
}
65-
})
66-
```
67-
68-
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:
69-
70-
```js{}[nuxt.config.ts]
71-
export default defineNuxtConfig({
72-
modules: ['nuxt-security'],
73-
74-
security: {
75-
removeLoggers: false
76-
}
77-
})
78-
```

package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
{
22
"name": "nuxt-security",
3-
"version": "2.0.0",
3+
"version": "2.1.0",
44
"license": "MIT",
55
"type": "module",
6+
"engines": {
7+
"node": ">=20.0.0"
8+
},
69
"homepage": "https://nuxt-security.vercel.app",
710
"description": "🛡️ Security Module for Nuxt based on HTTP Headers and Middleware",
811
"repository": {
@@ -54,7 +57,7 @@
5457
"@nuxt/kit": "^3.11.2",
5558
"basic-auth": "^2.0.1",
5659
"defu": "^6.1.1",
57-
"nuxt-csurf": "^1.5.1",
60+
"nuxt-csurf": "^1.6.5",
5861
"pathe": "^1.0.0",
5962
"unplugin-remove": "^1.0.3",
6063
"xss": "^1.0.14"

src/defaultConfig.ts

+2-8
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,7 @@ export const defaultSecurityConfig = (serverlUrl: string, strict: boolean) => {
7878
enabled: true,
7979
csrf: false,
8080
nonce: true,
81-
// https://github.com/Talljack/unplugin-remove/blob/main/src/types.ts
82-
removeLoggers: {
83-
external: [],
84-
consoleType: ['log', 'debug'],
85-
include: [/\.[jt]sx?$/, /\.vue\??/],
86-
exclude: [/node_modules/, /\.git/]
87-
},
81+
removeLoggers: true,
8882
ssg: {
8983
meta: true,
9084
hashScripts: true,
@@ -96,7 +90,7 @@ export const defaultSecurityConfig = (serverlUrl: string, strict: boolean) => {
9690
}
9791

9892
if (strict) {
99-
defaultConfig.headers.crossOriginEmbedderPolicy = 'require-corp'
93+
defaultConfig.headers.crossOriginEmbedderPolicy = process.env.NODE_ENV === 'development' ? 'unsafe-none' : 'require-corp'
10094
defaultConfig.headers.contentSecurityPolicy = {
10195
'base-uri': ["'none'"],
10296
'default-src' : ["'none'"],

src/module.ts

+42-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { defineNuxtModule, addServerHandler, installModule, addVitePlugin, addServerPlugin, createResolver, addImportsDir, useNitro, addServerImports } from '@nuxt/kit'
22
import { existsSync } from 'node:fs'
33
import { readFile, readdir } from 'node:fs/promises'
4-
import { join } from 'pathe'
4+
import { join, isAbsolute } from 'pathe'
55
import { defu } from 'defu'
66
import viteRemove from 'unplugin-remove/vite'
77
import { getHeadersApplicableToAllResources } from './utils/headers'
@@ -58,9 +58,38 @@ export default defineNuxtModule<ModuleOptions>({
5858
// Disable module when `enabled` is set to `false`
5959
if (!securityOptions.enabled) { return }
6060

61-
// Register Vite transform plugin to remove loggers
61+
// Register transform plugin to remove loggers
6262
if (securityOptions.removeLoggers) {
63-
addVitePlugin(viteRemove(securityOptions.removeLoggers))
63+
if (securityOptions.removeLoggers !== true) {
64+
// Uses the legacy unplugin-remove plugin method
65+
// This method is deprecated and will be removed in the future
66+
addVitePlugin(viteRemove(securityOptions.removeLoggers))
67+
68+
} else {
69+
// Uses the native method by Vite
70+
// Vite can use either esbuild or terser
71+
if (nuxt.options.vite.build?.minify === 'terser') {
72+
// In case of terser, set the drop_console and drop_debugger options
73+
nuxt.options.vite.build = defu(
74+
{
75+
terserOptions: { compress: { drop_console: true, drop_debugger: true } }
76+
},
77+
nuxt.options.vite.build
78+
)
79+
} else {
80+
// In the default case, make sure minification by esbuild is turned on and set the drop option
81+
nuxt.options.vite.build = defu(
82+
{ minify: true },
83+
nuxt.options.vite.build
84+
)
85+
nuxt.options.vite.esbuild = defu(
86+
{
87+
drop: ['console', 'debugger'] as ('console' | 'debugger')[],
88+
},
89+
nuxt.options.vite.esbuild
90+
)
91+
}
92+
}
6493
}
6594

6695
// Copy security headers that apply to all resources into standard route rules
@@ -273,13 +302,12 @@ function reorderNitroPlugins(nuxt: Nuxt) {
273302

274303

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

279308
// Will be later necessary to construct url
280309
const { cdnURL: appCdnUrl = '', baseURL: appBaseUrl } = nitro.options.runtimeConfig.app
281310

282-
283311
// Go through all public assets folder by folder
284312
const publicAssets = nitro.options.publicAssets
285313
for (const publicAsset of publicAssets) {
@@ -296,24 +324,27 @@ async function hashBundledAssets(nitro: Nitro) {
296324
// Node 16 compatibility maintained
297325
// Node 18.17+ supports entry.path on DirEnt
298326
// const fullPath = join(entry.path, entry.name)
299-
const fullPath = join(dir, entry.name)
300-
const fileContent = await readFile(fullPath)
301-
const hash = generateHash(fileContent, hashAlgorithm)
327+
const path = join(dir, entry.name)
328+
const content = await readFile(path)
329+
const hash = await generateHash(content, hashAlgorithm)
302330
// construct the url as it will appear in the head template
303-
const relativeUrl = join(baseURL, entry.name)
331+
const fullPath = join(baseURL, entry.name)
304332
let url: string
305333
if (appCdnUrl) {
306334
// If the cdnURL option was set, the url will be in the form https://...
307-
url = new URL(relativeUrl, appCdnUrl).href
335+
const relativePath = isAbsolute(fullPath) ? fullPath.slice(1) : fullPath
336+
const abdsoluteCdnUrl = appCdnUrl.endsWith('/') ? appCdnUrl : appCdnUrl + '/'
337+
url = new URL(relativePath, abdsoluteCdnUrl).href
308338
} else {
309339
// If not, the url will be in a relative form: /_nuxt/...
310-
url = join('/', appBaseUrl, relativeUrl)
340+
url = join('/', appBaseUrl, fullPath)
311341
}
312342
sriHashes[url] = hash
313343
}
314344
}
315345
}
316346
}
317347

348+
318349
return sriHashes
319350
}

src/runtime/nitro/plugins/30-cspSsgHashes.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default defineNitroPlugin((nitroApp) => {
2121
return
2222
}
2323

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

3939
// Parse HTML if SSG is enabled for this route
4040
if (rules.ssg) {
@@ -43,12 +43,13 @@ export default defineNitroPlugin((nitroApp) => {
4343
// Scan all relevant sections of the NuxtRenderHtmlContext
4444
const sections = ['body', 'bodyAppend', 'bodyPrepend', 'head'] as Section[]
4545
for (const section of sections) {
46-
html[section].forEach(element => {
46+
for (const element of html[section]) {
4747
if (hashScripts) {
4848
// Parse all script tags
4949
const inlineScriptMatches = element.matchAll(INLINE_SCRIPT_RE)
5050
for (const [, scriptText] of inlineScriptMatches) {
51-
scriptHashes.add(`'${generateHash(scriptText, hashAlgorithm)}'`)
51+
const hash = await generateHash(scriptText, hashAlgorithm)
52+
scriptHashes.add(`'${hash}'`)
5253
}
5354
const externalScriptMatches = element.matchAll(SCRIPT_RE)
5455
for (const [, integrity] of externalScriptMatches) {
@@ -60,7 +61,8 @@ export default defineNitroPlugin((nitroApp) => {
6061
if (hashStyles) {
6162
const styleMatches = element.matchAll(STYLE_RE)
6263
for (const [, styleText] of styleMatches) {
63-
styleHashes.add(`'${generateHash(styleText, hashAlgorithm)}'`)
64+
const hash = await generateHash(styleText, hashAlgorithm)
65+
styleHashes.add(`'${hash}'`)
6466
}
6567
}
6668

@@ -94,7 +96,7 @@ export default defineNitroPlugin((nitroApp) => {
9496
}
9597
}
9698
}
97-
})
99+
}
98100
}
99101
}
100102
})

src/runtime/nitro/plugins/40-cspSsrNonce.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { defineNitroPlugin } from '#imports'
2-
import { randomBytes } from 'node:crypto'
32
import { resolveSecurityRules } from '../context'
43

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

2928
const rules = resolveSecurityRules(event)
3029
if (rules.enabled && rules.nonce && !import.meta.prerender) {
31-
const nonce = randomBytes(16).toString('base64')
30+
const array = new Uint8Array(18);
31+
crypto.getRandomValues(array)
32+
const nonce = btoa(String.fromCharCode(...array))
3233
event.context.security!.nonce = nonce
3334
}
3435
})

src/types/headers.ts

+2
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ export type ContentSecurityPolicyValue = {
8686
//'navigate-to'?: ("'self'" | "'none'" | "'unsafe-allow-redirects'" | string)[] | string | false;
8787
'report-uri'?: string[] | string | false;
8888
'report-to'?: string | false;
89+
'require-trusted-types-for'?: string | false;
90+
'trusted-types'?: string[] | string | false;
8991
'upgrade-insecure-requests'?: boolean;
9092
};
9193

src/types/module.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ export interface ModuleOptions {
2828
sri: boolean
2929
basicAuth: BasicAuth | false;
3030
csrf: CsrfOptions | boolean;
31-
removeLoggers: RemoveOptions | false;
31+
removeLoggers: RemoveOptions | boolean;
3232
}
3333

3434
export type NuxtSecurityRouteRules = Partial<
35-
Omit<ModuleOptions, 'strict' | 'csrf' | 'basicAuth' | 'rateLimiter' | 'ssg' | 'requestSizeLimiter' >
35+
Omit<ModuleOptions, 'strict' | 'csrf' | 'basicAuth' | 'rateLimiter' | 'ssg' | 'requestSizeLimiter' | 'removeLoggers' >
3636
& { rateLimiter: Omit<RateLimiter, 'driver'> | false }
3737
& { ssg: Omit<Ssg, 'exportToPresets'> | false }
3838
& { requestSizeLimiter: RequestSizeLimiter | false }

src/utils/hash.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
import { createHash } from 'node:crypto';
21

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

0 commit comments

Comments
 (0)