Cherry-pick of gsd-build/gsd-2 65ca5aa2e — applies the security hardening hunks that conflicted minimally: - mcp-server/env-writer: validate writes against a strict allowlist - web/api/files: enforce path containment via web/lib/secure-path - vscode-extension: read binaryPath/autoStart only from trusted global/default scopes (resolveTrustedSfStartupConfig), avoiding workspace-controlled override (renamed Gsd → Sf for sf naming) - New regression tests: mcp-client-security, vscode-startup-security, web-files-symlink Skipped hunks (drifted): mcp-server/server.ts, mcp-client/index.ts, mcp-server/README.md. Co-Authored-By: Jeremy <jeremy@fluxlabs.net> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
48 lines
1.4 KiB
TypeScript
48 lines
1.4 KiB
TypeScript
import { existsSync, realpathSync } from "node:fs";
|
|
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
|
|
function isWithinRoot(rootRealPath: string, candidateRealPath: string): boolean {
|
|
const rel = relative(rootRealPath, candidateRealPath);
|
|
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
}
|
|
|
|
/**
|
|
* Validate and resolve a requested path against the given root directory.
|
|
* Returns the resolved absolute path or null if the path is invalid.
|
|
*/
|
|
export function resolveSecurePath(requestedPath: string, root: string, options: { mustExist?: boolean } = {}): string | null {
|
|
if (requestedPath.startsWith("/") || requestedPath.startsWith("\\")) {
|
|
return null;
|
|
}
|
|
if (requestedPath.includes("..")) {
|
|
return null;
|
|
}
|
|
|
|
let rootRealPath: string;
|
|
try {
|
|
rootRealPath = realpathSync.native(root);
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
const resolved = resolve(rootRealPath, requestedPath);
|
|
const rel = relative(rootRealPath, resolved);
|
|
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
if (existsSync(resolved)) {
|
|
const targetRealPath = realpathSync.native(resolved);
|
|
if (!isWithinRoot(rootRealPath, targetRealPath)) return null;
|
|
} else {
|
|
if (options.mustExist) return null;
|
|
const parentRealPath = realpathSync.native(dirname(resolved));
|
|
if (!isWithinRoot(rootRealPath, parentRealPath)) return null;
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
return resolved;
|
|
}
|