Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore/2.1.0 #532

Merged
merged 30 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fdef128
csp: add trusted types support
vejja Sep 26, 2024
080d90e
update docs for trusted types
vejja Sep 26, 2024
591e509
fix: devtools being blocked in strict mode
dungsil Sep 30, 2024
9a2e4df
Merge pull request #531 from dungsil/strict-devtools
Baroshem Oct 8, 2024
c018cd8
Merge pull request #529 from Baroshem/vejja/issue526
Baroshem Oct 8, 2024
a6b70b1
switch removeLoggers method to Vite native
vejja Oct 11, 2024
fa6c027
fix cdnUrl path component loss
vejja Oct 12, 2024
d604de8
getHeadersApplicableToAllRessources returns undefined if none
vejja Oct 24, 2024
a9bee58
Merge pull request #536 from Baroshem/vejja/issue535
vejja Oct 24, 2024
2c3d5a7
shorter syntax
vejja Oct 25, 2024
920d1fe
Merge pull request #534 from Baroshem/vejja/issue533
Baroshem Nov 4, 2024
71a4f0d
Merge pull request #539 from Baroshem/vejja/issue538
Baroshem Nov 4, 2024
4e2a603
fix-#477: bump nuxt-csurf
Baroshem Nov 4, 2024
8b22c37
replace node:crypto methods with WebCrypto equivalents
vejja Nov 11, 2024
474451f
update node minimal version to 20
vejja Nov 11, 2024
5f310ae
fix: devtools being blocked in strict mode
dungsil Sep 30, 2024
86d7470
csp: add trusted types support
vejja Sep 26, 2024
3f2ad85
update docs for trusted types
vejja Sep 26, 2024
16c0621
fix cdnUrl path component loss
vejja Oct 12, 2024
c472022
switch removeLoggers method to Vite native
vejja Oct 11, 2024
7016e5c
getHeadersApplicableToAllRessources returns undefined if none
vejja Oct 24, 2024
d83852e
shorter syntax
vejja Oct 25, 2024
1b07eec
fix-#477: bump nuxt-csurf
Baroshem Nov 4, 2024
88a46a4
Merge branch 'chore/2.1.0' into vejja/issue541
vejja Nov 11, 2024
f3706ab
Merge pull request #547 from Baroshem/vejja/issue541
vejja Nov 12, 2024
e993cf3
Add pgk.pr.new action script
vejja Nov 14, 2024
fe4c008
Merge pull request #549 from Baroshem/vejja/issue489
vejja Nov 14, 2024
8945670
Revert "feat(core): Continuous Releases"
vejja Nov 14, 2024
30f9562
Merge pull request #550 from Baroshem/revert-549-vejja/issue489
vejja Nov 14, 2024
7659ada
Update package.json
Baroshem Nov 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading