From 17a2f55edb7f9e8a1eea6b9912be922a78655d12 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Sun, 22 Mar 2026 09:39:21 -0600 Subject: [PATCH] fix: use path.sep for cross-platform path traversal guards and test assertions Path traversal guards used hardcoded "/" separator which fails on Windows where resolve() produces backslash paths. Test assertions also used forward-slash path fragments. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/context-injector.ts | 4 ++-- src/resources/extensions/gsd/custom-verification.ts | 4 ++-- src/resources/extensions/gsd/tests/run-manager.test.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/resources/extensions/gsd/context-injector.ts b/src/resources/extensions/gsd/context-injector.ts index 8aa966f7c..00dcae2c3 100644 --- a/src/resources/extensions/gsd/context-injector.ts +++ b/src/resources/extensions/gsd/context-injector.ts @@ -14,7 +14,7 @@ */ import { readFileSync, existsSync } from "node:fs"; -import { join, resolve } from "node:path"; +import { join, resolve, sep } from "node:path"; import type { StepDefinition } from "./definition-loader.js"; import { readFrozenDefinition } from "./custom-workflow-engine.js"; @@ -65,7 +65,7 @@ export function injectContext( for (const relPath of refStep.produces) { const absPath = resolve(runDir, relPath); // Path traversal guard: ensure resolved path stays within runDir - if (!absPath.startsWith(resolve(runDir) + "/") && absPath !== resolve(runDir)) { + if (!absPath.startsWith(resolve(runDir) + sep) && absPath !== resolve(runDir)) { console.warn( `context-injector: artifact path "${relPath}" resolves outside runDir — skipping`, ); diff --git a/src/resources/extensions/gsd/custom-verification.ts b/src/resources/extensions/gsd/custom-verification.ts index 326a5595c..6c9a28b72 100644 --- a/src/resources/extensions/gsd/custom-verification.ts +++ b/src/resources/extensions/gsd/custom-verification.ts @@ -18,7 +18,7 @@ */ import { readFileSync, existsSync, statSync } from "node:fs"; -import { join, resolve } from "node:path"; +import { join, resolve, sep } from "node:path"; import { spawnSync } from "node:child_process"; import type { StepDefinition, VerifyPolicy } from "./definition-loader.js"; import { readFrozenDefinition } from "./custom-workflow-engine.js"; @@ -105,7 +105,7 @@ function handleContentHeuristic( for (const relPath of produces) { const absPath = resolve(runDir, relPath); // Path traversal guard - if (!absPath.startsWith(resolve(runDir) + "/") && absPath !== resolve(runDir)) { + if (!absPath.startsWith(resolve(runDir) + sep) && absPath !== resolve(runDir)) { return "pause"; } diff --git a/src/resources/extensions/gsd/tests/run-manager.test.ts b/src/resources/extensions/gsd/tests/run-manager.test.ts index d4eb02ddd..f03ab9baa 100644 --- a/src/resources/extensions/gsd/tests/run-manager.test.ts +++ b/src/resources/extensions/gsd/tests/run-manager.test.ts @@ -114,7 +114,7 @@ describe("createRun", () => { assert.ok(!existsSync(join(runDir, "PARAMS.json")), "PARAMS.json should not exist without overrides"); // Run directory path matches convention - assert.ok(runDir.includes(".gsd/workflow-runs/test-workflow/"), "path should follow convention"); + assert.ok(runDir.includes(join(".gsd", "workflow-runs", "test-workflow")), "path should follow convention"); }); it("writes PARAMS.json and substituted prompts when overrides provided", () => {