mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
fix: keep temporary filename overrides inside their directory (#154828)
* fix: keep temporary filename overrides inside their directory * chore: sync main after cron and ACPX fixture repairs * chore: merge main fixes into fs-safe filename cleanup Integrate the released fs-safe 0.18.0 dependency and the landed approval readiness and request-cleanup fixture repair. Preserve the existing four-file filename patch unchanged. Validation: all 73 focused SDK, ClawHub, iMessage, signed-runtime approval, and approval-fixture cases passed on the merged tree. Existing managed P2 review and static-check proof apply to the unchanged authored patch. * chore: merge fixture repairs into fs-safe naming cleanup Incorporate the landed session-store and survivor fixture repairs while preserving the four-file fs-safe naming change. Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
parent
16759c1f55
commit
2bf25ef263
4 changed files with 44 additions and 33 deletions
|
|
@ -390,7 +390,7 @@ Use `isLoopbackHost(host)` when a plugin must accept only the local machine. It
|
|||
| `plugin-sdk/tool-results` | Typed text and JSON agent tool result builders |
|
||||
| `plugin-sdk/tool-send` | Extract canonical send target fields from tool args |
|
||||
| `plugin-sdk/sandbox` | Private-local after July 2026; Sandbox backend types and SSH/OpenShell command helpers, including fail-fast exec command preflight and `resolveReadOnlyWorkspaceSkillMounts` for canonical read-only skill overlays in writable workspaces |
|
||||
| `plugin-sdk/temp-path` | Shared temp-download path helpers and private secure temp workspaces |
|
||||
| `plugin-sdk/temp-path` | Shared temp-download path helpers and private secure temp workspaces. `buildRandomTempFilePath` retains relative `tmpDir` paths and trims an optional `uuid`; blank values generate a UUID, while nonblank values must be safe filename components. `sanitizeTempFileName` adds a suffix to reserved Windows device names, preserving their extension. |
|
||||
| `plugin-sdk/logging-core` | Subsystem logger and redaction helpers |
|
||||
| `plugin-sdk/markdown-table-runtime` | Private-local after July 2026; Markdown table mode and conversion helpers |
|
||||
| `plugin-sdk/model-session-runtime` | Model/session override helpers such as `applyModelOverrideToSessionEntry` and `resolveAgentMaxConcurrent` |
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ function createArchiveResponse(bytes: Uint8Array, headers?: HeadersInit): Respon
|
|||
`sha512-${createHash("sha512").update(bytes).digest("base64")}`,
|
||||
);
|
||||
responseHeaders.set("X-ClawHub-Npm-Shasum", createHash("sha1").update(bytes).digest("hex"));
|
||||
responseHeaders.set("X-ClawHub-Npm-Tarball-Name", "registry-selected.tgz");
|
||||
responseHeaders.set("X-ClawHub-Npm-Tarball-Name", "NUL.tgz");
|
||||
responseHeaders.set("X-ClawHub-ClawPack-Spec-Version", "3");
|
||||
return new Response(new Uint8Array(bytes), { status: 200, headers: responseHeaders });
|
||||
}
|
||||
|
|
@ -167,7 +167,7 @@ const archiveDownloadCases: Array<{
|
|||
fetchImpl: async () => response,
|
||||
}),
|
||||
expectedResource: "ClawPack download for demo@1.2.3",
|
||||
expectedFileName: "registry-selected.tgz",
|
||||
expectedFileName: "NUL_.tgz",
|
||||
expectedArtifact: "clawpack",
|
||||
},
|
||||
{
|
||||
|
|
@ -374,7 +374,7 @@ describe("clawhub artifacts", () => {
|
|||
`sha512-${createHash("sha512").update(bytes).digest("base64")}`,
|
||||
);
|
||||
expect(archive.npmShasum).toBe(createHash("sha1").update(bytes).digest("hex"));
|
||||
expect(archive.npmTarballName).toBe("registry-selected.tgz");
|
||||
expect(archive.npmTarballName).toBe("NUL.tgz");
|
||||
}
|
||||
|
||||
await archive.cleanup();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
// Creates private temporary workspaces for downloads.
|
||||
import "./fs-safe-defaults.js";
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import {
|
||||
buildRandomTempFilePath as buildRandomTempFilePathBase,
|
||||
sanitizeTempFileName,
|
||||
} from "@openclaw/fs-safe/advanced";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { tempWorkspace } from "./private-temp-workspace.js";
|
||||
import { resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir.js";
|
||||
|
|
@ -9,6 +12,7 @@ import { resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir.js";
|
|||
const logger = createSubsystemLogger("infra:temp-download");
|
||||
|
||||
export { resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir.js";
|
||||
export { sanitizeTempFileName };
|
||||
|
||||
// Download targets expose both a default path and a name-safe file builder so
|
||||
// callers can keep all transient files inside the same workspace.
|
||||
|
|
@ -29,24 +33,6 @@ function sanitizeTempPrefix(prefix: string): string {
|
|||
return normalized || "tmp";
|
||||
}
|
||||
|
||||
function sanitizeTempExtension(extension?: string): string {
|
||||
if (!extension) {
|
||||
return "";
|
||||
}
|
||||
const normalized = extension.startsWith(".") ? extension : `.${extension}`;
|
||||
const suffix = normalized.match(/[a-zA-Z0-9._-]+$/)?.[0] ?? "";
|
||||
const token = suffix.replace(/^[._-]+/, "");
|
||||
return token ? `.${token}` : "";
|
||||
}
|
||||
|
||||
export function sanitizeTempFileName(fileName: string): string {
|
||||
const base = path.basename(fileName).replace(/[^a-zA-Z0-9._-]+/g, "-");
|
||||
const normalized = base.replace(/^-+|-+$/g, "");
|
||||
// "." and ".." pass the character class above but are rejected as workspace
|
||||
// leaf names, so returning them hands callers a throw instead of a safe name.
|
||||
return !normalized || normalized === "." || normalized === ".." ? "download.bin" : normalized;
|
||||
}
|
||||
|
||||
/** Build a stable temp path shape while keeping caller-controlled text filename-safe. */
|
||||
export function buildRandomTempFilePath(params: {
|
||||
prefix: string;
|
||||
|
|
@ -55,16 +41,13 @@ export function buildRandomTempFilePath(params: {
|
|||
now?: number;
|
||||
uuid?: string;
|
||||
}): string {
|
||||
const nowCandidate = params.now;
|
||||
const now =
|
||||
typeof nowCandidate === "number" && Number.isFinite(nowCandidate)
|
||||
? Math.trunc(nowCandidate)
|
||||
: Date.now();
|
||||
const uuid = params.uuid?.trim() || crypto.randomUUID();
|
||||
return path.join(
|
||||
resolveTempRoot(params.tmpDir),
|
||||
`${sanitizeTempPrefix(params.prefix)}-${now}-${uuid}${sanitizeTempExtension(params.extension)}`,
|
||||
);
|
||||
const rootDir = resolveTempRoot(params.tmpDir);
|
||||
const filePath = buildRandomTempFilePathBase({
|
||||
...params,
|
||||
rootDir,
|
||||
uuid: params.uuid?.trim() || undefined,
|
||||
});
|
||||
return path.join(rootDir, path.basename(filePath));
|
||||
}
|
||||
|
||||
export async function createTempDownloadTarget(params: {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,19 @@ describe("buildRandomTempFilePath", () => {
|
|||
expectedBasename: "line-media-123-abc.jpg",
|
||||
verifyInsideTmpRoot: false,
|
||||
},
|
||||
{
|
||||
name: "preserves relative roots, trimmed UUIDs, and compound extensions",
|
||||
input: {
|
||||
prefix: "archive",
|
||||
extension: "tar.gz",
|
||||
tmpDir: "relative/tmp",
|
||||
now: 123.9,
|
||||
uuid: " abc ",
|
||||
},
|
||||
expectedPath: path.join("relative/tmp", "archive-123-abc.tar.gz"),
|
||||
expectedBasename: "archive-123-abc.tar.gz",
|
||||
verifyInsideTmpRoot: false,
|
||||
},
|
||||
{
|
||||
name: "sanitizes prefix and extension to avoid path traversal segments",
|
||||
input: {
|
||||
|
|
@ -55,6 +68,21 @@ describe("buildRandomTempFilePath", () => {
|
|||
expectPathInsideTmpRoot(result);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["../../../escaped", "..\\..\\escaped", "id/name", "id\0name"])(
|
||||
"rejects path-control bytes in the UUID override %j",
|
||||
(uuid) => {
|
||||
expect(() =>
|
||||
buildRandomTempFilePath({ prefix: "download", tmpDir: "/tmp/owned", now: 1, uuid }),
|
||||
).toThrow(/safe path segment/);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["", " "])("generates a UUID for a blank override %j", (uuid) => {
|
||||
const result = buildRandomTempFilePath({ prefix: "media", now: 123, extension: ".jpg", uuid });
|
||||
expect(path.basename(result)).toMatch(/^media-123-[\da-f-]{36}\.jpg$/u);
|
||||
expectPathInsideTmpRoot(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe("withTempDownloadPath", () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue