fix(security): route temp workspaces through private OpenClaw temp root (#101224) (#101246)

* fix(security): route temp workspaces through private OpenClaw temp root (#101224)

* test(infra): skip POSIX mode proof on Windows

* chore: align release changelog before main sync

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
This commit is contained in:
Qiong
2026-07-07 13:25:35 +08:00
committed by GitHub
parent 531c7a5f4c
commit 7db7487a5d
3 changed files with 59 additions and 7 deletions

View File

@@ -1385,7 +1385,6 @@ export async function downloadClawHubPackageArchive(params: {
const target = await createTempDownloadTarget({
prefix: "openclaw-clawhub-clawpack",
fileName: npmTarballName,
tmpDir: os.tmpdir(),
});
await fs.writeFile(target.path, bytes);
return {
@@ -1428,7 +1427,6 @@ export async function downloadClawHubPackageArchive(params: {
const target = await createTempDownloadTarget({
prefix: "openclaw-clawhub-package",
fileName: `${params.name}.zip`,
tmpDir: os.tmpdir(),
});
await fs.writeFile(target.path, bytes);
return {
@@ -1475,7 +1473,6 @@ export async function downloadClawHubSkillArchive(params: {
const target = await createTempDownloadTarget({
prefix: "openclaw-clawhub-skill",
fileName: `${params.slug}.zip`,
tmpDir: os.tmpdir(),
});
await fs.writeFile(target.path, bytes);
return {
@@ -1518,7 +1515,6 @@ export async function downloadClawHubSkillArchiveUrl(params: {
const target = await createTempDownloadTarget({
prefix: "openclaw-clawhub-skill",
fileName: "skill.zip",
tmpDir: os.tmpdir(),
});
await fs.writeFile(target.path, bytes);
return {
@@ -1555,7 +1551,6 @@ export async function downloadClawHubGitHubSkillArchive(params: {
const target = await createTempDownloadTarget({
prefix: "openclaw-clawhub-github-skill",
fileName: `${params.commit}.zip`,
tmpDir: os.tmpdir(),
});
await fs.writeFile(target.path, bytes);
return {

View File

@@ -0,0 +1,57 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
const resolvePreferredOpenClawTmpDirMock = vi.hoisted(() => vi.fn());
vi.mock("./tmp-openclaw-dir.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./tmp-openclaw-dir.js")>();
return {
...actual,
resolvePreferredOpenClawTmpDir: resolvePreferredOpenClawTmpDirMock,
};
});
import { withTempDir } from "./install-source-utils.js";
describe("withTempDir private root", () => {
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
it.runIf(process.platform !== "win32")(
"preserves parent temp root permissions when using private OpenClaw temp root",
async () => {
const mockParentRoot = tempDirs.make("openclaw-chmod-test-");
const mockOpenClawDir = path.join(mockParentRoot, "openclaw");
await fs.mkdir(mockOpenClawDir, { recursive: true });
await fs.chmod(mockParentRoot, 0o1777);
const canonicalOpenClawDir = await fs.realpath(mockOpenClawDir);
resolvePreferredOpenClawTmpDirMock.mockReturnValue(mockOpenClawDir);
let observedDir = "";
const value = await withTempDir("openclaw-test-", async (tmpDir) => {
observedDir = tmpDir;
expect(path.dirname(tmpDir)).toBe(canonicalOpenClawDir);
await fs.writeFile(path.join(tmpDir, "marker.txt"), "ok");
return "done";
});
expect(value).toBe("done");
await expect(
fs.stat(observedDir).then(
() => true,
() => false,
),
).resolves.toBe(false);
const privateRootStat = await fs.stat(mockOpenClawDir);
expect(privateRootStat.mode & 0o7777).toBe(0o700);
const parentStat = await fs.stat(mockParentRoot);
expect(parentStat.mode & 0o7777).toBe(0o1777);
},
);
});

View File

@@ -1,6 +1,5 @@
// Resolves and packages install sources for plugin installs.
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
@@ -11,6 +10,7 @@ import { resolveArchiveKind } from "./archive.js";
import { pathExists } from "./fs-safe.js";
import { applyNpmFreshnessBypassEnv, type NpmProjectInstallEnvOptions } from "./npm-install-env.js";
import { withTempWorkspace } from "./private-temp-workspace.js";
import { resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir.js";
/** Metadata npm reports when resolving a registry spec or packed archive. */
export type NpmSpecResolution = {
@@ -141,7 +141,7 @@ export async function withTempDir<T>(
fn: (tmpDir: string) => Promise<T>,
options?: { rootDir?: string },
): Promise<T> {
const rootDir = options?.rootDir ?? os.tmpdir();
const rootDir = options?.rootDir ?? resolvePreferredOpenClawTmpDir();
return await withTempWorkspace({ rootDir, prefix }, async (tmp) => fn(tmp.dir));
}