@daloyjs/core 0.34.3 → 0.35.1
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 +151 -32
- package/dist/adapters/bun.d.ts +0 -1
- package/dist/adapters/bun.js +0 -1
- package/dist/adapters/cloudflare.d.ts +0 -1
- package/dist/adapters/cloudflare.js +0 -1
- package/dist/adapters/deno.d.ts +0 -1
- package/dist/adapters/deno.js +0 -1
- package/dist/adapters/fastly.d.ts +0 -1
- package/dist/adapters/fastly.js +0 -1
- package/dist/adapters/lambda.d.ts +0 -1
- package/dist/adapters/lambda.js +0 -1
- package/dist/adapters/node.d.ts +0 -1
- package/dist/adapters/node.js +175 -35
- package/dist/adapters/vercel.d.ts +0 -1
- package/dist/adapters/vercel.js +0 -1
- package/dist/app.d.ts +18 -1
- package/dist/app.js +396 -118
- package/dist/banner.d.ts +0 -1
- package/dist/banner.js +0 -1
- package/dist/cli.d.ts +32 -1
- package/dist/cli.js +62 -5
- package/dist/client.d.ts +0 -1
- package/dist/client.js +0 -1
- package/dist/combine.d.ts +0 -1
- package/dist/combine.js +0 -1
- package/dist/compression.d.ts +0 -1
- package/dist/compression.js +0 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/conn-info.d.ts +0 -1
- package/dist/conn-info.js +0 -1
- package/dist/contract.d.ts +0 -1
- package/dist/contract.js +0 -1
- package/dist/cookie.d.ts +0 -1
- package/dist/cookie.js +0 -1
- package/dist/dependency.d.ts +0 -1
- package/dist/dependency.js +0 -1
- package/dist/discriminator.d.ts +0 -1
- package/dist/discriminator.js +0 -1
- package/dist/docs.d.ts +0 -1
- package/dist/docs.js +0 -1
- package/dist/errors.d.ts +0 -1
- package/dist/errors.js +0 -1
- package/dist/etag.d.ts +0 -1
- package/dist/etag.js +0 -1
- package/dist/fetch-guard.d.ts +50 -6
- package/dist/fetch-guard.js +83 -18
- package/dist/hashing.d.ts +0 -1
- package/dist/hashing.js +0 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -3
- package/dist/ip-restriction.d.ts +0 -1
- package/dist/ip-restriction.js +0 -1
- package/dist/jwk.d.ts +0 -1
- package/dist/jwk.js +0 -1
- package/dist/jwt.d.ts +15 -1
- package/dist/jwt.js +56 -3
- package/dist/load-shedding.d.ts +0 -1
- package/dist/load-shedding.js +0 -1
- package/dist/logger.d.ts +0 -1
- package/dist/logger.js +0 -1
- package/dist/middleware.d.ts +78 -1
- package/dist/middleware.js +150 -18
- package/dist/multipart.d.ts +0 -1
- package/dist/multipart.js +0 -1
- package/dist/openapi.d.ts +0 -1
- package/dist/openapi.js +0 -1
- package/dist/rate-limit-redis.d.ts +0 -1
- package/dist/rate-limit-redis.js +0 -1
- package/dist/router.d.ts +0 -1
- package/dist/router.js +0 -1
- package/dist/safe-redirect.d.ts +91 -0
- package/dist/safe-redirect.js +168 -0
- package/dist/sbom.cdx.json +13 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/schema.d.ts +0 -1
- package/dist/schema.js +0 -1
- package/dist/security-schemes.d.ts +0 -1
- package/dist/security-schemes.js +0 -1
- package/dist/security.d.ts +134 -3
- package/dist/security.js +265 -6
- package/dist/session.d.ts +0 -1
- package/dist/session.js +0 -1
- package/dist/streaming.d.ts +0 -1
- package/dist/streaming.js +0 -1
- package/dist/subdomains.d.ts +0 -1
- package/dist/subdomains.js +0 -1
- package/dist/time-claims.d.ts +0 -1
- package/dist/time-claims.js +0 -1
- package/dist/tracing.d.ts +0 -1
- package/dist/tracing.js +0 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.js +0 -1
- package/dist/websocket.d.ts +0 -1
- package/dist/websocket.js +0 -1
- package/package.json +35 -15
- package/dist/adapters/bun.d.ts.map +0 -1
- package/dist/adapters/bun.js.map +0 -1
- package/dist/adapters/cloudflare.d.ts.map +0 -1
- package/dist/adapters/cloudflare.js.map +0 -1
- package/dist/adapters/deno.d.ts.map +0 -1
- package/dist/adapters/deno.js.map +0 -1
- package/dist/adapters/fastly.d.ts.map +0 -1
- package/dist/adapters/fastly.js.map +0 -1
- package/dist/adapters/lambda.d.ts.map +0 -1
- package/dist/adapters/lambda.js.map +0 -1
- package/dist/adapters/node.d.ts.map +0 -1
- package/dist/adapters/node.js.map +0 -1
- package/dist/adapters/vercel.d.ts.map +0 -1
- package/dist/adapters/vercel.js.map +0 -1
- package/dist/app.d.ts.map +0 -1
- package/dist/app.js.map +0 -1
- package/dist/banner.d.ts.map +0 -1
- package/dist/banner.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/combine.d.ts.map +0 -1
- package/dist/combine.js.map +0 -1
- package/dist/compression.d.ts.map +0 -1
- package/dist/compression.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/conn-info.d.ts.map +0 -1
- package/dist/conn-info.js.map +0 -1
- package/dist/contract.d.ts.map +0 -1
- package/dist/contract.js.map +0 -1
- package/dist/cookie.d.ts.map +0 -1
- package/dist/cookie.js.map +0 -1
- package/dist/dependency.d.ts.map +0 -1
- package/dist/dependency.js.map +0 -1
- package/dist/discriminator.d.ts.map +0 -1
- package/dist/discriminator.js.map +0 -1
- package/dist/docs.d.ts.map +0 -1
- package/dist/docs.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/etag.d.ts.map +0 -1
- package/dist/etag.js.map +0 -1
- package/dist/fetch-guard.d.ts.map +0 -1
- package/dist/fetch-guard.js.map +0 -1
- package/dist/hashing.d.ts.map +0 -1
- package/dist/hashing.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/ip-restriction.d.ts.map +0 -1
- package/dist/ip-restriction.js.map +0 -1
- package/dist/jwk.d.ts.map +0 -1
- package/dist/jwk.js.map +0 -1
- package/dist/jwt.d.ts.map +0 -1
- package/dist/jwt.js.map +0 -1
- package/dist/load-shedding.d.ts.map +0 -1
- package/dist/load-shedding.js.map +0 -1
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/middleware.d.ts.map +0 -1
- package/dist/middleware.js.map +0 -1
- package/dist/multipart.d.ts.map +0 -1
- package/dist/multipart.js.map +0 -1
- package/dist/openapi.d.ts.map +0 -1
- package/dist/openapi.js.map +0 -1
- package/dist/rate-limit-redis.d.ts.map +0 -1
- package/dist/rate-limit-redis.js.map +0 -1
- package/dist/router.d.ts.map +0 -1
- package/dist/router.js.map +0 -1
- package/dist/schema.d.ts.map +0 -1
- package/dist/schema.js.map +0 -1
- package/dist/security-schemes.d.ts.map +0 -1
- package/dist/security-schemes.js.map +0 -1
- package/dist/security.d.ts.map +0 -1
- package/dist/security.js.map +0 -1
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js.map +0 -1
- package/dist/streaming.d.ts.map +0 -1
- package/dist/streaming.js.map +0 -1
- package/dist/subdomains.d.ts.map +0 -1
- package/dist/subdomains.js.map +0 -1
- package/dist/time-claims.d.ts.map +0 -1
- package/dist/time-claims.js.map +0 -1
- package/dist/tracing.d.ts.map +0 -1
- package/dist/tracing.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/websocket.d.ts.map +0 -1
- package/dist/websocket.js.map +0 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-party `safeRedirect()` helper that refuses open-redirect inputs.
|
|
3
|
+
*
|
|
4
|
+
* Open redirects (Aikido "Top 10 app security problems" #10, OWASP
|
|
5
|
+
* "Unvalidated Redirects and Forwards") happen when an app blindly trusts
|
|
6
|
+
* a `?next=...` / `?returnTo=...` query parameter and emits a `Location`
|
|
7
|
+
* header pointing wherever the attacker wants. The fix the industry has
|
|
8
|
+
* settled on is the same one Daloy enforces here: validate every
|
|
9
|
+
* candidate URL against an explicit allowlist of internal paths and
|
|
10
|
+
* external origins **before** building the redirect response.
|
|
11
|
+
*
|
|
12
|
+
* Defaults are deliberately strict:
|
|
13
|
+
*
|
|
14
|
+
* - Same-origin paths must start with `/` and must not start with `//`
|
|
15
|
+
* or `/\` (which browsers interpret as protocol-relative URLs that
|
|
16
|
+
* escape your origin).
|
|
17
|
+
* - Backslashes, control characters, and `CR`/`LF` are rejected to
|
|
18
|
+
* stop response-splitting and homograph tricks.
|
|
19
|
+
* - Absolute URLs are only allowed when their `origin` exactly matches
|
|
20
|
+
* one of the entries in `allowedOrigins`.
|
|
21
|
+
* - `javascript:`, `data:`, `vbscript:`, and `file:` schemes are always
|
|
22
|
+
* refused, even if the caller wrote a bug into the allowlist.
|
|
23
|
+
* - The default status is `303 See Other`, which is the
|
|
24
|
+
* POST-redirect-GET-safe choice. Override with `status` when you
|
|
25
|
+
* genuinely need `301`/`302`/`307`/`308`.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { safeRedirect } from "@daloyjs/core";
|
|
30
|
+
*
|
|
31
|
+
* app.get("/login/callback", (ctx) => {
|
|
32
|
+
* const next = new URL(ctx.request.url).searchParams.get("next") ?? "/";
|
|
33
|
+
* return safeRedirect(next, {
|
|
34
|
+
* allowedPaths: ["/", "/dashboard", "/account"],
|
|
35
|
+
* allowedOrigins: ["https://app.example.com"],
|
|
36
|
+
* fallback: "/",
|
|
37
|
+
* });
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @since 0.35.1
|
|
42
|
+
*/
|
|
43
|
+
/** Thrown when {@link safeRedirect} refuses a candidate URL and no `fallback` is configured. */
|
|
44
|
+
export class OpenRedirectBlockedError extends Error {
|
|
45
|
+
reason;
|
|
46
|
+
target;
|
|
47
|
+
constructor(reason, target) {
|
|
48
|
+
super(`safeRedirect: refused redirect (${reason})`);
|
|
49
|
+
this.name = "OpenRedirectBlockedError";
|
|
50
|
+
this.reason = reason;
|
|
51
|
+
this.target = target;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const FORBIDDEN_SCHEMES = new Set([
|
|
55
|
+
"javascript:",
|
|
56
|
+
"data:",
|
|
57
|
+
"vbscript:",
|
|
58
|
+
"file:",
|
|
59
|
+
]);
|
|
60
|
+
const ALLOWED_REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
61
|
+
// Reject NUL, CR, LF, and other C0/C1 control characters; they enable
|
|
62
|
+
// response-splitting via the `Location` header.
|
|
63
|
+
// eslint-disable-next-line no-control-regex
|
|
64
|
+
const CONTROL_CHAR_RE = /[\u0000-\u001f\u007f-\u009f]/;
|
|
65
|
+
function buildResponse(location, status, headers) {
|
|
66
|
+
const merged = new Headers(headers);
|
|
67
|
+
merged.set("Location", location);
|
|
68
|
+
// Belt and suspenders: cache directives so browsers and shared caches
|
|
69
|
+
// never reuse a redirect that was scoped to a single request.
|
|
70
|
+
if (!merged.has("Cache-Control"))
|
|
71
|
+
merged.set("Cache-Control", "no-store");
|
|
72
|
+
return new Response(null, { status, headers: merged });
|
|
73
|
+
}
|
|
74
|
+
function classify(target, allowedPaths, allowedOrigins) {
|
|
75
|
+
if (typeof target !== "string" || target.length === 0) {
|
|
76
|
+
return { ok: false, reason: "empty-target" };
|
|
77
|
+
}
|
|
78
|
+
if (CONTROL_CHAR_RE.test(target)) {
|
|
79
|
+
return { ok: false, reason: "invalid-control-characters" };
|
|
80
|
+
}
|
|
81
|
+
// Protocol-relative (`//evil.com`) is the classic open-redirect bypass.
|
|
82
|
+
if (target.startsWith("//"))
|
|
83
|
+
return { ok: false, reason: "protocol-relative" };
|
|
84
|
+
// `/\evil.com` is interpreted by some browsers as protocol-relative too.
|
|
85
|
+
if (target.startsWith("/\\"))
|
|
86
|
+
return { ok: false, reason: "backslash-path" };
|
|
87
|
+
if (target.startsWith("/")) {
|
|
88
|
+
// Same-origin path. Backslashes anywhere in the path can confuse
|
|
89
|
+
// user agents and proxies — refuse them outright.
|
|
90
|
+
if (target.includes("\\"))
|
|
91
|
+
return { ok: false, reason: "backslash-path" };
|
|
92
|
+
if (allowedPaths.length === 0) {
|
|
93
|
+
return { ok: false, reason: "path-not-allowed" };
|
|
94
|
+
}
|
|
95
|
+
if (allowedPaths.includes("/*"))
|
|
96
|
+
return { ok: true, location: target };
|
|
97
|
+
// Match on pathname only; preserve any user-provided query/fragment.
|
|
98
|
+
const qIdx = target.search(/[?#]/);
|
|
99
|
+
const pathname = qIdx === -1 ? target : target.slice(0, qIdx);
|
|
100
|
+
if (!allowedPaths.includes(pathname)) {
|
|
101
|
+
return { ok: false, reason: "path-not-allowed" };
|
|
102
|
+
}
|
|
103
|
+
return { ok: true, location: target };
|
|
104
|
+
}
|
|
105
|
+
// Absolute URL path. Parse and compare origin.
|
|
106
|
+
let parsed;
|
|
107
|
+
try {
|
|
108
|
+
parsed = new URL(target);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return { ok: false, reason: "parse-failed" };
|
|
112
|
+
}
|
|
113
|
+
if (FORBIDDEN_SCHEMES.has(parsed.protocol.toLowerCase())) {
|
|
114
|
+
return { ok: false, reason: "scheme-not-allowed" };
|
|
115
|
+
}
|
|
116
|
+
if (!allowedOrigins.includes(parsed.origin)) {
|
|
117
|
+
return { ok: false, reason: "origin-not-allowed" };
|
|
118
|
+
}
|
|
119
|
+
return { ok: true, location: parsed.toString() };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Build a redirect `Response` after validating the target against an
|
|
123
|
+
* explicit allowlist. Throws {@link OpenRedirectBlockedError} when the
|
|
124
|
+
* candidate fails validation and no `fallback` is configured.
|
|
125
|
+
*
|
|
126
|
+
* @param target - User-supplied URL candidate (path or absolute URL).
|
|
127
|
+
* @param options - Allowlist + response configuration.
|
|
128
|
+
*
|
|
129
|
+
* @since 0.35.1
|
|
130
|
+
*/
|
|
131
|
+
export function safeRedirect(target, options = {}) {
|
|
132
|
+
const allowedPaths = options.allowedPaths ?? [];
|
|
133
|
+
const allowedOrigins = options.allowedOrigins ?? [];
|
|
134
|
+
const status = options.status ?? 303;
|
|
135
|
+
if (!ALLOWED_REDIRECT_STATUSES.has(status)) {
|
|
136
|
+
throw new TypeError(`safeRedirect: status ${String(status)} is not a redirect status (allowed: 301, 302, 303, 307, 308)`);
|
|
137
|
+
}
|
|
138
|
+
for (const p of allowedPaths) {
|
|
139
|
+
if (typeof p !== "string" || (p !== "/*" && !p.startsWith("/"))) {
|
|
140
|
+
throw new TypeError(`safeRedirect: allowedPaths entries must start with "/" (got ${JSON.stringify(p)})`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const o of allowedOrigins) {
|
|
144
|
+
if (typeof o !== "string" || o.length === 0) {
|
|
145
|
+
throw new TypeError("safeRedirect: allowedOrigins entries must be non-empty strings");
|
|
146
|
+
}
|
|
147
|
+
let parsed;
|
|
148
|
+
try {
|
|
149
|
+
parsed = new URL(o);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
throw new TypeError(`safeRedirect: allowedOrigins entry is not a valid URL: ${o}`);
|
|
153
|
+
}
|
|
154
|
+
if (parsed.origin !== o) {
|
|
155
|
+
throw new TypeError(`safeRedirect: allowedOrigins entry must be a bare origin (scheme + host [+ port]); got ${o}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const result = classify(target, allowedPaths, allowedOrigins);
|
|
159
|
+
if (result.ok)
|
|
160
|
+
return buildResponse(result.location, status, options.headers);
|
|
161
|
+
if (options.fallback !== undefined) {
|
|
162
|
+
if (!options.fallback.startsWith("/") || options.fallback.startsWith("//")) {
|
|
163
|
+
throw new TypeError(`safeRedirect: fallback must be a same-origin path starting with "/"; got ${options.fallback}`);
|
|
164
|
+
}
|
|
165
|
+
return buildResponse(options.fallback, status, options.headers);
|
|
166
|
+
}
|
|
167
|
+
throw new OpenRedirectBlockedError(result.reason, target);
|
|
168
|
+
}
|
package/dist/sbom.cdx.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.5",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:3d46c2b6-0b38-564b-9dd2-79098e99155b",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-05-
|
|
7
|
+
"timestamp": "2026-05-26T23:20:20.188Z",
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"vendor": "DaloyJS",
|
|
11
11
|
"name": "daloy-generate-sbom",
|
|
12
|
-
"version": "0.
|
|
12
|
+
"version": "0.35.1"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"authors": [
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
],
|
|
20
20
|
"component": {
|
|
21
21
|
"type": "library",
|
|
22
|
-
"bom-ref": "pkg:npm/@daloyjs/core@0.
|
|
22
|
+
"bom-ref": "pkg:npm/@daloyjs/core@0.35.1",
|
|
23
23
|
"name": "@daloyjs/core",
|
|
24
|
-
"version": "0.
|
|
24
|
+
"version": "0.35.1",
|
|
25
25
|
"description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
|
|
26
|
-
"purl": "pkg:npm/@daloyjs/core@0.
|
|
26
|
+
"purl": "pkg:npm/@daloyjs/core@0.35.1",
|
|
27
27
|
"licenses": [
|
|
28
28
|
{
|
|
29
29
|
"license": {
|
|
@@ -39,12 +39,16 @@
|
|
|
39
39
|
{
|
|
40
40
|
"type": "website",
|
|
41
41
|
"url": "https://daloyjs.dev"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"type": "distribution",
|
|
45
|
+
"url": "https://www.npmjs.com/package/@daloyjs/core"
|
|
42
46
|
}
|
|
43
47
|
],
|
|
44
48
|
"swid": {
|
|
45
|
-
"tagId": "swidtag--daloyjs-core-0.
|
|
49
|
+
"tagId": "swidtag--daloyjs-core-0.35.1",
|
|
46
50
|
"name": "@daloyjs/core",
|
|
47
|
-
"version": "0.
|
|
51
|
+
"version": "0.35.1",
|
|
48
52
|
"tagVersion": 0,
|
|
49
53
|
"patch": false
|
|
50
54
|
}
|
|
@@ -53,7 +57,7 @@
|
|
|
53
57
|
"components": [],
|
|
54
58
|
"dependencies": [
|
|
55
59
|
{
|
|
56
|
-
"ref": "pkg:npm/@daloyjs/core@0.
|
|
60
|
+
"ref": "pkg:npm/@daloyjs/core@0.35.1",
|
|
57
61
|
"dependsOn": []
|
|
58
62
|
}
|
|
59
63
|
]
|
package/dist/sbom.spdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"spdxVersion": "SPDX-2.3",
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
|
-
"name": "@daloyjs/core-0.
|
|
6
|
-
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-0.
|
|
5
|
+
"name": "@daloyjs/core-0.35.1",
|
|
6
|
+
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-0.35.1-3d46c2b6-0b38-564b-9dd2-79098e99155b",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-05-
|
|
8
|
+
"created": "2026-05-26T23:20:20.188Z",
|
|
9
9
|
"creators": [
|
|
10
10
|
"Tool: daloy-generate-sbom",
|
|
11
11
|
"Organization: DaloyJS"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
{
|
|
17
17
|
"SPDXID": "SPDXRef-Package--daloyjs-core",
|
|
18
18
|
"name": "@daloyjs/core",
|
|
19
|
-
"versionInfo": "0.
|
|
19
|
+
"versionInfo": "0.35.1",
|
|
20
20
|
"downloadLocation": "https://github.com/daloyjs/daloy",
|
|
21
21
|
"filesAnalyzed": false,
|
|
22
22
|
"licenseConcluded": "MIT",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
{
|
|
28
28
|
"referenceCategory": "PACKAGE-MANAGER",
|
|
29
29
|
"referenceType": "purl",
|
|
30
|
-
"referenceLocator": "pkg:npm/@daloyjs/core@0.
|
|
30
|
+
"referenceLocator": "pkg:npm/@daloyjs/core@0.35.1"
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
}
|
package/dist/schema.d.ts
CHANGED
package/dist/schema.js
CHANGED
package/dist/security-schemes.js
CHANGED
package/dist/security.d.ts
CHANGED
|
@@ -104,8 +104,8 @@ export declare function randomId(): string;
|
|
|
104
104
|
/**
|
|
105
105
|
* Header names that MUST appear at most once on a request per RFC 7230.
|
|
106
106
|
* Duplicate values for these headers are a classic HTTP-request-smuggling
|
|
107
|
-
* vector — front-end and back-end proxies can disagree on
|
|
108
|
-
*
|
|
107
|
+
* vector — front-end and back-end proxies can disagree on framing or
|
|
108
|
+
* routing, letting an attacker desync the connection.
|
|
109
109
|
*
|
|
110
110
|
* Exported for adapter authors that need the same set.
|
|
111
111
|
*
|
|
@@ -202,6 +202,14 @@ export declare function assertStrongSecret(secret: unknown, scope: string): void
|
|
|
202
202
|
* @since 0.15.0
|
|
203
203
|
*/
|
|
204
204
|
export type WebhookHmacAlgorithm = "sha256" | "sha384" | "sha512";
|
|
205
|
+
/**
|
|
206
|
+
* Default tolerance window (in seconds) for webhook timestamp verification.
|
|
207
|
+
* Matches Stripe / Standard Webhooks defaults — five minutes either side of
|
|
208
|
+
* the receiver's clock. Override per call via `toleranceSeconds`.
|
|
209
|
+
*
|
|
210
|
+
* @since 0.21.0
|
|
211
|
+
*/
|
|
212
|
+
export declare const WEBHOOK_DEFAULT_TOLERANCE_SECONDS = 300;
|
|
205
213
|
/**
|
|
206
214
|
* Verify an HMAC signature for a webhook payload in constant time.
|
|
207
215
|
*
|
|
@@ -243,6 +251,35 @@ export declare function verifyWebhookSignature(opts: {
|
|
|
243
251
|
signature: string | Uint8Array;
|
|
244
252
|
secret: string | Uint8Array;
|
|
245
253
|
algorithm?: WebhookHmacAlgorithm;
|
|
254
|
+
/**
|
|
255
|
+
* Optional event timestamp (Unix seconds; string or number). When
|
|
256
|
+
* supplied, the HMAC is computed over `"<timestamp>.<payload>"` instead
|
|
257
|
+
* of just `payload`, which binds the signature to a specific point in
|
|
258
|
+
* time and lets the verifier reject replays whose timestamp drifts
|
|
259
|
+
* outside `toleranceSeconds`. Matches the Stripe / Standard Webhooks
|
|
260
|
+
* convention. Pass the value extracted from the producer's signature
|
|
261
|
+
* header (e.g. the `t=` field of `Stripe-Signature`).
|
|
262
|
+
*
|
|
263
|
+
* @since 0.21.0
|
|
264
|
+
*/
|
|
265
|
+
timestamp?: string | number;
|
|
266
|
+
/**
|
|
267
|
+
* Maximum drift (in seconds) between `timestamp` and `now()` before the
|
|
268
|
+
* signature is rejected as a likely replay. Only consulted when
|
|
269
|
+
* `timestamp` is supplied. Defaults to
|
|
270
|
+
* {@link WEBHOOK_DEFAULT_TOLERANCE_SECONDS} (5 minutes).
|
|
271
|
+
*
|
|
272
|
+
* @since 0.21.0
|
|
273
|
+
*/
|
|
274
|
+
toleranceSeconds?: number;
|
|
275
|
+
/**
|
|
276
|
+
* Clock used for replay-window checks. Returns milliseconds since the
|
|
277
|
+
* Unix epoch. Defaults to {@link Date.now}; override in tests to make
|
|
278
|
+
* the clock deterministic.
|
|
279
|
+
*
|
|
280
|
+
* @since 0.21.0
|
|
281
|
+
*/
|
|
282
|
+
now?: () => number;
|
|
246
283
|
}): Promise<boolean>;
|
|
247
284
|
/**
|
|
248
285
|
* Compute an HMAC signature over `payload` and return it hex-encoded. The
|
|
@@ -255,5 +292,99 @@ export declare function signWebhookPayload(opts: {
|
|
|
255
292
|
payload: Uint8Array | string;
|
|
256
293
|
secret: string | Uint8Array;
|
|
257
294
|
algorithm?: WebhookHmacAlgorithm;
|
|
295
|
+
/**
|
|
296
|
+
* Optional event timestamp (Unix seconds; string or number). When
|
|
297
|
+
* supplied, the HMAC is computed over `"<timestamp>.<payload>"` so the
|
|
298
|
+
* receiver can use {@link verifyWebhookSignature} with the same
|
|
299
|
+
* `timestamp` and a `toleranceSeconds` window to defeat replay attacks.
|
|
300
|
+
*
|
|
301
|
+
* @since 0.21.0
|
|
302
|
+
*/
|
|
303
|
+
timestamp?: string | number;
|
|
258
304
|
}): Promise<string>;
|
|
259
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Return a single-segment, storage-safe basename derived from a
|
|
307
|
+
* (potentially attacker-controlled) filename — for example, the `name`
|
|
308
|
+
* field on a multipart `UploadedFile` before passing it to `fs.writeFile`
|
|
309
|
+
* or any other filesystem sink.
|
|
310
|
+
*
|
|
311
|
+
* The transformation:
|
|
312
|
+
*
|
|
313
|
+
* 1. Takes the basename only — anything before the last `/` or `\` is
|
|
314
|
+
* discarded (`../../etc/passwd` → `passwd`, `C:\\foo\\bar.txt` →
|
|
315
|
+
* `bar.txt`).
|
|
316
|
+
* 2. Strips NUL bytes and other control characters (NUL truncation is
|
|
317
|
+
* the classic `evil.png\0.exe` bypass).
|
|
318
|
+
* 3. Strips Windows-reserved characters (`<>:"|?*`) and replaces them
|
|
319
|
+
* with `_`.
|
|
320
|
+
* 4. Strips leading dots so the result cannot be `.`, `..`, or a hidden
|
|
321
|
+
* dotfile (`.htaccess` → `htaccess`).
|
|
322
|
+
* 5. Trims trailing dots and spaces (Windows silently strips these on
|
|
323
|
+
* `CreateFile`, so `file.txt ` and `file.txt` collide).
|
|
324
|
+
* 6. Refuses Windows-reserved names (`CON`, `PRN`, `AUX`, `NUL`,
|
|
325
|
+
* `COM1`–`COM9`, `LPT1`–`LPT9`) — case-insensitive, with or without
|
|
326
|
+
* extension.
|
|
327
|
+
* 7. Throws {@link BadRequestError} if the result is empty.
|
|
328
|
+
*
|
|
329
|
+
* Returns a basename only — never includes a directory. Callers are
|
|
330
|
+
* expected to combine it with a trusted base directory via
|
|
331
|
+
* `path.join(baseDir, sanitized)`. Combined with a final
|
|
332
|
+
* `path.resolve(baseDir, sanitized).startsWith(path.resolve(baseDir))`
|
|
333
|
+
* check this fully closes the Zip-Slip and arbitrary-write class of bug.
|
|
334
|
+
*
|
|
335
|
+
* @param name - The candidate filename (typically from
|
|
336
|
+
* `UploadedFile.name`, a `Content-Disposition` header, or any other
|
|
337
|
+
* untrusted source).
|
|
338
|
+
* @returns A sanitized basename safe to combine with a trusted directory.
|
|
339
|
+
* @throws {BadRequestError} When the input reduces to an empty string or
|
|
340
|
+
* matches a Windows-reserved device name.
|
|
341
|
+
* @since 0.35.1
|
|
342
|
+
*/
|
|
343
|
+
export declare function sanitizeFilename(name: string): string;
|
|
344
|
+
/**
|
|
345
|
+
* Validate that a candidate relative path is safe to combine with a
|
|
346
|
+
* trusted base directory — i.e. it cannot escape via `..` segments,
|
|
347
|
+
* absolute roots, drive letters, NUL truncation, or mixed
|
|
348
|
+
* POSIX/Windows separators.
|
|
349
|
+
*
|
|
350
|
+
* Throws {@link BadRequestError} on any of:
|
|
351
|
+
*
|
|
352
|
+
* - Empty input
|
|
353
|
+
* - NUL bytes (`evil\0../../etc/passwd` truncation)
|
|
354
|
+
* - Backslash characters (treated as Windows separators; rejecting
|
|
355
|
+
* them prevents POSIX callers being bypassed by `..\\..\\etc`)
|
|
356
|
+
* - POSIX absolute paths (`/etc/passwd`)
|
|
357
|
+
* - Windows drive letters or UNC roots (`C:\\foo`, `\\\\server\\share`)
|
|
358
|
+
* - Any segment equal to `..` after `/`-splitting
|
|
359
|
+
*
|
|
360
|
+
* Returns the normalized POSIX-style relative path on success. Designed
|
|
361
|
+
* for the Aikido / OWASP "Directory Traversal & File Exposure" class
|
|
362
|
+
* (`CVE-2023-26111` `node-static`, Zip Slip, `req.query.file` →
|
|
363
|
+
* `sendFile`).
|
|
364
|
+
*
|
|
365
|
+
* @param input - The candidate relative path.
|
|
366
|
+
* @returns The input unchanged when safe.
|
|
367
|
+
* @throws {BadRequestError} When the path could escape its base directory.
|
|
368
|
+
* @since 0.35.1
|
|
369
|
+
*/
|
|
370
|
+
export declare function assertSafeRelativePath(input: string): string;
|
|
371
|
+
/**
|
|
372
|
+
* Recursively scan a parsed JSON value (object / array / scalar) for any
|
|
373
|
+
* property key that starts with `$` — the MongoDB / NoSQL operator
|
|
374
|
+
* namespace (`$ne`, `$gt`, `$regex`, `$where`, …).
|
|
375
|
+
*
|
|
376
|
+
* Returns `true` on the first hit. Use before passing untrusted data
|
|
377
|
+
* into a query object that may be interpreted as an operator expression.
|
|
378
|
+
*
|
|
379
|
+
* @since 0.35.1
|
|
380
|
+
*/
|
|
381
|
+
export declare function hasMongoOperatorKeys(value: unknown): boolean;
|
|
382
|
+
/**
|
|
383
|
+
* Throw {@link BadRequestError} when {@link hasMongoOperatorKeys}
|
|
384
|
+
* returns `true`. Safe to call on the parsed request body before
|
|
385
|
+
* threading it into a NoSQL driver — closes the
|
|
386
|
+
* `{"password": {"$ne": null}}` authentication-bypass class of bug.
|
|
387
|
+
*
|
|
388
|
+
* @since 0.35.1
|
|
389
|
+
*/
|
|
390
|
+
export declare function assertNoMongoOperators(value: unknown): void;
|