nuxt-security-module 4.3.3 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -8
- package/dist/constants.js +7 -0
- package/dist/index.js +3 -6
- package/dist/models/RequestSizeLimiterOptions.d.ts +0 -1
- package/dist/models/SsgElement.d.ts +1 -0
- package/dist/models/SsgHashSource.d.ts +2 -0
- package/dist/models/SsgOptions.d.ts +0 -2
- package/dist/runtime/constants.d.ts +1 -3
- package/dist/runtime/constants.js +9 -9
- package/dist/runtime/nitro/plugins/hidePoweredBy.js +2 -2
- package/dist/runtime/nitro/plugins/nonce.js +1 -6
- package/dist/runtime/nitro/plugins/routeRules.js +8 -4
- package/dist/runtime/nitro/plugins/ssgHashes.js +2 -7
- package/dist/runtime/nitro/plugins/subresourceIntegrity.js +1 -6
- package/dist/runtime/server/middleware/basicAuth.js +1 -1
- package/dist/runtime/server/middleware/corsHandler.js +3 -2
- package/dist/runtime/server/middleware/rateLimiter.js +1 -1
- package/dist/runtime/server/middleware/requestSizeLimiter.js +5 -3
- package/dist/runtime/services/checkIsBase64.d.ts +1 -0
- package/dist/runtime/services/checkIsBase64.js +5 -0
- package/dist/runtime/services/checkIsBasicAuthorized.js +1 -0
- package/dist/runtime/services/checkIsBodyCounted.d.ts +1 -0
- package/dist/runtime/services/checkIsBodyCounted.js +4 -0
- package/dist/runtime/services/checkIsRequestTooLarge.d.ts +1 -1
- package/dist/runtime/services/checkIsRequestTooLarge.js +2 -3
- package/dist/runtime/services/checkIsSecurityHeaderKey.d.ts +2 -0
- package/dist/runtime/services/checkIsSecurityHeaderKey.js +4 -0
- package/dist/runtime/services/checkIsStaticNonce.d.ts +1 -0
- package/dist/runtime/services/checkIsStaticNonce.js +4 -0
- package/dist/runtime/services/checkIsStringList.d.ts +1 -0
- package/dist/runtime/services/checkIsStringList.js +3 -0
- package/dist/runtime/services/collectSsgHashes.js +2 -24
- package/dist/runtime/services/getBasicAuthCredentials.js +1 -3
- package/dist/runtime/services/getBasicAuthOptions.js +1 -1
- package/dist/runtime/services/getSsgHashSource.d.ts +4 -0
- package/dist/runtime/services/getSsgHashSource.js +26 -0
- package/dist/runtime/services/injectSubresourceIntegrity.js +1 -1
- package/dist/runtime/services/readHtmlTagAttribute.js +1 -1
- package/dist/runtime/services/securityRouteMatcher.d.ts +1 -0
- package/dist/runtime/services/securityRouteMatcher.js +9 -2
- package/dist/runtime/services/serializeSecurityHeaders.js +2 -10
- package/dist/runtime/services/serializeValue.d.ts +1 -0
- package/dist/runtime/services/serializeValue.js +11 -0
- package/dist/runtime/services/unquote.d.ts +1 -0
- package/dist/runtime/services/unquote.js +3 -0
- package/dist/runtime/services/updateContentSecurityPolicy.js +1 -1
- package/dist/schemas/requestSizeLimiterOptionsSchema.js +1 -4
- package/dist/schemas/ssgOptionsSchema.js +1 -3
- package/dist/services/computeSubresourceIntegrityHashes.js +10 -15
- package/dist/services/constants.js +3 -0
- package/dist/services/formatLeafIssue.js +5 -0
- package/dist/services/formatZodIssues.js +2 -12
- package/dist/services/getDeepestPathLength.js +3 -0
- package/dist/services/getDefaultModuleOptions.js +2 -7
- package/dist/services/getDefaultSecurityHeaders.js +1 -1
- package/dist/services/getLeafIssues.js +11 -0
- package/dist/services/mergeDirectives.js +12 -0
- package/dist/services/mergeFeatures.js +10 -0
- package/dist/services/mergeHeaders.js +14 -0
- package/dist/services/mergeModuleOptions.js +1 -27
- package/dist/services/mergeSources.js +9 -0
- package/package.json +9 -20
package/README.md
CHANGED
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
[![NPM downloads][badge-npm-downloads]][url-npm]
|
|
6
6
|
[![NPM Unpacked Size (with version)][badge-npm-unpacked-size]][url-npm]
|
|
7
7
|
|
|
8
|
-
Nuxt security module for [Nitro 3](https://nitro.build) and h3 2
|
|
8
|
+
This package is a Nuxt security module for [Nitro 3](https://nitro.build) and h3 2 that sets security headers on rendered pages. It also provides a per-request nonce Content Security Policy, subresource integrity, CORS, allowed methods, basic auth and a request size limit.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Design notes live in [the specs](https://github.com/Esposter/Esposter/tree/main/openspec/specs/nuxt-security-module).
|
|
11
11
|
|
|
12
12
|
## Table of Contents
|
|
13
13
|
|
|
14
14
|
- 🚀 [Getting Started](#getting-started)
|
|
15
|
-
-
|
|
15
|
+
- 🧩 [Key Exports](#key-exports)
|
|
16
|
+
- 📝 [Usage](#usage)
|
|
16
17
|
- ⚖️ [License](#license)
|
|
17
18
|
|
|
18
19
|
---
|
|
@@ -28,17 +29,43 @@ pnpm i nuxt-security-module
|
|
|
28
29
|
export default defineNuxtConfig({ modules: ["nuxt-security-module"] });
|
|
29
30
|
```
|
|
30
31
|
|
|
31
|
-
## <a name="
|
|
32
|
+
## <a name="key-exports">🧩 Key Exports</a>
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
| Export | Kind | Purpose |
|
|
35
|
+
| :----------------------- | :-------- | :----------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
+
| `createRouteRulesPlugin` | function | Exported from `./runtime/nitro/plugins/routeRules`: the plugin that matches each request against the build's route rules |
|
|
37
|
+
| `default` | const | The Nuxt module, set up from the `security` config key; each feature registers only when its option is on |
|
|
38
|
+
| `ModuleOptions` | interface | The module's options, one key per feature, each on or off; the `security` config key takes a partial of them |
|
|
39
|
+
| `rateLimiter` | const | Default export of `./runtime/server/middleware/rateLimiter`: refuses a request with `429` once its key's window is spent |
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
## <a name="usage">📝 Usage</a>
|
|
36
42
|
|
|
37
|
-
|
|
43
|
+
```ts
|
|
44
|
+
// nuxt.config.ts
|
|
45
|
+
export default defineNuxtConfig({
|
|
46
|
+
modules: ["nuxt-security-module"],
|
|
47
|
+
security: {
|
|
48
|
+
// Refuses a client with 429 after 100 requests in each minute-long window
|
|
49
|
+
rateLimiter: { intervalInMilliseconds: 60000, tokensPerInterval: 100 },
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A request's body limit is the one its route sets, whatever its `Content-Type` claims, since a client writes that header. A route that takes a larger body names its own limit in its route rule:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// nuxt.config.ts
|
|
58
|
+
export default defineNuxtConfig({
|
|
59
|
+
routeRules: {
|
|
60
|
+
// Lets a request to this route carry a 10 MB body, where the rest of the app takes the 2 MB default
|
|
61
|
+
"/api/import/**": { security: { requestSizeLimiter: { maxRequestSizeInBytes: 10_000_000 } } },
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
```
|
|
38
65
|
|
|
39
66
|
## <a name="license">⚖️ License</a>
|
|
40
67
|
|
|
41
|
-
Apache-2.0
|
|
68
|
+
Licensed under [Apache-2.0][url-license].
|
|
42
69
|
|
|
43
70
|
[badge-license]: https://img.shields.io/github/license/Esposter/Esposter.svg?color=blue
|
|
44
71
|
[url-license]: https://github.com/Esposter/Esposter/blob/main/LICENSE
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { MODULE_NAME } from "./runtime/constants.js";
|
|
2
|
+
// src/constants.ts
|
|
3
|
+
const ROUTE_RULES_TEMPLATE_FILENAME = `#${MODULE_NAME}/route-rules.mjs`;
|
|
4
|
+
const SRI_HASHES_TEMPLATE_FILENAME = `#${MODULE_NAME}/sri-hashes.mjs`;
|
|
5
|
+
const ROUTE_RULES_PLUGIN_FILENAME = `#${MODULE_NAME}/route-rules-plugin.mjs`;
|
|
6
|
+
const SRI_PLUGIN_FILENAME = `#${MODULE_NAME}/sri-plugin.mjs`;
|
|
7
|
+
export { ROUTE_RULES_PLUGIN_FILENAME, ROUTE_RULES_TEMPLATE_FILENAME, SRI_HASHES_TEMPLATE_FILENAME, SRI_PLUGIN_FILENAME };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ROUTE_RULES_PLUGIN_FILENAME, ROUTE_RULES_TEMPLATE_FILENAME, SRI_HASHES_TEMPLATE_FILENAME, SRI_PLUGIN_FILENAME } from "./constants.js";
|
|
1
2
|
import { MODULE_NAME } from "./runtime/constants.js";
|
|
2
3
|
import { checkIsRecord } from "./runtime/services/checkIsRecord.js";
|
|
3
4
|
import { computeSubresourceIntegrityHashes } from "./services/computeSubresourceIntegrityHashes.js";
|
|
@@ -5,18 +6,14 @@ import { getDefaultModuleOptions } from "./services/getDefaultModuleOptions.js";
|
|
|
5
6
|
import { getPluginTemplate } from "./services/getPluginTemplate.js";
|
|
6
7
|
import { getRemoveLoggersViteConfig } from "./services/getRemoveLoggersViteConfig.js";
|
|
7
8
|
import { getRouteRulesTemplate } from "./services/getRouteRulesTemplate.js";
|
|
8
|
-
import { mergeModuleOptions } from "./services/mergeModuleOptions.js";
|
|
9
9
|
import { getRouteSecurityRules } from "./services/getRouteSecurityRules.js";
|
|
10
10
|
import { getStaticResourceHeaders } from "./services/getStaticResourceHeaders.js";
|
|
11
11
|
import { getTypeTemplate } from "./services/getTypeTemplate.js";
|
|
12
|
+
import { mergeModuleOptions } from "./services/mergeModuleOptions.js";
|
|
12
13
|
import { parseModuleOptions } from "./services/parseModuleOptions.js";
|
|
13
14
|
import { defu } from "defu";
|
|
14
15
|
import { addNitroPlugin, addServerHandler, addServerTemplate, addTypeTemplate, createResolver, defineNuxtModule } from "@nuxt/kit";
|
|
15
16
|
// src/module.ts
|
|
16
|
-
const ROUTE_RULES_TEMPLATE_FILENAME = `#${MODULE_NAME}/route-rules.mjs`;
|
|
17
|
-
const SRI_HASHES_TEMPLATE_FILENAME = `#${MODULE_NAME}/sri-hashes.mjs`;
|
|
18
|
-
const ROUTE_RULES_PLUGIN_FILENAME = `#${MODULE_NAME}/route-rules-plugin.mjs`;
|
|
19
|
-
const SRI_PLUGIN_FILENAME = `#${MODULE_NAME}/sri-plugin.mjs`;
|
|
20
17
|
const securityModule = defineNuxtModule({
|
|
21
18
|
meta: {
|
|
22
19
|
compatibility: { nuxt: ">=5.0.0-0" },
|
|
@@ -88,7 +85,7 @@ const securityModule = defineNuxtModule({
|
|
|
88
85
|
filename: SRI_HASHES_TEMPLATE_FILENAME,
|
|
89
86
|
getContents: () => `export default ${JSON.stringify(sriHashes)}\n`
|
|
90
87
|
});
|
|
91
|
-
nuxt.hook("nitro:build:
|
|
88
|
+
nuxt.hook("nitro:build:public-assets", async (nitro) => {
|
|
92
89
|
sriHashes = await computeSubresourceIntegrityHashes(nitro);
|
|
93
90
|
});
|
|
94
91
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type SsgElement = [name: string, attributes: string, body: string | undefined];
|
|
@@ -11,12 +11,10 @@ export declare const NONCE_BYTE_LENGTH = 18;
|
|
|
11
11
|
export declare const NONCE_PLACEHOLDER = "'nonce-{{nonce}}'";
|
|
12
12
|
export declare const SRI_HASH_ALGORITHM = "SHA-384";
|
|
13
13
|
export declare const CSP_HASH_ALGORITHM = "SHA-256";
|
|
14
|
-
export declare const PRERENDERED_HEADERS_DIRECTORY_NAME = "nuxt-security";
|
|
15
|
-
export declare const PRERENDERED_HEADERS_FILE_NAME = "headers.json";
|
|
16
14
|
export declare const HTML_ATTRIBUTE_REGEX: RegExp;
|
|
17
15
|
export declare const HTML_TAG_REGEX: RegExp;
|
|
18
16
|
export declare const NONCE_TAG_NAMES: readonly string[];
|
|
19
|
-
export declare const
|
|
17
|
+
export declare const HTML_SECTIONS: readonly ["head", "bodyPrepend", "body", "bodyAppend"];
|
|
20
18
|
export declare const SIZE_LIMITED_METHODS: readonly string[];
|
|
21
19
|
export declare const DEFAULT_BASIC_AUTH_REALM = "Please enter username and password";
|
|
22
20
|
export declare const BASIC_AUTHORIZATION_SCHEME = "Basic";
|
|
@@ -19,19 +19,18 @@ const SECURITY_HEADER_NAMES = {
|
|
|
19
19
|
const CONTENT_SECURITY_POLICY_REPORT_ONLY_HEADER_NAME = "Content-Security-Policy-Report-Only";
|
|
20
20
|
const MAX_RATE_LIMIT_ENTRIES = 1e4;
|
|
21
21
|
const UNKNOWN_CLIENT_KEY = "unknown";
|
|
22
|
-
const
|
|
22
|
+
const STATE_CHANGING_METHODS = [
|
|
23
23
|
"DELETE",
|
|
24
24
|
"PATCH",
|
|
25
25
|
"POST",
|
|
26
26
|
"PUT"
|
|
27
27
|
];
|
|
28
|
+
const CSRF_UNSAFE_METHODS = STATE_CHANGING_METHODS;
|
|
28
29
|
const CSRF_TOKEN_BYTE_LENGTH = 32;
|
|
29
30
|
const NONCE_BYTE_LENGTH = 18;
|
|
30
31
|
const NONCE_PLACEHOLDER = "'nonce-{{nonce}}'";
|
|
31
32
|
const SRI_HASH_ALGORITHM = "SHA-384";
|
|
32
33
|
const CSP_HASH_ALGORITHM = "SHA-256";
|
|
33
|
-
const PRERENDERED_HEADERS_DIRECTORY_NAME = "nuxt-security";
|
|
34
|
-
const PRERENDERED_HEADERS_FILE_NAME = "headers.json";
|
|
35
34
|
const HTML_ATTRIBUTE_PATTERN = String.raw`\s+[^\s"'<>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'<>=]+))?`;
|
|
36
35
|
const HTML_ATTRIBUTE_REGEX = /\s+(?<name>[^\s"'<>/=]+)(?<assignment>\s*=\s*(?<value>"[^"]*"|'[^']*'|[^\s"'<>=]+))?/gu;
|
|
37
36
|
const HTML_TAG_REGEX = new RegExp(String.raw`<(script|style)\b((?:${HTML_ATTRIBUTE_PATTERN})*)(\s*\/?)>([\s\S]*?)<\/\1\s*>|<([a-z][\w:-]*)((?:${HTML_ATTRIBUTE_PATTERN})*)(\s*\/?)>`, "giu");
|
|
@@ -40,12 +39,13 @@ const NONCE_TAG_NAMES = [
|
|
|
40
39
|
"script",
|
|
41
40
|
"style"
|
|
42
41
|
];
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
42
|
+
const HTML_SECTIONS = [
|
|
43
|
+
"head",
|
|
44
|
+
"bodyPrepend",
|
|
45
|
+
"body",
|
|
46
|
+
"bodyAppend"
|
|
48
47
|
];
|
|
48
|
+
const SIZE_LIMITED_METHODS = STATE_CHANGING_METHODS;
|
|
49
49
|
const DEFAULT_BASIC_AUTH_REALM = "Please enter username and password";
|
|
50
50
|
const BASIC_AUTHORIZATION_SCHEME = "Basic";
|
|
51
|
-
export { BASIC_AUTHORIZATION_SCHEME, CONTENT_SECURITY_POLICY_REPORT_ONLY_HEADER_NAME, CSP_HASH_ALGORITHM, CSRF_TOKEN_BYTE_LENGTH, CSRF_UNSAFE_METHODS, DEFAULT_BASIC_AUTH_REALM,
|
|
51
|
+
export { BASIC_AUTHORIZATION_SCHEME, CONTENT_SECURITY_POLICY_REPORT_ONLY_HEADER_NAME, CSP_HASH_ALGORITHM, CSRF_TOKEN_BYTE_LENGTH, CSRF_UNSAFE_METHODS, DEFAULT_BASIC_AUTH_REALM, HTML_ATTRIBUTE_REGEX, HTML_SECTIONS, HTML_TAG_REGEX, MAX_RATE_LIMIT_ENTRIES, MODULE_NAME, NONCE_BYTE_LENGTH, NONCE_PLACEHOLDER, NONCE_TAG_NAMES, ROUTE_RULES_HOOK, SECURITY_HEADER_NAMES, SIZE_LIMITED_METHODS, SRI_HASH_ALGORITHM, UNKNOWN_CLIENT_KEY };
|
|
@@ -3,10 +3,10 @@ import { definePlugin } from "nitro";
|
|
|
3
3
|
import { isEvent } from "nitro/h3";
|
|
4
4
|
// src/runtime/nitro/plugins/hidePoweredBy.ts
|
|
5
5
|
const hidePoweredBy = definePlugin((nitroApp) => {
|
|
6
|
-
nitroApp.hooks.hook("response", (
|
|
6
|
+
nitroApp.hooks.hook("response", (response, event) => {
|
|
7
7
|
if (!isEvent(event)) return;
|
|
8
8
|
const rules = getSecurityRules(event);
|
|
9
|
-
if (rules?.enabled && rules.hidePoweredBy)
|
|
9
|
+
if (rules?.enabled && rules.hidePoweredBy) response.headers.delete("x-powered-by");
|
|
10
10
|
});
|
|
11
11
|
});
|
|
12
12
|
export { hidePoweredBy as default };
|
|
@@ -1,15 +1,10 @@
|
|
|
1
|
+
import { HTML_SECTIONS } from "../../constants.js";
|
|
1
2
|
import { generateNonce } from "../../services/generateNonce.js";
|
|
2
3
|
import { getSecurityRules } from "../../services/getSecurityRules.js";
|
|
3
4
|
import { getSecurityState } from "../../services/getSecurityState.js";
|
|
4
5
|
import { injectNonce } from "../../services/injectNonce.js";
|
|
5
6
|
import { definePlugin } from "nitro";
|
|
6
7
|
// src/runtime/nitro/plugins/nonce.ts
|
|
7
|
-
const HTML_SECTIONS = [
|
|
8
|
-
"body",
|
|
9
|
-
"bodyAppend",
|
|
10
|
-
"bodyPrepend",
|
|
11
|
-
"head"
|
|
12
|
-
];
|
|
13
8
|
const noncePlugin = definePlugin((nitroApp) => {
|
|
14
9
|
if (import.meta.prerender) return;
|
|
15
10
|
nitroApp.hooks.hook("render:route", (_context, { event }) => {
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { ROUTE_RULES_HOOK } from "../../constants.js";
|
|
2
|
-
import { setSecurityRouteMatcher } from "../../services/securityRouteMatcher.js";
|
|
2
|
+
import { setSecurityRouteMatcher, setSecurityRouteMatcherFailure } from "../../services/securityRouteMatcher.js";
|
|
3
3
|
import { definePlugin } from "nitro";
|
|
4
4
|
// src/runtime/nitro/plugins/routeRules.ts
|
|
5
5
|
const createRouteRulesPlugin = (routeRules) => definePlugin((nitroApp) => {
|
|
6
|
+
const resolveRouteRules = async () => {
|
|
7
|
+
const routes = structuredClone(routeRules);
|
|
8
|
+
await nitroApp.hooks.callHook(ROUTE_RULES_HOOK, routes);
|
|
9
|
+
setSecurityRouteMatcher(routes);
|
|
10
|
+
};
|
|
6
11
|
let routesResolved;
|
|
7
12
|
nitroApp.hooks.hook("request", () => {
|
|
8
13
|
routesResolved ??= (async () => {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
setSecurityRouteMatcher(routes);
|
|
14
|
+
const [resolution] = await Promise.allSettled([resolveRouteRules()]);
|
|
15
|
+
if (resolution.status === "rejected") setSecurityRouteMatcherFailure(resolution.reason);
|
|
12
16
|
})();
|
|
13
17
|
return routesResolved;
|
|
14
18
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HTML_SECTIONS } from "../../constants.js";
|
|
1
2
|
import { collectSsgHashes } from "../../services/collectSsgHashes.js";
|
|
2
3
|
import { getSecurityRules } from "../../services/getSecurityRules.js";
|
|
3
4
|
import { getSecurityState } from "../../services/getSecurityState.js";
|
|
@@ -5,12 +6,6 @@ import { insertContentSecurityPolicyMeta } from "../../services/insertContentSec
|
|
|
5
6
|
import { updateContentSecurityPolicy } from "../../services/updateContentSecurityPolicy.js";
|
|
6
7
|
import { definePlugin } from "nitro";
|
|
7
8
|
// src/runtime/nitro/plugins/ssgHashes.ts
|
|
8
|
-
const HTML_SECTIONS = [
|
|
9
|
-
"head",
|
|
10
|
-
"bodyPrepend",
|
|
11
|
-
"body",
|
|
12
|
-
"bodyAppend"
|
|
13
|
-
];
|
|
14
9
|
const ssgHashes = definePlugin((nitroApp) => {
|
|
15
10
|
if (!import.meta.prerender) return;
|
|
16
11
|
nitroApp.hooks.hook("render:html", async (html, { event }) => {
|
|
@@ -18,7 +13,7 @@ const ssgHashes = definePlugin((nitroApp) => {
|
|
|
18
13
|
if (!rules?.enabled || !rules.ssg || !rules.headers || !rules.headers.contentSecurityPolicy) return;
|
|
19
14
|
const state = getSecurityState(event);
|
|
20
15
|
state.hashes = await collectSsgHashes(HTML_SECTIONS.map((section) => html[section].join("")).join(""), rules.ssg);
|
|
21
|
-
if (!rules.ssg.meta) return;
|
|
16
|
+
if (!rules.ssg.meta || rules.contentSecurityPolicyReportOnly) return;
|
|
22
17
|
const policy = updateContentSecurityPolicy(rules.headers.contentSecurityPolicy, state);
|
|
23
18
|
html.head = [insertContentSecurityPolicyMeta(html.head.join(""), policy)];
|
|
24
19
|
});
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
+
import { HTML_SECTIONS } from "../../constants.js";
|
|
1
2
|
import { getSecurityRules } from "../../services/getSecurityRules.js";
|
|
2
3
|
import { injectSubresourceIntegrity } from "../../services/injectSubresourceIntegrity.js";
|
|
3
4
|
import { definePlugin } from "nitro";
|
|
4
5
|
// src/runtime/nitro/plugins/subresourceIntegrity.ts
|
|
5
|
-
const HTML_SECTIONS = [
|
|
6
|
-
"body",
|
|
7
|
-
"bodyAppend",
|
|
8
|
-
"bodyPrepend",
|
|
9
|
-
"head"
|
|
10
|
-
];
|
|
11
6
|
const createSubresourceIntegrityPlugin = (sriHashes) => definePlugin((nitroApp) => {
|
|
12
7
|
nitroApp.hooks.hook("render:html", (html, { event }) => {
|
|
13
8
|
const rules = getSecurityRules(event);
|
|
@@ -13,8 +13,8 @@ const basicAuthMiddleware = defineMiddleware((event) => {
|
|
|
13
13
|
const { pathname } = event.url;
|
|
14
14
|
if (!checkIsChallenged(pathname, basicAuth)) return;
|
|
15
15
|
if (checkIsBasicAuthorized(event.req.headers.get("authorization"), basicAuth)) return;
|
|
16
|
-
event.res.headers.set("WWW-Authenticate", serializeBasicAuthChallenge(basicAuth.message || "Please enter username and password"));
|
|
17
16
|
throw new HTTPError({
|
|
17
|
+
headers: { "WWW-Authenticate": serializeBasicAuthChallenge(basicAuth.message || "Please enter username and password") },
|
|
18
18
|
status: 401,
|
|
19
19
|
statusText: "Access denied"
|
|
20
20
|
});
|
|
@@ -4,9 +4,9 @@ import { handleCors } from "nitro/h3";
|
|
|
4
4
|
// src/runtime/server/middleware/corsHandler.ts
|
|
5
5
|
const corsHandler = defineMiddleware((event) => {
|
|
6
6
|
const rules = getSecurityRules(event);
|
|
7
|
-
if (!rules?.enabled || !rules.corsHandler) return;
|
|
7
|
+
if (!rules?.enabled || !rules.corsHandler) return void 0;
|
|
8
8
|
const { allowHeaders, credentials, exposeHeaders, maxAge, methods, origin, preflight } = rules.corsHandler;
|
|
9
|
-
handleCors(event, {
|
|
9
|
+
const response = handleCors(event, {
|
|
10
10
|
allowHeaders,
|
|
11
11
|
credentials,
|
|
12
12
|
exposeHeaders,
|
|
@@ -15,5 +15,6 @@ const corsHandler = defineMiddleware((event) => {
|
|
|
15
15
|
origin: [origin].flat(),
|
|
16
16
|
preflight
|
|
17
17
|
});
|
|
18
|
+
return response === false ? void 0 : response;
|
|
18
19
|
});
|
|
19
20
|
export { corsHandler as default };
|
|
@@ -12,8 +12,8 @@ const rateLimiter = defineMiddleware(async (event) => {
|
|
|
12
12
|
const key = getRequestIP(event, { xForwardedFor: false }) ?? "unknown";
|
|
13
13
|
const retryAfterSeconds = await getRetryAfterSeconds(rateLimiterStore, key, rules.rateLimiter);
|
|
14
14
|
if (retryAfterSeconds === void 0) return;
|
|
15
|
-
event.res.headers.set("Retry-After", String(retryAfterSeconds));
|
|
16
15
|
throw new HTTPError({
|
|
16
|
+
headers: { "Retry-After": String(retryAfterSeconds) },
|
|
17
17
|
status: 429,
|
|
18
18
|
statusText: "Too Many Requests"
|
|
19
19
|
});
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
import { checkIsBodyCounted } from "../../services/checkIsBodyCounted.js";
|
|
1
2
|
import { checkIsRequestTooLarge } from "../../services/checkIsRequestTooLarge.js";
|
|
2
3
|
import { getSecurityRules } from "../../services/getSecurityRules.js";
|
|
3
4
|
import { defineMiddleware } from "nitro";
|
|
4
|
-
import { HTTPError } from "nitro/h3";
|
|
5
|
+
import { HTTPError, assertBodySize } from "nitro/h3";
|
|
5
6
|
// src/runtime/server/middleware/requestSizeLimiter.ts
|
|
6
7
|
const requestSizeLimiter = defineMiddleware((event) => {
|
|
7
8
|
const rules = getSecurityRules(event);
|
|
8
9
|
if (!rules?.enabled || !rules.requestSizeLimiter) return;
|
|
10
|
+
const { method } = event.req;
|
|
9
11
|
const contentLength = event.req.headers.get("content-length");
|
|
10
|
-
|
|
11
|
-
if (checkIsRequestTooLarge(event.req.method, contentLength, contentType, rules.requestSizeLimiter)) throw new HTTPError({
|
|
12
|
+
if (checkIsRequestTooLarge(method, contentLength, rules.requestSizeLimiter)) throw new HTTPError({
|
|
12
13
|
status: 413,
|
|
13
14
|
statusText: "Payload Too Large"
|
|
14
15
|
});
|
|
16
|
+
if (checkIsBodyCounted(method, contentLength)) assertBodySize(event, rules.requestSizeLimiter.maxRequestSizeInBytes);
|
|
15
17
|
});
|
|
16
18
|
export { requestSizeLimiter as default };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const checkIsBase64: (value: string) => boolean;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// src/runtime/services/checkIsBase64.ts
|
|
2
|
+
const BASE64_REGEX = /^[\d+/A-Za-z]*={0,2}$/u;
|
|
3
|
+
const BASE64_QUANTUM_LENGTH = 4;
|
|
4
|
+
const checkIsBase64 = (value) => BASE64_REGEX.test(value) && (value.endsWith("=") ? value.length % BASE64_QUANTUM_LENGTH === 0 : value.length % BASE64_QUANTUM_LENGTH !== 1);
|
|
5
|
+
export { checkIsBase64 };
|
|
@@ -2,6 +2,7 @@ import { checkIsEqualInConstantTime } from "./checkIsEqualInConstantTime.js";
|
|
|
2
2
|
import { getBasicAuthCredentials } from "./getBasicAuthCredentials.js";
|
|
3
3
|
// src/runtime/services/checkIsBasicAuthorized.ts
|
|
4
4
|
const checkIsBasicAuthorized = (authorization, { name, pass }) => {
|
|
5
|
+
if (!name || !pass) return false;
|
|
5
6
|
const credentials = getBasicAuthCredentials(authorization);
|
|
6
7
|
if (!credentials) return false;
|
|
7
8
|
const isNameEqual = checkIsEqualInConstantTime(credentials.name, name);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const checkIsBodyCounted: (method: string, contentLength: null | string) => boolean;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { RequestSizeLimiterOptions } from "../../models/RequestSizeLimiterOptions.js";
|
|
2
|
-
export declare const checkIsRequestTooLarge: (method: string, contentLength: null | string,
|
|
2
|
+
export declare const checkIsRequestTooLarge: (method: string, contentLength: null | string, { maxRequestSizeInBytes }: RequestSizeLimiterOptions) => boolean;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { SIZE_LIMITED_METHODS } from "../constants.js";
|
|
2
2
|
// src/runtime/services/checkIsRequestTooLarge.ts
|
|
3
|
-
const checkIsRequestTooLarge = (method, contentLength,
|
|
3
|
+
const checkIsRequestTooLarge = (method, contentLength, { maxRequestSizeInBytes }) => {
|
|
4
4
|
if (!SIZE_LIMITED_METHODS.includes(method) || contentLength === null) return false;
|
|
5
|
-
|
|
6
|
-
return Math.trunc(Number(contentLength)) >= limit;
|
|
5
|
+
return Math.trunc(Number(contentLength)) > maxRequestSizeInBytes;
|
|
7
6
|
};
|
|
8
7
|
export { checkIsRequestTooLarge };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const checkIsStaticNonce: (source: string) => boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const checkIsStringList: (value: unknown) => value is string[];
|
|
@@ -1,28 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { readHtmlTagAttribute } from "./readHtmlTagAttribute.js";
|
|
1
|
+
import { HTML_TAG_REGEX } from "../constants.js";
|
|
2
|
+
import { getSsgHashSource } from "./getSsgHashSource.js";
|
|
4
3
|
// src/runtime/services/collectSsgHashes.ts
|
|
5
|
-
const SCRIPT_PRELOAD_TYPES = /* @__PURE__ */ new Set([
|
|
6
|
-
"audioworklet",
|
|
7
|
-
"paintworklet",
|
|
8
|
-
"script",
|
|
9
|
-
"xslt"
|
|
10
|
-
]);
|
|
11
|
-
const getSsgHashSource = async ([name, attributes, body], { hashScripts, hashStyles }) => {
|
|
12
|
-
const integrity = readHtmlTagAttribute(attributes, "integrity");
|
|
13
|
-
if (name === "script" && hashScripts) {
|
|
14
|
-
const source = readHtmlTagAttribute(attributes, "src");
|
|
15
|
-
if (source !== void 0 && integrity !== void 0) return ["script", `'${integrity}'`];
|
|
16
|
-
if (source === void 0 && body) return ["script", `'${await generateHash(body, CSP_HASH_ALGORITHM)}'`];
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
if (name === "style" && hashStyles && body) return ["style", `'${await generateHash(body, CSP_HASH_ALGORITHM)}'`];
|
|
20
|
-
if (name !== "link" || integrity === void 0) return void 0;
|
|
21
|
-
const rel = readHtmlTagAttribute(attributes, "rel");
|
|
22
|
-
const as = readHtmlTagAttribute(attributes, "as") ?? "";
|
|
23
|
-
if (rel === "stylesheet" && hashStyles) return ["style", `'${integrity}'`];
|
|
24
|
-
if (hashScripts && (rel === "modulepreload" || rel === "preload" && SCRIPT_PRELOAD_TYPES.has(as))) return ["script", `'${integrity}'`];
|
|
25
|
-
};
|
|
26
4
|
const collectSsgHashes = async (html, options) => {
|
|
27
5
|
const elements = Array.from(html.matchAll(HTML_TAG_REGEX), ([, elementName, elementAttributes, , body, tagName, tagAttributes]) => [
|
|
28
6
|
(elementName ?? tagName ?? "").toLowerCase(),
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { BASIC_AUTHORIZATION_SCHEME } from "../constants.js";
|
|
2
|
+
import { checkIsBase64 } from "./checkIsBase64.js";
|
|
2
3
|
// src/runtime/services/getBasicAuthCredentials.ts
|
|
3
|
-
const BASE64_REGEX = /^[\d+/A-Za-z]*={0,2}$/u;
|
|
4
|
-
const BASE64_QUANTUM_LENGTH = 4;
|
|
5
|
-
const checkIsBase64 = (value) => BASE64_REGEX.test(value) && (value.endsWith("=") ? value.length % BASE64_QUANTUM_LENGTH === 0 : value.length % BASE64_QUANTUM_LENGTH !== 1);
|
|
6
4
|
const getBasicAuthCredentials = (authorization) => {
|
|
7
5
|
if (!authorization?.startsWith(`Basic `)) return void 0;
|
|
8
6
|
const encoded = authorization.slice(BASIC_AUTHORIZATION_SCHEME.length + 1).trim();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { checkIsRecord } from "./checkIsRecord.js";
|
|
2
|
+
import { checkIsStringList } from "./checkIsStringList.js";
|
|
2
3
|
// src/runtime/services/getBasicAuthOptions.ts
|
|
3
|
-
const checkIsStringList = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
4
4
|
const getBasicAuthOptions = (privateRuntimeConfig) => {
|
|
5
5
|
if (!checkIsRecord(privateRuntimeConfig) || !checkIsRecord(privateRuntimeConfig.basicAuth)) return void 0;
|
|
6
6
|
const { exclude, include, message, name, pass } = privateRuntimeConfig.basicAuth;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { SsgElement } from "../../models/SsgElement.js";
|
|
2
|
+
import { SsgHashSource } from "../../models/SsgHashSource.js";
|
|
3
|
+
import { SsgOptions } from "../../models/SsgOptions.js";
|
|
4
|
+
export declare const getSsgHashSource: ([name, attributes, body]: SsgElement, { hashScripts, hashStyles }: SsgOptions) => Promise<SsgHashSource | undefined>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CSP_HASH_ALGORITHM } from "../constants.js";
|
|
2
|
+
import { generateHash } from "./generateHash.js";
|
|
3
|
+
import { readHtmlTagAttribute } from "./readHtmlTagAttribute.js";
|
|
4
|
+
// src/runtime/services/getSsgHashSource.ts
|
|
5
|
+
const SCRIPT_PRELOAD_TYPES = /* @__PURE__ */ new Set([
|
|
6
|
+
"audioworklet",
|
|
7
|
+
"paintworklet",
|
|
8
|
+
"script",
|
|
9
|
+
"xslt"
|
|
10
|
+
]);
|
|
11
|
+
const getSsgHashSource = async ([name, attributes, body], { hashScripts, hashStyles }) => {
|
|
12
|
+
const integrity = readHtmlTagAttribute(attributes, "integrity");
|
|
13
|
+
if (name === "script" && hashScripts) {
|
|
14
|
+
const source = readHtmlTagAttribute(attributes, "src");
|
|
15
|
+
if (source !== void 0 && integrity !== void 0) return ["script", `'${integrity}'`];
|
|
16
|
+
if (source === void 0 && body) return ["script", `'${await generateHash(body, CSP_HASH_ALGORITHM)}'`];
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (name === "style" && hashStyles && body) return ["style", `'${await generateHash(body, CSP_HASH_ALGORITHM)}'`];
|
|
20
|
+
if (name !== "link" || integrity === void 0) return void 0;
|
|
21
|
+
const rel = readHtmlTagAttribute(attributes, "rel");
|
|
22
|
+
const as = readHtmlTagAttribute(attributes, "as") ?? "";
|
|
23
|
+
if (rel === "stylesheet" && hashStyles) return ["style", `'${integrity}'`];
|
|
24
|
+
if (hashScripts && (rel === "modulepreload" || rel === "preload" && SCRIPT_PRELOAD_TYPES.has(as))) return ["script", `'${integrity}'`];
|
|
25
|
+
};
|
|
26
|
+
export { getSsgHashSource };
|
|
@@ -14,7 +14,7 @@ const injectSubresourceIntegrity = (html, hashes) => rewriteHtmlTags(html, (tagN
|
|
|
14
14
|
const resourceAttributeName = RESOURCE_ATTRIBUTE_NAMES[tagName.toLowerCase()];
|
|
15
15
|
if (!resourceAttributeName || readHtmlTagAttribute(attributes, "integrity") !== void 0) return void 0;
|
|
16
16
|
const resource = readHtmlTagAttribute(attributes, resourceAttributeName);
|
|
17
|
-
if (resource === void 0 || hashes
|
|
17
|
+
if (resource === void 0 || !Object.hasOwn(hashes, resource)) return void 0;
|
|
18
18
|
if (tagName.toLowerCase() === "link" && !INTEGRITY_LINK_RELS.has(readHtmlTagAttribute(attributes, "rel") ?? "")) return void 0;
|
|
19
19
|
return ` integrity="${hashes[resource]}"${attributes}`;
|
|
20
20
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HTML_ATTRIBUTE_REGEX } from "../constants.js";
|
|
2
|
+
import { unquote } from "./unquote.js";
|
|
2
3
|
// src/runtime/services/readHtmlTagAttribute.ts
|
|
3
4
|
const readHtmlTagAttribute = (attributes, attributeName) => {
|
|
4
5
|
for (const [, name, , value] of attributes.matchAll(HTML_ATTRIBUTE_REGEX)) if (name?.toLowerCase() === attributeName) return unquote(value ?? "");
|
|
5
6
|
};
|
|
6
|
-
const unquote = (value) => value.replace(/^(?<quote>["'])(?<inner>.*)\k<quote>$/su, "$<inner>");
|
|
7
7
|
export { readHtmlTagAttribute };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { SecurityRules } from "../../models/SecurityRules.js";
|
|
2
2
|
export declare const setSecurityRouteMatcher: (routes: Record<string, SecurityRules>) => void;
|
|
3
|
+
export declare const setSecurityRouteMatcherFailure: (cause: unknown) => void;
|
|
3
4
|
export declare const findSecurityRules: (pathname: string) => SecurityRules[] | undefined;
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { addRoute, createRouter, findAllRoutes } from "rou3";
|
|
2
2
|
// src/runtime/services/securityRouteMatcher.ts
|
|
3
3
|
let securityRouter;
|
|
4
|
+
let securityRouteFailure;
|
|
4
5
|
const setSecurityRouteMatcher = (routes) => {
|
|
5
6
|
const router = createRouter();
|
|
6
7
|
for (const [pattern, rules] of Object.entries(routes)) addRoute(router, "", pattern, rules);
|
|
7
8
|
securityRouter = router;
|
|
8
9
|
};
|
|
9
|
-
const
|
|
10
|
-
|
|
10
|
+
const setSecurityRouteMatcherFailure = (cause) => {
|
|
11
|
+
securityRouteFailure = new TypeError("nuxt-security-module: the security route rules did not resolve", { cause });
|
|
12
|
+
};
|
|
13
|
+
const findSecurityRules = (pathname) => {
|
|
14
|
+
if (securityRouteFailure) throw securityRouteFailure;
|
|
15
|
+
return securityRouter && findAllRoutes(securityRouter, "", pathname).flatMap(({ data }) => data ? [data] : []);
|
|
16
|
+
};
|
|
17
|
+
export { findSecurityRules, setSecurityRouteMatcher, setSecurityRouteMatcherFailure };
|
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
import { SECURITY_HEADER_NAMES } from "../constants.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { serializeStrictTransportSecurity } from "./serializeStrictTransportSecurity.js";
|
|
2
|
+
import { checkIsSecurityHeaderKey } from "./checkIsSecurityHeaderKey.js";
|
|
3
|
+
import { serializeValue } from "./serializeValue.js";
|
|
5
4
|
// src/runtime/services/serializeSecurityHeaders.ts
|
|
6
|
-
const checkIsSecurityHeaderKey = (key) => Object.hasOwn(SECURITY_HEADER_NAMES, key);
|
|
7
5
|
const serializeSecurityHeaders = (headers) => Object.fromEntries(Object.keys(headers).flatMap((key) => {
|
|
8
6
|
if (!checkIsSecurityHeaderKey(key)) return [];
|
|
9
7
|
const value = headers[key];
|
|
10
8
|
return value === false || value === void 0 ? [] : [[SECURITY_HEADER_NAMES[key], serializeValue(key, value)]];
|
|
11
9
|
}));
|
|
12
|
-
const serializeValue = (key, value) => {
|
|
13
|
-
if (key === "contentSecurityPolicy") return serializeContentSecurityPolicy(value);
|
|
14
|
-
if (key === "permissionsPolicy") return serializePermissionsPolicy(value);
|
|
15
|
-
if (key === "strictTransportSecurity") return serializeStrictTransportSecurity(value);
|
|
16
|
-
return String(value);
|
|
17
|
-
};
|
|
18
10
|
export { serializeSecurityHeaders };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const serializeValue: (key: string, value: unknown) => string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { serializeContentSecurityPolicy } from "./serializeContentSecurityPolicy.js";
|
|
2
|
+
import { serializePermissionsPolicy } from "./serializePermissionsPolicy.js";
|
|
3
|
+
import { serializeStrictTransportSecurity } from "./serializeStrictTransportSecurity.js";
|
|
4
|
+
// src/runtime/services/serializeValue.ts
|
|
5
|
+
const serializeValue = (key, value) => {
|
|
6
|
+
if (key === "contentSecurityPolicy") return serializeContentSecurityPolicy(value);
|
|
7
|
+
if (key === "permissionsPolicy") return serializePermissionsPolicy(value);
|
|
8
|
+
if (key === "strictTransportSecurity") return serializeStrictTransportSecurity(value);
|
|
9
|
+
return String(value);
|
|
10
|
+
};
|
|
11
|
+
export { serializeValue };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const unquote: (value: string) => string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NONCE_PLACEHOLDER } from "../constants.js";
|
|
2
|
+
import { checkIsStaticNonce } from "./checkIsStaticNonce.js";
|
|
2
3
|
// src/runtime/services/updateContentSecurityPolicy.ts
|
|
3
4
|
const SCRIPT_DIRECTIVES = /* @__PURE__ */ new Set(["script-src", "script-src-elem"]);
|
|
4
5
|
const STYLE_DIRECTIVES = /* @__PURE__ */ new Set(["style-src", "style-src-elem"]);
|
|
@@ -10,5 +11,4 @@ const updateContentSecurityPolicy = (policy, { hashes, nonce }) => Object.fromEn
|
|
|
10
11
|
if (STYLE_DIRECTIVES.has(directive)) sources.push(...hashes.style);
|
|
11
12
|
return [directive, sources];
|
|
12
13
|
}));
|
|
13
|
-
const checkIsStaticNonce = (source) => source.startsWith("'nonce-") && source !== "'nonce-{{nonce}}'";
|
|
14
14
|
export { updateContentSecurityPolicy };
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
// src/schemas/requestSizeLimiterOptionsSchema.ts
|
|
3
|
-
const requestSizeLimiterOptionsSchema = z.strictObject({
|
|
4
|
-
maxRequestSizeInBytes: z.int("must be a whole number of bytes").positive("must be more than zero bytes"),
|
|
5
|
-
maxUploadFileRequestInBytes: z.int("must be a whole number of bytes").positive("must be more than zero bytes")
|
|
6
|
-
});
|
|
3
|
+
const requestSizeLimiterOptionsSchema = z.strictObject({ maxRequestSizeInBytes: z.int("must be a whole number of bytes").positive("must be more than zero bytes") });
|
|
7
4
|
export { requestSizeLimiterOptionsSchema };
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
// src/schemas/ssgOptionsSchema.ts
|
|
3
3
|
const ssgOptionsSchema = z.strictObject({
|
|
4
|
-
exportToPresets: z.boolean(),
|
|
5
4
|
hashScripts: z.boolean(),
|
|
6
5
|
hashStyles: z.boolean(),
|
|
7
|
-
meta: z.boolean()
|
|
8
|
-
nitroHeaders: z.boolean()
|
|
6
|
+
meta: z.boolean()
|
|
9
7
|
});
|
|
10
8
|
export { ssgOptionsSchema };
|
|
@@ -6,22 +6,17 @@ import { join, posix, relative, sep } from "node:path";
|
|
|
6
6
|
// src/services/computeSubresourceIntegrityHashes.ts
|
|
7
7
|
const computeSubresourceIntegrityHashes = async (nitro) => {
|
|
8
8
|
const { baseURL: appBaseUrl, cdnURL: appCdnUrl } = nitro.options.runtimeConfig.app;
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
filePath,
|
|
18
|
-
fullPath: posix.join(baseURL, ...relative(directory, filePath).split(sep))
|
|
19
|
-
};
|
|
20
|
-
});
|
|
21
|
-
}));
|
|
22
|
-
const hashEntries = await Promise.all(filesByPublicAsset.flat().map(async ({ filePath, fullPath }) => {
|
|
9
|
+
const { publicDir } = nitro.options.output;
|
|
10
|
+
if (!existsSync(publicDir)) return {};
|
|
11
|
+
const entries = await readdir(publicDir, {
|
|
12
|
+
recursive: true,
|
|
13
|
+
withFileTypes: true
|
|
14
|
+
});
|
|
15
|
+
const hashEntries = await Promise.all(entries.filter((entry) => entry.isFile()).map(async (entry) => {
|
|
16
|
+
const filePath = join(entry.parentPath, entry.name);
|
|
23
17
|
const hash = await generateHash(await readFile(filePath), SRI_HASH_ALGORITHM);
|
|
24
|
-
|
|
18
|
+
const fullPath = posix.join(...relative(publicDir, filePath).split(sep));
|
|
19
|
+
return [appCdnUrl ? new URL(fullPath, appCdnUrl.endsWith("/") ? appCdnUrl : `${appCdnUrl}/`).href : posix.join("/", appBaseUrl, fullPath), hash];
|
|
25
20
|
}));
|
|
26
21
|
return Object.fromEntries(hashEntries);
|
|
27
22
|
};
|
|
@@ -1,15 +1,5 @@
|
|
|
1
|
+
import { formatLeafIssue } from "./formatLeafIssue.js";
|
|
2
|
+
import { getLeafIssues } from "./getLeafIssues.js";
|
|
1
3
|
// src/services/formatZodIssues.ts
|
|
2
|
-
const getDeepestPathLength = (leafIssues) => Math.max(-1, ...leafIssues.map(({ path }) => path.length));
|
|
3
|
-
const getLeafIssues = (issue, parentPath) => {
|
|
4
|
-
const path = [...parentPath, ...issue.path];
|
|
5
|
-
if (issue.code !== "invalid_union") return [{
|
|
6
|
-
issue,
|
|
7
|
-
path
|
|
8
|
-
}];
|
|
9
|
-
return issue.errors.map((branch) => branch.flatMap((branchIssue) => getLeafIssues(branchIssue, path))).reduce((deepest, branch) => getDeepestPathLength(branch) > getDeepestPathLength(deepest) ? branch : deepest);
|
|
10
|
-
};
|
|
11
|
-
const formatLeafIssue = ({ issue, path }) => {
|
|
12
|
-
return `${path.map(String).join(".") || "(options)"}: ${issue.message}`;
|
|
13
|
-
};
|
|
14
4
|
const formatZodIssues = (issues) => issues.flatMap((issue) => getLeafIssues(issue, [])).map((leafIssue) => formatLeafIssue(leafIssue)).join("; ");
|
|
15
5
|
export { formatZodIssues };
|
|
@@ -23,17 +23,12 @@ const getDefaultModuleOptions = (isDevelopment, devServerUrl, isStrict) => ({
|
|
|
23
23
|
nonce: true,
|
|
24
24
|
rateLimiter: false,
|
|
25
25
|
removeLoggers: false,
|
|
26
|
-
requestSizeLimiter: {
|
|
27
|
-
maxRequestSizeInBytes: 2e6,
|
|
28
|
-
maxUploadFileRequestInBytes: 8e6
|
|
29
|
-
},
|
|
26
|
+
requestSizeLimiter: { maxRequestSizeInBytes: 2e6 },
|
|
30
27
|
sri: true,
|
|
31
28
|
ssg: {
|
|
32
|
-
exportToPresets: true,
|
|
33
29
|
hashScripts: true,
|
|
34
30
|
hashStyles: false,
|
|
35
|
-
meta: true
|
|
36
|
-
nitroHeaders: true
|
|
31
|
+
meta: true
|
|
37
32
|
},
|
|
38
33
|
strict: isStrict
|
|
39
34
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { NONCE_PLACEHOLDER } from "../runtime/constants.js";
|
|
2
1
|
import { CrossOriginEmbedderPolicy } from "../models/CrossOriginEmbedderPolicy.js";
|
|
3
2
|
import { XFrameOptions } from "../models/XFrameOptions.js";
|
|
3
|
+
import { NONCE_PLACEHOLDER } from "../runtime/constants.js";
|
|
4
4
|
// src/services/getDefaultSecurityHeaders.ts
|
|
5
5
|
const getDefaultSecurityHeaders = (isDevelopment, isStrict) => ({
|
|
6
6
|
contentSecurityPolicy: {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getDeepestPathLength } from "./getDeepestPathLength.js";
|
|
2
|
+
// src/services/getLeafIssues.ts
|
|
3
|
+
const getLeafIssues = (issue, parentPath) => {
|
|
4
|
+
const path = [...parentPath, ...issue.path];
|
|
5
|
+
if (issue.code !== "invalid_union") return [{
|
|
6
|
+
issue,
|
|
7
|
+
path
|
|
8
|
+
}];
|
|
9
|
+
return issue.errors.map((branch) => branch.flatMap((branchIssue) => getLeafIssues(branchIssue, path))).reduce((deepest, branch) => getDeepestPathLength(branch) > getDeepestPathLength(deepest) ? branch : deepest);
|
|
10
|
+
};
|
|
11
|
+
export { getLeafIssues };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { mergeSources } from "./mergeSources.js";
|
|
2
|
+
// src/services/mergeDirectives.ts
|
|
3
|
+
const mergeDirectives = (defaults, overrides) => {
|
|
4
|
+
if (overrides === false || defaults === false) return overrides ?? defaults;
|
|
5
|
+
if (overrides === void 0) return defaults;
|
|
6
|
+
const merged = { ...defaults };
|
|
7
|
+
for (const [directive, value] of Object.entries(overrides)) if (value === false) delete merged[directive];
|
|
8
|
+
else if (Array.isArray(value)) merged[directive] = mergeSources(merged[directive], value);
|
|
9
|
+
else merged[directive] = value;
|
|
10
|
+
return merged;
|
|
11
|
+
};
|
|
12
|
+
export { mergeDirectives };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// src/services/mergeFeatures.ts
|
|
2
|
+
const mergeFeatures = (defaults, overrides) => {
|
|
3
|
+
if (overrides === false || defaults === false) return overrides ?? defaults;
|
|
4
|
+
if (overrides === void 0) return defaults;
|
|
5
|
+
const merged = { ...defaults };
|
|
6
|
+
for (const [feature, sources] of Object.entries(overrides)) if (sources === false) delete merged[feature];
|
|
7
|
+
else merged[feature] = [.../* @__PURE__ */ new Set([...merged[feature] || [], ...sources || []])];
|
|
8
|
+
return merged;
|
|
9
|
+
};
|
|
10
|
+
export { mergeFeatures };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { mergeDirectives } from "./mergeDirectives.js";
|
|
2
|
+
import { mergeFeatures } from "./mergeFeatures.js";
|
|
3
|
+
// src/services/mergeHeaders.ts
|
|
4
|
+
const mergeHeaders = (defaults, overrides) => {
|
|
5
|
+
if (overrides === false || defaults === false) return overrides ?? defaults;
|
|
6
|
+
if (overrides === void 0) return defaults;
|
|
7
|
+
return {
|
|
8
|
+
...defaults,
|
|
9
|
+
...overrides,
|
|
10
|
+
contentSecurityPolicy: mergeDirectives(defaults.contentSecurityPolicy, overrides.contentSecurityPolicy),
|
|
11
|
+
permissionsPolicy: mergeFeatures(defaults.permissionsPolicy, overrides.permissionsPolicy)
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export { mergeHeaders };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mergeHeaders } from "./mergeHeaders.js";
|
|
1
2
|
// src/services/mergeModuleOptions.ts
|
|
2
3
|
const mergeModuleOptions = (defaults, overrides) => ({
|
|
3
4
|
...defaults,
|
|
@@ -8,31 +9,4 @@ const mergeModuleOptions = (defaults, overrides) => ({
|
|
|
8
9
|
...overrides.ssg
|
|
9
10
|
}
|
|
10
11
|
});
|
|
11
|
-
const mergeHeaders = (defaults, overrides) => {
|
|
12
|
-
if (overrides === false || defaults === false) return overrides ?? defaults;
|
|
13
|
-
if (overrides === void 0) return defaults;
|
|
14
|
-
return {
|
|
15
|
-
...defaults,
|
|
16
|
-
...overrides,
|
|
17
|
-
contentSecurityPolicy: mergeDirectives(defaults.contentSecurityPolicy, overrides.contentSecurityPolicy),
|
|
18
|
-
permissionsPolicy: mergeFeatures(defaults.permissionsPolicy, overrides.permissionsPolicy)
|
|
19
|
-
};
|
|
20
|
-
};
|
|
21
|
-
const mergeDirectives = (defaults, overrides) => {
|
|
22
|
-
if (overrides === false || defaults === false) return overrides ?? defaults;
|
|
23
|
-
if (overrides === void 0) return defaults;
|
|
24
|
-
const merged = { ...defaults };
|
|
25
|
-
for (const [directive, value] of Object.entries(overrides)) if (value === false) delete merged[directive];
|
|
26
|
-
else if (Array.isArray(value)) merged[directive] = [.../* @__PURE__ */ new Set([...Array.isArray(merged[directive]) ? merged[directive] : [], ...value])];
|
|
27
|
-
else merged[directive] = value;
|
|
28
|
-
return merged;
|
|
29
|
-
};
|
|
30
|
-
const mergeFeatures = (defaults, overrides) => {
|
|
31
|
-
if (overrides === false || defaults === false) return overrides ?? defaults;
|
|
32
|
-
if (overrides === void 0) return defaults;
|
|
33
|
-
const merged = { ...defaults };
|
|
34
|
-
for (const [feature, sources] of Object.entries(overrides)) if (sources === false) delete merged[feature];
|
|
35
|
-
else merged[feature] = [.../* @__PURE__ */ new Set([...merged[feature] || [], ...sources || []])];
|
|
36
|
-
return merged;
|
|
37
|
-
};
|
|
38
12
|
export { mergeModuleOptions };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NONE_SOURCE } from "./constants.js";
|
|
2
|
+
// src/services/mergeSources.ts
|
|
3
|
+
const mergeSources = (defaultSources, sources) => {
|
|
4
|
+
if (sources.length === 1 && sources[0] === "'none'") return sources;
|
|
5
|
+
const mergedSources = /* @__PURE__ */ new Set([...Array.isArray(defaultSources) ? defaultSources : [], ...sources]);
|
|
6
|
+
if (mergedSources.size > 1) mergedSources.delete(NONE_SOURCE);
|
|
7
|
+
return [...mergedSources];
|
|
8
|
+
};
|
|
9
|
+
export { mergeSources };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-security-module",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "Nuxt security module for Nitro 3 and h3 2 — security headers, a per-request nonce Content Security Policy, subresource integrity, CORS, allowed methods, basic auth and a request size limit.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Jimmy Chen",
|
|
@@ -19,19 +19,8 @@
|
|
|
19
19
|
},
|
|
20
20
|
"exports": {
|
|
21
21
|
".": "./dist/index.js",
|
|
22
|
-
"./runtime/constants": "./dist/runtime/constants.js",
|
|
23
|
-
"./runtime/nitro/plugins/hidePoweredBy": "./dist/runtime/nitro/plugins/hidePoweredBy.js",
|
|
24
|
-
"./runtime/nitro/plugins/nonce": "./dist/runtime/nitro/plugins/nonce.js",
|
|
25
22
|
"./runtime/nitro/plugins/routeRules": "./dist/runtime/nitro/plugins/routeRules.js",
|
|
26
|
-
"./runtime/nitro/plugins/securityHeaders": "./dist/runtime/nitro/plugins/securityHeaders.js",
|
|
27
|
-
"./runtime/nitro/plugins/ssgHashes": "./dist/runtime/nitro/plugins/ssgHashes.js",
|
|
28
|
-
"./runtime/nitro/plugins/subresourceIntegrity": "./dist/runtime/nitro/plugins/subresourceIntegrity.js",
|
|
29
|
-
"./runtime/server/middleware/allowedMethodsRestricter": "./dist/runtime/server/middleware/allowedMethodsRestricter.js",
|
|
30
|
-
"./runtime/server/middleware/basicAuth": "./dist/runtime/server/middleware/basicAuth.js",
|
|
31
|
-
"./runtime/server/middleware/corsHandler": "./dist/runtime/server/middleware/corsHandler.js",
|
|
32
|
-
"./runtime/server/middleware/csrf": "./dist/runtime/server/middleware/csrf.js",
|
|
33
23
|
"./runtime/server/middleware/rateLimiter": "./dist/runtime/server/middleware/rateLimiter.js",
|
|
34
|
-
"./runtime/server/middleware/requestSizeLimiter": "./dist/runtime/server/middleware/requestSizeLimiter.js",
|
|
35
24
|
"./package.json": "./package.json"
|
|
36
25
|
},
|
|
37
26
|
"scripts": {
|
|
@@ -40,7 +29,7 @@
|
|
|
40
29
|
"lint": "eslint .",
|
|
41
30
|
"lint:fix": "eslint --fix .",
|
|
42
31
|
"test": "vitest",
|
|
43
|
-
"typecheck": "
|
|
32
|
+
"typecheck": "tsrs --fast"
|
|
44
33
|
},
|
|
45
34
|
"dependencies": {
|
|
46
35
|
"@nuxt/kit": "npm:@nuxt/kit-nightly@5x",
|
|
@@ -50,16 +39,16 @@
|
|
|
50
39
|
"zod": "^4.6.7"
|
|
51
40
|
},
|
|
52
41
|
"devDependencies": {
|
|
53
|
-
"@esposter/configuration": "4.
|
|
54
|
-
"@esposter/shared-node": "4.
|
|
42
|
+
"@esposter/configuration": "4.5.0",
|
|
43
|
+
"@esposter/shared-node": "4.5.0",
|
|
55
44
|
"@nuxt/nitro-server": "npm:@nuxt/nitro-server-nightly@5x",
|
|
56
|
-
"@types/node": "^26.6.
|
|
57
|
-
"h3": "^2.0.
|
|
58
|
-
"tsdown": "^0.23.
|
|
45
|
+
"@types/node": "^26.6.6",
|
|
46
|
+
"h3": "^2.0.8",
|
|
47
|
+
"tsdown": "^0.23.5"
|
|
59
48
|
},
|
|
60
49
|
"peerDependencies": {
|
|
61
|
-
"nitro": "npm:nitro-nightly@
|
|
50
|
+
"nitro": "npm:nitro-nightly@latest",
|
|
62
51
|
"nuxt": "npm:nuxt-nightly@5x"
|
|
63
52
|
},
|
|
64
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "e30c001f3b4b15693e75fb53092d62a97ade4d2b"
|
|
65
54
|
}
|