From 9e951e266dae187d5270c8a9ec79909ced8774e8 Mon Sep 17 00:00:00 2001 From: haelyra <49814733+haelyra@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:49:31 -0400 Subject: [PATCH] fix: normalize portable workspace path separators --- skills/agent-security-hardening/SKILL.md | 3 ++- tests/ci/agent-security-hardening-skill.test.js | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/skills/agent-security-hardening/SKILL.md b/skills/agent-security-hardening/SKILL.md index d392c8823..726cd3e2e 100644 --- a/skills/agent-security-hardening/SKILL.md +++ b/skills/agent-security-hardening/SKILL.md @@ -97,7 +97,8 @@ WINDOWS_RESERVED_NAME = re.compile( ) def workspace_path(workspace: Path, requested: str) -> Path: - relative = Path(requested) + # Treat both platform separators consistently before checking components. + relative = Path(requested.replace("\\", "/")) if relative.is_absolute() or ".." in relative.parts: raise ValueError("path must be workspace-relative") for part in relative.parts: diff --git a/tests/ci/agent-security-hardening-skill.test.js b/tests/ci/agent-security-hardening-skill.test.js index 5cc662046..b2c1b7fce 100644 --- a/tests/ci/agent-security-hardening-skill.test.js +++ b/tests/ci/agent-security-hardening-skill.test.js @@ -70,7 +70,7 @@ const python = findPython(); test("accepts ordinary nested and look-alike workspace paths", () => { if (!python) return { skipped: "python >= 3.9 not available" }; - const requests = ["reports/q1.txt", "console.txt", "CONFIG", "com10.txt", "nullable.md", ".env"]; + const requests = ["reports/q1.txt", "reports\\q1.txt", "console.txt", "CONFIG", "com10.txt", "nullable.md", ".env"]; const results = classify(python, requests); for (const requested of requests) { assert.strictEqual(results[requested], "accepted", `${requested} should be accepted`); @@ -79,7 +79,7 @@ test("accepts ordinary nested and look-alike workspace paths", () => { test("rejects traversal and absolute paths", () => { if (!python) return { skipped: "python >= 3.9 not available" }; - const requests = ["../outside.txt", "nested/../../outside.txt", path.resolve(os.tmpdir(), "abs.txt")]; + const requests = ["../outside.txt", "nested/../../outside.txt", "..\\outside.txt", "nested\\..\\..\\outside.txt", "\\\\server\\share\\file.txt", path.resolve(os.tmpdir(), "abs.txt")]; const results = classify(python, requests); for (const requested of requests) { assert.strictEqual(results[requested], "rejected", `${requested} should be rejected`); @@ -102,11 +102,14 @@ test("rejects Windows reserved device names, trailing dots or spaces, and stream "COM¹", "lpt³.txt", "sub/CON/file.txt", + "sub\\CON\\file.txt", "notes.", "notes ", "dir./file.txt", "report.txt:ads", "report.txt::$DATA", + "C:\\Windows\\system32", + "C:/Windows/system32", ]; const results = classify(python, requests); for (const requested of requests) {