fix(security): harden archive extraction (#16203)

* fix(browser): confine upload paths for file chooser

* fix(browser): sanitize suggested download filenames

* chore(lint): avoid control regex in download sanitizer

* test(browser): cover absolute escape paths

* docs(browser): update upload example path

* refactor(browser): centralize upload path confinement

* fix(infra): harden tmp dir selection

* fix(security): harden archive extraction

* fix(infra): harden tar extraction filter
This commit is contained in:
Peter Steinberger
2026-02-14 14:42:08 +01:00
committed by GitHub
parent 9a134c8a10
commit 3aa94afcfd
19 changed files with 1179 additions and 100 deletions

View File

@@ -5,6 +5,7 @@ import os from "node:os";
import path from "node:path";
import { pipeline } from "node:stream/promises";
import type { RuntimeEnv } from "../runtime.js";
import { extractArchive } from "../infra/archive.js";
import { resolveBrewExecutable } from "../infra/brew.js";
import { runCommandWithTimeout } from "../process/exec.js";
import { CONFIG_DIR } from "../utils.js";
@@ -31,6 +32,15 @@ export type SignalInstallResult = {
error?: string;
};
/** @internal Exported for testing. */
export async function extractSignalCliArchive(
archivePath: string,
installRoot: string,
timeoutMs: number,
): Promise<void> {
await extractArchive({ archivePath, destDir: installRoot, timeoutMs });
}
/** @internal Exported for testing. */
export function looksLikeArchive(name: string): boolean {
return name.endsWith(".tar.gz") || name.endsWith(".tgz") || name.endsWith(".zip");
@@ -241,17 +251,18 @@ async function installSignalCliFromRelease(runtime: RuntimeEnv): Promise<SignalI
const installRoot = path.join(CONFIG_DIR, "tools", "signal-cli", version);
await fs.mkdir(installRoot, { recursive: true });
if (asset.name.endsWith(".zip")) {
await runCommandWithTimeout(["unzip", "-q", archivePath, "-d", installRoot], {
timeoutMs: 60_000,
});
} else if (asset.name.endsWith(".tar.gz") || asset.name.endsWith(".tgz")) {
await runCommandWithTimeout(["tar", "-xzf", archivePath, "-C", installRoot], {
timeoutMs: 60_000,
});
} else {
if (!looksLikeArchive(asset.name.toLowerCase())) {
return { ok: false, error: `Unsupported archive type: ${asset.name}` };
}
try {
await extractSignalCliArchive(archivePath, installRoot, 60_000);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return {
ok: false,
error: `Failed to extract ${asset.name}: ${message}`,
};
}
const cliPath = await findSignalCliBinary(installRoot);
if (!cliPath) {