veryfront 0.1.241 → 0.1.243
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/esm/cli/templates/manifest.js +77 -77
- package/esm/deno.js +1 -1
- package/esm/src/agent/conversation-root-run-context.d.ts.map +1 -1
- package/esm/src/agent/conversation-root-run-context.js +2 -0
- package/esm/src/agent/conversation-run-context.d.ts +2 -0
- package/esm/src/agent/conversation-run-context.d.ts.map +1 -1
- package/esm/src/agent/durable.d.ts +23 -0
- package/esm/src/agent/durable.d.ts.map +1 -1
- package/esm/src/agent/durable.js +39 -0
- package/esm/src/agent/index.d.ts +1 -1
- package/esm/src/agent/index.d.ts.map +1 -1
- package/esm/src/agent/index.js +1 -1
- package/esm/src/oauth/handlers/callback-handler.d.ts +2 -2
- package/esm/src/oauth/handlers/callback-handler.d.ts.map +1 -1
- package/esm/src/oauth/handlers/callback-handler.js +17 -5
- package/esm/src/oauth/handlers/init-handler.d.ts +24 -4
- package/esm/src/oauth/handlers/init-handler.d.ts.map +1 -1
- package/esm/src/oauth/handlers/init-handler.js +47 -10
- package/esm/src/oauth/providers/base.d.ts +9 -2
- package/esm/src/oauth/providers/base.d.ts.map +1 -1
- package/esm/src/oauth/providers/base.js +12 -5
- package/esm/src/oauth/token-store/index.d.ts +1 -1
- package/esm/src/oauth/token-store/index.d.ts.map +1 -1
- package/esm/src/oauth/token-store/memory.d.ts +21 -9
- package/esm/src/oauth/token-store/memory.d.ts.map +1 -1
- package/esm/src/oauth/token-store/memory.js +42 -28
- package/esm/src/oauth/types.d.ts +33 -7
- package/esm/src/oauth/types.d.ts.map +1 -1
- package/esm/src/platform/compat/framework-source-resolver.d.ts.map +1 -1
- package/esm/src/platform/compat/framework-source-resolver.js +34 -0
- package/esm/src/routing/api/module-loader/loader.d.ts +11 -0
- package/esm/src/routing/api/module-loader/loader.d.ts.map +1 -1
- package/esm/src/routing/api/module-loader/loader.js +18 -2
- package/esm/src/server/handlers/dev/dashboard/api.d.ts.map +1 -1
- package/esm/src/server/handlers/dev/dashboard/api.js +34 -13
- package/esm/src/server/handlers/dev/files/esbuild-plugins.d.ts.map +1 -1
- package/esm/src/server/handlers/dev/files/esbuild-plugins.js +45 -4
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/package.json +1 -1
- package/src/cli/templates/manifest.js +77 -77
- package/src/deno.js +1 -1
- package/src/src/agent/conversation-root-run-context.ts +2 -0
- package/src/src/agent/durable.ts +60 -0
- package/src/src/agent/index.ts +3 -0
- package/src/src/oauth/handlers/callback-handler.ts +25 -8
- package/src/src/oauth/handlers/init-handler.ts +83 -15
- package/src/src/oauth/providers/base.ts +12 -5
- package/src/src/oauth/token-store/index.ts +1 -1
- package/src/src/oauth/token-store/memory.ts +48 -35
- package/src/src/oauth/types.ts +34 -7
- package/src/src/platform/compat/framework-source-resolver.ts +32 -0
- package/src/src/routing/api/module-loader/loader.ts +18 -2
- package/src/src/server/handlers/dev/dashboard/api.ts +32 -10
- package/src/src/server/handlers/dev/files/esbuild-plugins.ts +54 -5
- package/src/src/utils/version-constant.ts +1 -1
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
const
|
|
1
|
+
/** State expiry window: reject any state older than this (10 minutes). */
|
|
2
|
+
const STATE_EXPIRY_MS = 10 * 60 * 1_000;
|
|
3
|
+
/**
|
|
4
|
+
* In-memory TokenStore keyed by `(serviceId, userId)`.
|
|
5
|
+
*
|
|
6
|
+
* Suitable for development and tests. For production use a persistent store
|
|
7
|
+
* (Redis, Postgres, ...) keyed the same way. Never share a single slot per
|
|
8
|
+
* service across users — see VULN-AUTH-2.
|
|
9
|
+
*/
|
|
3
10
|
export class MemoryTokenStore {
|
|
4
11
|
tokens = new Map();
|
|
5
12
|
states = new Map();
|
|
@@ -7,48 +14,55 @@ export class MemoryTokenStore {
|
|
|
7
14
|
constructor(projectId = "default") {
|
|
8
15
|
this.projectId = projectId;
|
|
9
16
|
}
|
|
10
|
-
scopedKey(serviceId) {
|
|
11
|
-
return `${this.projectId}:${serviceId}`;
|
|
17
|
+
scopedKey(serviceId, userId) {
|
|
18
|
+
return `${this.projectId}:${serviceId}:${userId}`;
|
|
12
19
|
}
|
|
13
|
-
|
|
14
|
-
return this.tokens.get(this.scopedKey(serviceId)) ?? null;
|
|
20
|
+
getTokens(serviceId, userId) {
|
|
21
|
+
return Promise.resolve(this.tokens.get(this.scopedKey(serviceId, userId)) ?? null);
|
|
15
22
|
}
|
|
16
|
-
|
|
17
|
-
this.tokens.set(this.scopedKey(serviceId), tokens);
|
|
23
|
+
setTokens(serviceId, userId, tokens) {
|
|
24
|
+
this.tokens.set(this.scopedKey(serviceId, userId), tokens);
|
|
25
|
+
return Promise.resolve();
|
|
18
26
|
}
|
|
19
|
-
|
|
20
|
-
this.tokens.delete(this.scopedKey(serviceId));
|
|
27
|
+
clearTokens(serviceId, userId) {
|
|
28
|
+
this.tokens.delete(this.scopedKey(serviceId, userId));
|
|
29
|
+
return Promise.resolve();
|
|
21
30
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (!storedState)
|
|
25
|
-
return null;
|
|
26
|
-
if (Date.now() - storedState.createdAt > STATE_EXPIRATION_MS) {
|
|
27
|
-
this.states.delete(state);
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
return storedState;
|
|
31
|
-
}
|
|
32
|
-
async setState(storedState) {
|
|
33
|
-
this.states.set(storedState.state, storedState);
|
|
31
|
+
setState(state, meta) {
|
|
32
|
+
this.states.set(state, meta);
|
|
34
33
|
this.cleanupExpiredStates();
|
|
34
|
+
return Promise.resolve();
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Atomically read and delete state (one-shot). Returns null for unknown or
|
|
38
|
+
* expired entries. Expired entries are removed on read.
|
|
39
|
+
*/
|
|
40
|
+
consumeState(state) {
|
|
41
|
+
const meta = this.states.get(state);
|
|
42
|
+
if (!meta)
|
|
43
|
+
return Promise.resolve(null);
|
|
37
44
|
this.states.delete(state);
|
|
45
|
+
if (Date.now() - meta.createdAt > STATE_EXPIRY_MS) {
|
|
46
|
+
return Promise.resolve(null);
|
|
47
|
+
}
|
|
48
|
+
return Promise.resolve(meta);
|
|
38
49
|
}
|
|
39
50
|
cleanupExpiredStates() {
|
|
40
51
|
const now = Date.now();
|
|
41
|
-
for (const [state,
|
|
42
|
-
if (now -
|
|
52
|
+
for (const [state, meta] of this.states) {
|
|
53
|
+
if (now - meta.createdAt > STATE_EXPIRY_MS) {
|
|
43
54
|
this.states.delete(state);
|
|
44
55
|
}
|
|
45
56
|
}
|
|
46
57
|
}
|
|
58
|
+
/** List connected slots as `${serviceId}:${userId}` strings (test/debug aid). */
|
|
47
59
|
getConnectedServices() {
|
|
48
|
-
|
|
60
|
+
const prefix = `${this.projectId}:`;
|
|
61
|
+
return [...this.tokens.keys()].map((key) => key.startsWith(prefix) ? key.slice(prefix.length) : key);
|
|
49
62
|
}
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
/** Whether a given user has usable tokens for a service. */
|
|
64
|
+
isConnected(serviceId, userId) {
|
|
65
|
+
const tokens = this.tokens.get(this.scopedKey(serviceId, userId));
|
|
52
66
|
if (!tokens)
|
|
53
67
|
return false;
|
|
54
68
|
const isExpired = tokens.expiresAt != null && Date.now() > tokens.expiresAt;
|
package/esm/src/oauth/types.d.ts
CHANGED
|
@@ -1,11 +1,37 @@
|
|
|
1
1
|
export type { AuthorizationUrlOptions, OAuthProviderConfig, OAuthServiceConfig, OAuthState, OAuthTokens, TokenExchangeOptions, TokenExchangeResult, } from "./schemas/index.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { OAuthTokens } from "./schemas/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Persisted OAuth state row. Created when init handler starts a flow and
|
|
5
|
+
* consumed exactly once by the callback handler.
|
|
6
|
+
*
|
|
7
|
+
* `userId` binds the flow to the authenticated user who initiated it so the
|
|
8
|
+
* resulting tokens are stored in that user's slot (not a shared one).
|
|
9
|
+
*/
|
|
10
|
+
export interface StoredOAuthState {
|
|
11
|
+
userId: string;
|
|
12
|
+
serviceId: string;
|
|
13
|
+
codeVerifier?: string;
|
|
14
|
+
redirectUri?: string;
|
|
15
|
+
scopes?: string[];
|
|
16
|
+
createdAt: number;
|
|
17
|
+
metadata?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* TokenStore is keyed by `(serviceId, userId)` — tokens are per-user.
|
|
21
|
+
*
|
|
22
|
+
* Using a single-slot-per-service store is a vulnerability: the last OAuth
|
|
23
|
+
* completion overwrites all others, so an attacker who starts and finishes
|
|
24
|
+
* an OAuth flow with their own account can cause server-side code to act
|
|
25
|
+
* on the attacker's account. Callers MUST pass `userId` from authenticated
|
|
26
|
+
* session context.
|
|
27
|
+
*/
|
|
3
28
|
export interface TokenStore {
|
|
4
|
-
getTokens(serviceId: string): Promise<OAuthTokens | null>;
|
|
5
|
-
setTokens(serviceId: string, tokens: OAuthTokens): Promise<void>;
|
|
6
|
-
clearTokens(serviceId: string): Promise<void>;
|
|
7
|
-
|
|
8
|
-
setState(state:
|
|
9
|
-
|
|
29
|
+
getTokens(serviceId: string, userId: string): Promise<OAuthTokens | null>;
|
|
30
|
+
setTokens(serviceId: string, userId: string, tokens: OAuthTokens): Promise<void>;
|
|
31
|
+
clearTokens(serviceId: string, userId: string): Promise<void>;
|
|
32
|
+
/** Persist a new OAuth state row for the initiating user. */
|
|
33
|
+
setState(state: string, meta: StoredOAuthState): Promise<void>;
|
|
34
|
+
/** Atomically read and delete state. Returns null if unknown/expired. */
|
|
35
|
+
consumeState(state: string): Promise<StoredOAuthState | null>;
|
|
10
36
|
}
|
|
11
37
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/src/oauth/types.ts"],"names":[],"mappings":"AACA,YAAY,EACV,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,EAClB,UAAU,EACV,WAAW,EACX,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/src/oauth/types.ts"],"names":[],"mappings":"AACA,YAAY,EACV,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,EAClB,UAAU,EACV,WAAW,EACX,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC1E,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjF,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,yEAAyE;IACzE,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;CAC/D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"framework-source-resolver.d.ts","sourceRoot":"","sources":["../../../../src/src/platform/compat/framework-source-resolver.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"framework-source-resolver.d.ts","sourceRoot":"","sources":["../../../../src/src/platform/compat/framework-source-resolver.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AA2BpD,eAAO,MAAM,cAAc,QAA4C,CAAC;AACxE,eAAO,MAAM,iBAAiB,QAA8B,CAAC;AAC7D,eAAO,MAAM,0BAA0B,QAAgD,CAAC;AAExF,eAAO,MAAM,mCAAmC,6HAatC,CAAC;AAEX,MAAM,WAAW,yBAAyB;IACxC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iCAAiC;IAChD,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,2CAA2C;IAC1D,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,wBAAgB,4BAA4B,CAAC,eAAe,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CAarF;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAG3D;AAuCD,wBAAsB,0BAA0B,CAC9C,sBAAsB,EAAE,MAAM,EAC9B,OAAO,GAAE,iCAAsC,GAC9C,OAAO,CAAC,2BAA2B,GAAG,IAAI,CAAC,CAwC7C;AAED,wBAAsB,oCAAoC,CACxD,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE,2CAAgD,GACxD,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA6CxB"}
|
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import { join } from "./path/index.js";
|
|
2
|
+
import { isWithinDirectory } from "../../security/path-validation.js";
|
|
2
3
|
import { createFileSystem } from "./fs.js";
|
|
3
4
|
import { getFrameworkRoot, getFrameworkRootFromMeta } from "./vfs-paths.js";
|
|
5
|
+
/**
|
|
6
|
+
* Reject candidate paths that contain traversal indicators — plain `..`,
|
|
7
|
+
* NUL, or any percent-encoded variant (including multiply-encoded forms such
|
|
8
|
+
* as `%252e` or `%25252e`). The public `/_vf_modules/...` route reaches this
|
|
9
|
+
* resolver, so a malicious basePath like
|
|
10
|
+
* `_veryfront/%2e%2e%2fsecret.ts` would otherwise be joined with the
|
|
11
|
+
* framework lookupDir and escape it.
|
|
12
|
+
*/
|
|
13
|
+
function hasDangerousSegments(candidate) {
|
|
14
|
+
if (candidate.includes("\0"))
|
|
15
|
+
return true;
|
|
16
|
+
// Plain-text traversal (post URL-decode).
|
|
17
|
+
if (/(^|[/\\])\.\.([/\\]|$)/.test(candidate))
|
|
18
|
+
return true;
|
|
19
|
+
// Any occurrence of a percent sign is treated as suspicious: this resolver
|
|
20
|
+
// is called with inputs taken from URL path segments which have already
|
|
21
|
+
// been decoded once upstream. A lingering `%` means the attacker
|
|
22
|
+
// double-encoded the input, or that decoding missed a sequence — either
|
|
23
|
+
// way, refuse to probe the filesystem. Framework source paths never
|
|
24
|
+
// legitimately contain `%`.
|
|
25
|
+
if (candidate.includes("%"))
|
|
26
|
+
return true;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
4
29
|
export const FRAMEWORK_ROOT = getFrameworkRootFromMeta(globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url);
|
|
5
30
|
export const FRAMEWORK_SRC_DIR = join(FRAMEWORK_ROOT, "src");
|
|
6
31
|
export const FRAMEWORK_EMBEDDED_SRC_DIR = join(FRAMEWORK_ROOT, "dist", "framework-src");
|
|
@@ -67,6 +92,11 @@ async function findExistingFrameworkCandidate(candidatePath, options = {}) {
|
|
|
67
92
|
return null;
|
|
68
93
|
}
|
|
69
94
|
export async function resolveFrameworkSourcePath(relativePathWithoutExt, options = {}) {
|
|
95
|
+
// VULN-FS-3: Reject any candidate containing traversal indicators
|
|
96
|
+
// (plain or percent-encoded) before joining with the framework lookup dir.
|
|
97
|
+
// The public /_vf_modules/... route reaches this function with user input.
|
|
98
|
+
if (hasDangerousSegments(relativePathWithoutExt))
|
|
99
|
+
return null;
|
|
70
100
|
const fs = options.fileSystem ?? createFileSystem();
|
|
71
101
|
const lookupDirs = getFrameworkSourceLookupDirs(options.extraLookupDirs);
|
|
72
102
|
const extensions = options.extensions ?? DEFAULT_FRAMEWORK_SOURCE_EXTENSIONS;
|
|
@@ -78,6 +108,10 @@ export async function resolveFrameworkSourcePath(relativePathWithoutExt, options
|
|
|
78
108
|
for (const candidate of candidates) {
|
|
79
109
|
for (const ext of extensions) {
|
|
80
110
|
const candidatePath = join(lookupDir, candidate + ext);
|
|
111
|
+
// Defence in depth: even if the candidate passed the textual gate
|
|
112
|
+
// above, confirm the joined path is physically within the lookup dir.
|
|
113
|
+
if (!isWithinDirectory(lookupDir, candidatePath))
|
|
114
|
+
continue;
|
|
81
115
|
try {
|
|
82
116
|
const stat = await fs.stat(candidatePath);
|
|
83
117
|
if (stat.isFile) {
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { APIRoute, LoadModuleOptions } from "./types.js";
|
|
2
2
|
import type { FileSystem } from "../../../platform/compat/fs.js";
|
|
3
3
|
export { toCjsDestructureBindings } from "./loader-helpers.js";
|
|
4
|
+
/**
|
|
5
|
+
* Generates a CJS module loader shim for compiled Deno binaries.
|
|
6
|
+
*
|
|
7
|
+
* In compiled binaries, `createRequire()` can resolve module paths and load
|
|
8
|
+
* built-in modules (fs, path, etc.), but cannot load CJS files from disk
|
|
9
|
+
* (loadMaybeCjs fails with "path not found"). This shim works around that
|
|
10
|
+
* limitation by using `Deno.readTextFileSync` to read CJS files and
|
|
11
|
+
* `new Function` to evaluate them in a proper CJS wrapper with require,
|
|
12
|
+
* exports, module, __filename, and __dirname bindings.
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateCompiledBinaryRequireShim(projectDir: string): string;
|
|
4
15
|
export declare function loadHandlerModule(options: LoadModuleOptions): Promise<APIRoute | null>;
|
|
5
16
|
export declare function getNodeExternalPackagesToResolve(userDeps: Map<string, string>): string[];
|
|
6
17
|
export declare function resolveNodePackageToFileUrl(projectDir: string, packageName: string, fs: FileSystem, pathToFileURL: typeof import("node:url").pathToFileURL): Promise<string | null>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../../../src/src/routing/api/module-loader/loader.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAI9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAkBjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../../../src/src/routing/api/module-loader/loader.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAI9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAkBjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAqD/D;;;;;;;;;GASG;AACH,wBAAgB,iCAAiC,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA+E5E;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAyBtF;AAuaD,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,EAAE,CAUxF;AAED,wBAAsB,2BAA2B,CAC/C,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,UAAU,EACd,aAAa,EAAE,cAAc,UAAU,EAAE,aAAa,GACrD,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoBxB;AAED,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,UAAU,GACb,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAW9C;AAED,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,UAAU,EACd,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,OAAO,CAAC,MAAM,CAAC,CA6EjB;AAED,wBAAgB,qCAAqC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqB1E;AAED,wBAAgB,0CAA0C,CACxD,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,MAAM,CAoDR;AAED,wBAAsB,+BAA+B,CACnD,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,UAAU,EACd,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,OAAO,CAAC,MAAM,CAAC,CA0BjB;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgBlE"}
|
|
@@ -73,7 +73,7 @@ async function readProjectDependencies(projectDir, fs) {
|
|
|
73
73
|
* `new Function` to evaluate them in a proper CJS wrapper with require,
|
|
74
74
|
* exports, module, __filename, and __dirname bindings.
|
|
75
75
|
*/
|
|
76
|
-
function generateCompiledBinaryRequireShim(projectDir) {
|
|
76
|
+
export function generateCompiledBinaryRequireShim(projectDir) {
|
|
77
77
|
const builtinSet = JSON.stringify(NODE_BUILTINS);
|
|
78
78
|
const safeProjectDir = JSON.stringify(projectDir + "/package.json");
|
|
79
79
|
const safeProjectRoot = JSON.stringify(pathHelper.resolve(projectDir));
|
|
@@ -83,6 +83,12 @@ import { dirname as __vf_dirname, resolve as __vf_resolve } from "node:path";
|
|
|
83
83
|
var __vf_builtinRequire = __vf_createRequire(${safeProjectDir});
|
|
84
84
|
var __vf_builtinSet = new Set(${builtinSet});
|
|
85
85
|
var __vf_projectRoot = ${safeProjectRoot};
|
|
86
|
+
// VULN-FS-5: Canonicalize the project root so containment checks using
|
|
87
|
+
// Deno.realPathSync(resolved) compare canonical-vs-canonical. Without this,
|
|
88
|
+
// when the project itself is opened via a symlink, the realpath'd resolved
|
|
89
|
+
// module path has a different prefix than the non-canonical projectRoot and
|
|
90
|
+
// legitimate dependencies would be rejected.
|
|
91
|
+
try { __vf_projectRoot = Deno.realPathSync(__vf_projectRoot); } catch (_) { /* expected: projectRoot may not exist at shim init in some environments */ }
|
|
86
92
|
var __vf_cache = Object.create(null);
|
|
87
93
|
function __vf_assertContained(resolved) {
|
|
88
94
|
var norm = __vf_resolve(resolved).replace(/\\\\/g, "/");
|
|
@@ -105,10 +111,20 @@ function __vf_loadCjs(id, parentDir) {
|
|
|
105
111
|
try { Deno.statSync(resolved + exts[i]); resolved += exts[i]; break; } catch (_) { /* expected: probing file extensions */ }
|
|
106
112
|
}
|
|
107
113
|
}
|
|
108
|
-
__vf_assertContained(resolved);
|
|
109
114
|
} else {
|
|
110
115
|
resolved = __vf_builtinRequire.resolve(id);
|
|
111
116
|
}
|
|
117
|
+
// VULN-FS-5: Always assert containment after resolution (both branches),
|
|
118
|
+
// then re-canonicalize via realPathSync to resist symlinked node_modules
|
|
119
|
+
// entries that could point outside the project root.
|
|
120
|
+
__vf_assertContained(resolved);
|
|
121
|
+
try {
|
|
122
|
+
var real = Deno.realPathSync(resolved);
|
|
123
|
+
__vf_assertContained(real);
|
|
124
|
+
resolved = real;
|
|
125
|
+
} catch (_) {
|
|
126
|
+
/* expected: realPathSync fails for non-existent paths — assertContained above already held */
|
|
127
|
+
}
|
|
112
128
|
if (resolved in __vf_cache) return __vf_cache[resolved];
|
|
113
129
|
var code = Deno.readTextFileSync(resolved);
|
|
114
130
|
if (resolved.endsWith(".json")) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../../../../src/src/server/handlers/dev/dashboard/api.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../../../../src/src/server/handlers/dev/dashboard/api.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAwDrD,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,QAAQ,GAAG,IAAI,CAgE5C"}
|
|
@@ -17,16 +17,29 @@ import { isRSCEnabled } from "../../../../utils/feature-flags.js";
|
|
|
17
17
|
import { getEnvironmentConfig } from "../../../../config/environment-config.js";
|
|
18
18
|
import { getErrorCollector } from "../../../../observability/error-collector.js";
|
|
19
19
|
import { getLogBuffer } from "../../../../observability/log-buffer.js";
|
|
20
|
+
import { validatePathSync } from "../../../../security/index.js";
|
|
20
21
|
import { ReloadNotifier } from "../../../reload-notifier.js";
|
|
21
22
|
const WORKFLOW_EXECUTION_TIMEOUT_MS = 30_000;
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Validate a relative path against the project directory.
|
|
25
|
+
*
|
|
26
|
+
* Uses `validatePathSync` in strict mode (rejects absolute paths, null bytes,
|
|
27
|
+
* `..` traversal, and any resolved path that escapes `baseDir`).
|
|
28
|
+
*
|
|
29
|
+
* Note: `searchParams.get()` already percent-decodes; no extra decoding needed
|
|
30
|
+
* (double-decoding would itself be a vulnerability).
|
|
31
|
+
*
|
|
32
|
+
* Returns the canonicalized absolute path on success, or `null` when invalid.
|
|
33
|
+
*/
|
|
34
|
+
function validateRelativePath(path, projectDir) {
|
|
35
|
+
const result = validatePathSync(path, {
|
|
36
|
+
baseDir: projectDir,
|
|
37
|
+
allowAbsolute: false,
|
|
38
|
+
level: "strict",
|
|
39
|
+
});
|
|
40
|
+
if (!result.valid || !result.canonicalPath)
|
|
41
|
+
return null;
|
|
42
|
+
return result.canonicalPath;
|
|
30
43
|
}
|
|
31
44
|
const JSON_HEADERS = {
|
|
32
45
|
"Content-Type": "application/json",
|
|
@@ -353,9 +366,16 @@ async function handleListFiles(req, ctx) {
|
|
|
353
366
|
if (!projectDir)
|
|
354
367
|
return errorResponse("No project directory configured", 500);
|
|
355
368
|
const relativePath = new URL(req.url).searchParams.get("path") ?? "";
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
369
|
+
let fullPath;
|
|
370
|
+
if (relativePath === "") {
|
|
371
|
+
fullPath = projectDir;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
const canonical = validateRelativePath(relativePath, projectDir);
|
|
375
|
+
if (canonical === null)
|
|
376
|
+
return errorResponse("Invalid path", 400);
|
|
377
|
+
fullPath = canonical;
|
|
378
|
+
}
|
|
359
379
|
try {
|
|
360
380
|
const files = [];
|
|
361
381
|
for await (const entry of adapter.fs.readDir(fullPath)) {
|
|
@@ -386,10 +406,11 @@ async function handleReadFileContent(req, ctx) {
|
|
|
386
406
|
const relativePath = new URL(req.url).searchParams.get("path") ?? "";
|
|
387
407
|
if (!relativePath)
|
|
388
408
|
return errorResponse("path parameter is required", 400);
|
|
389
|
-
|
|
409
|
+
const canonical = validateRelativePath(relativePath, projectDir);
|
|
410
|
+
if (canonical === null)
|
|
390
411
|
return errorResponse("Invalid path", 400);
|
|
391
412
|
try {
|
|
392
|
-
const content = await adapter.fs.readFile(
|
|
413
|
+
const content = await adapter.fs.readFile(canonical);
|
|
393
414
|
const extension = relativePath.split(".").pop() ?? "";
|
|
394
415
|
if (!TEXT_EXTENSIONS.has(extension.toLowerCase())) {
|
|
395
416
|
return jsonResponse({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"esbuild-plugins.d.ts","sourceRoot":"","sources":["../../../../../../src/src/server/handlers/dev/files/esbuild-plugins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA6B,MAAM,EAAe,MAAM,SAAS,CAAC;AAG9E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;
|
|
1
|
+
{"version":3,"file":"esbuild-plugins.d.ts","sourceRoot":"","sources":["../../../../../../src/src/server/handlers/dev/files/esbuild-plugins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA6B,MAAM,EAAe,MAAM,SAAS,CAAC;AAG9E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAQ5E,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,sCAAsC,CAAC;AAkB9C,gFAAgF;AAChF,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,MAAM,CAyF1F;AAKD,UAAU,yBAAyB;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AA+DD,kFAAkF;AAClF,wBAAgB,wBAAwB,CACtC,OAAO,GAAE,yBAAyB,GAAG,OAAe,GACnD,MAAM,CAqER;AAED,wBAAgB,wBAAwB,IAAI,MAAM,CAUjD"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NETWORK_ERROR } from "../../../../errors/index.js";
|
|
2
|
-
import { getDirectory, joinPath } from "../../../../utils/path-utils.js";
|
|
2
|
+
import { getDirectory, isWithinDirectory, joinPath, normalizePath, } from "../../../../utils/path-utils.js";
|
|
3
3
|
import { computeIntegrity, createLockfileManager, } from "../../../../utils/import-lockfile.js";
|
|
4
4
|
import { importMapOwnsSpecifier, mergeBrowserImportMapImports, } from "../../../../utils/import-map.js";
|
|
5
5
|
import { serverLogger } from "../../../../utils/logger/index.js";
|
|
@@ -20,16 +20,40 @@ export function createRelativeFsPlugin(projectDir, adapter) {
|
|
|
20
20
|
setup(build) {
|
|
21
21
|
const exts = [".tsx", ".ts", ".jsx", ".js", ".mjs"];
|
|
22
22
|
build.onResolve({ filter: /^(\.?\.?\/|\/)\/*/ }, async (args) => {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// VULN-FS-6: NUL bytes are never legitimate in module paths.
|
|
24
|
+
if (args.path.includes("\0")) {
|
|
25
|
+
return {
|
|
26
|
+
errors: [{ text: `Import path contains NUL byte: ${args.path}`, location: null }],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const basedir = args.resolveDir ||
|
|
30
|
+
(args.importer ? getDirectory(args.importer) : projectDir);
|
|
31
|
+
// normalizePath collapses `./` and `foo/../` segments produced by
|
|
32
|
+
// `joinPath` so downstream `adapter.fs.stat` lookups match the file
|
|
33
|
+
// system's canonical key. Still inside the containment check below.
|
|
34
|
+
const candidate = normalizePath(args.path.startsWith("/")
|
|
25
35
|
? joinPath(projectDir, args.path)
|
|
26
|
-
: joinPath(basedir, args.path);
|
|
36
|
+
: joinPath(basedir, args.path));
|
|
37
|
+
// VULN-FS-6: refuse anything that, after joining, escapes the project
|
|
38
|
+
// root. esbuild plugins fire per-import; an entry file with
|
|
39
|
+
// `import "../../../../etc/hostname"` would otherwise embed the file.
|
|
40
|
+
if (!isWithinDirectory(projectDir, candidate)) {
|
|
41
|
+
return {
|
|
42
|
+
errors: [{
|
|
43
|
+
text: `Import escapes project directory: ${args.path}`,
|
|
44
|
+
location: null,
|
|
45
|
+
}],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
27
48
|
const candidates = [candidate];
|
|
28
49
|
for (const ext of exts)
|
|
29
50
|
candidates.push(candidate + ext);
|
|
30
51
|
for (const ext of exts)
|
|
31
52
|
candidates.push(joinPath(candidate, `index${ext}`));
|
|
32
53
|
for (const f of candidates) {
|
|
54
|
+
// Defence in depth: each extension probe must also stay inside.
|
|
55
|
+
if (!isWithinDirectory(projectDir, f))
|
|
56
|
+
continue;
|
|
33
57
|
try {
|
|
34
58
|
const st = await adapter.fs.stat(f);
|
|
35
59
|
if (st.isFile)
|
|
@@ -42,6 +66,23 @@ export function createRelativeFsPlugin(projectDir, adapter) {
|
|
|
42
66
|
return undefined;
|
|
43
67
|
});
|
|
44
68
|
build.onLoad({ filter: /\.(tsx?|jsx?|mjs)$/ }, async (args) => {
|
|
69
|
+
// VULN-FS-6: belt-and-braces — reject any onLoad call whose path
|
|
70
|
+
// escapes the project root or carries a NUL byte. onResolve already
|
|
71
|
+
// gates this, but esbuild can call onLoad with paths produced by
|
|
72
|
+
// other plugins or namespaces.
|
|
73
|
+
if (args.path.includes("\0")) {
|
|
74
|
+
return {
|
|
75
|
+
errors: [{ text: `Load path contains NUL byte: ${args.path}`, location: null }],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (!isWithinDirectory(projectDir, args.path)) {
|
|
79
|
+
return {
|
|
80
|
+
errors: [{
|
|
81
|
+
text: `Load path escapes project directory: ${args.path}`,
|
|
82
|
+
location: null,
|
|
83
|
+
}],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
45
86
|
try {
|
|
46
87
|
const contents = await adapter.fs.readFile(args.path);
|
|
47
88
|
return { contents, loader: getLoaderForPath(args.path) };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.243";
|
|
2
2
|
//# sourceMappingURL=version-constant.d.ts.map
|