From bfc9ecf32eb992c52d5044ae3fee6b8debcdecd9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Feb 2026 11:00:44 +0100 Subject: [PATCH] test: harden temp path guard detection (#23398) --- src/security/temp-path-guard.test.ts | 91 ++++++++++++++++++++++++---- 1 file changed, 78 insertions(+), 13 deletions(-) diff --git a/src/security/temp-path-guard.test.ts b/src/security/temp-path-guard.test.ts index acae79d42..8fa99feba 100644 --- a/src/security/temp-path-guard.test.ts +++ b/src/security/temp-path-guard.test.ts @@ -1,5 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; +import ts from "typescript"; import { describe, expect, it } from "vitest"; const RUNTIME_ROOTS = ["src", "extensions"]; @@ -17,19 +18,61 @@ function shouldSkip(relativePath: string): boolean { return SKIP_PATTERNS.some((pattern) => pattern.test(relativePath)); } -function hasDynamicTmpdirTemplateJoin(source: string): boolean { - const needle = "path.join(os.tmpdir(),"; - let cursor = source.indexOf(needle); - while (cursor !== -1) { - const window = source.slice(cursor, Math.min(source.length, cursor + 240)); - const closeIdx = window.indexOf(")"); - const expr = closeIdx === -1 ? window : window.slice(0, closeIdx + 1); - if (expr.includes("`") && expr.includes("${")) { - return true; +function isIdentifierNamed(node: ts.Node, name: string): node is ts.Identifier { + return ts.isIdentifier(node) && node.text === name; +} + +function isPathJoinCall(expr: ts.LeftHandSideExpression): boolean { + return ( + ts.isPropertyAccessExpression(expr) && + expr.name.text === "join" && + isIdentifierNamed(expr.expression, "path") + ); +} + +function isOsTmpdirCall(node: ts.Expression): boolean { + return ( + ts.isCallExpression(node) && + node.arguments.length === 0 && + ts.isPropertyAccessExpression(node.expression) && + node.expression.name.text === "tmpdir" && + isIdentifierNamed(node.expression.expression, "os") + ); +} + +function isDynamicTemplateSegment(node: ts.Expression): boolean { + return ts.isTemplateExpression(node); +} + +function hasDynamicTmpdirJoin(source: string, filePath = "fixture.ts"): boolean { + const sourceFile = ts.createSourceFile( + filePath, + source, + ts.ScriptTarget.Latest, + true, + filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS, + ); + let found = false; + + const visit = (node: ts.Node): void => { + if (found) { + return; } - cursor = source.indexOf(needle, cursor + needle.length); - } - return false; + if ( + ts.isCallExpression(node) && + isPathJoinCall(node.expression) && + node.arguments.length >= 2 && + isOsTmpdirCall(node.arguments[0]) && + node.arguments.slice(1).some((arg) => isDynamicTemplateSegment(arg)) + ) { + found = true; + return; + } + ts.forEachChild(node, visit); + }; + + visit(sourceFile); + return found; } async function listTsFiles(dir: string): Promise { @@ -61,6 +104,28 @@ describe("temp path guard", () => { expect(shouldSkip("src\\commands\\sessions.test-helpers.ts")).toBe(true); }); + it("detects dynamic and ignores static fixtures", () => { + const dynamicFixtures = [ + "const p = path.join(os.tmpdir(), `openclaw-${id}`);", + "const p = path.join(os.tmpdir(), 'safe', `${token}`);", + ]; + const staticFixtures = [ + "const p = path.join(os.tmpdir(), 'openclaw-fixed');", + "const p = path.join(os.tmpdir(), `openclaw-fixed`);", + "const p = path.join(os.tmpdir(), prefix + '-x');", + "const p = path.join(os.tmpdir(), segment);", + "const p = path.join('/tmp', `openclaw-${id}`);", + "// path.join(os.tmpdir(), `openclaw-${id}`)", + "const p = path.join(os.tmpdir());", + ]; + + for (const fixture of dynamicFixtures) { + expect(hasDynamicTmpdirJoin(fixture)).toBe(true); + } + for (const fixture of staticFixtures) { + expect(hasDynamicTmpdirJoin(fixture)).toBe(false); + } + }); it("blocks dynamic template path.join(os.tmpdir(), ...) in runtime source files", async () => { const repoRoot = process.cwd(); const offenders: string[] = []; @@ -74,7 +139,7 @@ describe("temp path guard", () => { continue; } const source = await fs.readFile(file, "utf-8"); - if (hasDynamicTmpdirTemplateJoin(source)) { + if (hasDynamicTmpdirJoin(source, relativePath)) { offenders.push(relativePath); } }