mirror of https://github.com/openclaw/openclaw.git
fix(windows): normalize namespaced path containment checks
This commit is contained in:
parent
dc6e4a5b13
commit
f7041fbee3
|
|
@ -1,12 +1,34 @@
|
|||
import { posix } from "node:path";
|
||||
import { resolvePathViaExistingAncestorSync } from "../../infra/boundary-path.js";
|
||||
|
||||
function stripWindowsNamespacePrefix(input: string): string {
|
||||
if (input.startsWith("\\\\?\\")) {
|
||||
const withoutPrefix = input.slice(4);
|
||||
if (withoutPrefix.toUpperCase().startsWith("UNC\\")) {
|
||||
return `\\\\${withoutPrefix.slice(4)}`;
|
||||
}
|
||||
return withoutPrefix;
|
||||
}
|
||||
if (input.startsWith("//?/")) {
|
||||
const withoutPrefix = input.slice(4);
|
||||
if (withoutPrefix.toUpperCase().startsWith("UNC/")) {
|
||||
return `//${withoutPrefix.slice(4)}`;
|
||||
}
|
||||
return withoutPrefix;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a POSIX host path: resolve `.`, `..`, collapse `//`, strip trailing `/`.
|
||||
*/
|
||||
export function normalizeSandboxHostPath(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
return posix.normalize(trimmed).replace(/\/+$/, "") || "/";
|
||||
const trimmed = stripWindowsNamespacePrefix(raw.trim());
|
||||
if (!trimmed) {
|
||||
return "/";
|
||||
}
|
||||
const normalized = posix.normalize(trimmed.replaceAll("\\", "/"));
|
||||
return normalized.replace(/\/+$/, "") || "/";
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
|
||||
|
|
@ -99,7 +100,7 @@ describe("onboardCommand", () => {
|
|||
|
||||
expect(mocks.handleReset).toHaveBeenCalledWith(
|
||||
"config+creds+sessions",
|
||||
"/tmp/openclaw-custom-workspace",
|
||||
path.resolve("/tmp/openclaw-custom-workspace"),
|
||||
runtime,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -630,7 +630,7 @@ describe("security: path traversal protection (CWE-22)", () => {
|
|||
"{ logging: { redactSensitive: 'tools' } }\n",
|
||||
"utf-8",
|
||||
);
|
||||
await fs.symlink(realRoot, linkRoot);
|
||||
await fs.symlink(realRoot, linkRoot, process.platform === "win32" ? "junction" : undefined);
|
||||
|
||||
const result = resolveConfigIncludes(
|
||||
{ $include: "./includes/extra.json5" },
|
||||
|
|
|
|||
|
|
@ -3,6 +3,17 @@ import path from "node:path";
|
|||
const NOT_FOUND_CODES = new Set(["ENOENT", "ENOTDIR"]);
|
||||
const SYMLINK_OPEN_CODES = new Set(["ELOOP", "EINVAL", "ENOTSUP"]);
|
||||
|
||||
function normalizeWindowsPathForComparison(input: string): string {
|
||||
let normalized = path.win32.normalize(input);
|
||||
if (normalized.startsWith("\\\\?\\")) {
|
||||
normalized = normalized.slice(4);
|
||||
if (normalized.toUpperCase().startsWith("UNC\\")) {
|
||||
normalized = `\\\\${normalized.slice(4)}`;
|
||||
}
|
||||
}
|
||||
return normalized.replaceAll("/", "\\").toLowerCase();
|
||||
}
|
||||
|
||||
export function isNodeError(value: unknown): value is NodeJS.ErrnoException {
|
||||
return Boolean(
|
||||
value && typeof value === "object" && "code" in (value as Record<string, unknown>),
|
||||
|
|
@ -26,7 +37,9 @@ export function isPathInside(root: string, target: string): boolean {
|
|||
const resolvedTarget = path.resolve(target);
|
||||
|
||||
if (process.platform === "win32") {
|
||||
const relative = path.win32.relative(resolvedRoot.toLowerCase(), resolvedTarget.toLowerCase());
|
||||
const rootForCompare = normalizeWindowsPathForComparison(resolvedRoot);
|
||||
const targetForCompare = normalizeWindowsPathForComparison(resolvedTarget);
|
||||
const relative = path.win32.relative(rootForCompare, targetForCompare);
|
||||
return relative === "" || (!relative.startsWith("..") && !path.win32.isAbsolute(relative));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ afterEach(() => {
|
|||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("findGatewayPidsOnPortSync", () => {
|
||||
describe.runIf(process.platform !== "win32")("findGatewayPidsOnPortSync", () => {
|
||||
it("parses lsof output and filters non-openclaw/current processes", () => {
|
||||
spawnSyncMock.mockReturnValue({
|
||||
error: undefined,
|
||||
|
|
@ -76,7 +76,7 @@ describe("findGatewayPidsOnPortSync", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("cleanStaleGatewayProcessesSync", () => {
|
||||
describe.runIf(process.platform !== "win32")("cleanStaleGatewayProcessesSync", () => {
|
||||
it("kills stale gateway pids discovered on the gateway port", () => {
|
||||
spawnSyncMock.mockReturnValue({
|
||||
error: undefined,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { isPathInside as isBoundaryPathInside } from "../infra/path-guards.js";
|
||||
|
||||
export function isPathInside(baseDir: string, targetPath: string): boolean {
|
||||
const rel = path.relative(baseDir, targetPath);
|
||||
if (!rel) {
|
||||
return true;
|
||||
}
|
||||
return !rel.startsWith("..") && !path.isAbsolute(rel);
|
||||
return isBoundaryPathInside(baseDir, targetPath);
|
||||
}
|
||||
|
||||
export function safeRealpathSync(targetPath: string, cache?: Map<string, string>): string | null {
|
||||
|
|
|
|||
Loading…
Reference in New Issue