Files
dsh-purge2/lib/zip-extract.js
叫我小杨同学 335ddee90f fix: 全仓审计加固与死代码清理,版本 1.1.87
- 回滚/备份:空 bak 改名 .bad 跳过;升级刷新 bak 补上单数 marker 补丁;统一原子写
- 补丁命中:optional 命中不符记软失败;#42 插入型替换加探针防重复插入
- Windows:进程枚举改用 | 分隔 + UTF-8,修复中文路径换包不杀旧进程;换包失败拉起原版
- 安全:asar 解包防越界;红队路径校验 realpath 防空 symlink;更新仅 https;报告回放去换行注入
- 提示词钩子:停用后 rewriteFn 返回原装配文本并清理 assemble guard;缓存签名纳入红队/策略/override
- 红队:listEngagements 默认不算统计;listVulns/报告支持 evidenceLimit
- 子代理名额占位即时生效并回退;--apply 有失败项退出码 1
- 清理:删除 lib/mnemon-settings.js 及 13 处死代码/未使用 import;移除 test 目录与 test 脚本
2026-11-01 21:47:03 +08:00

187 lines
6.5 KiB
JavaScript

import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import zlib from "node:zlib";
const HIDDEN = {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
windowsHide: true,
timeout: 60000,
};
/** 各平台解压顺序。node 不调用外部命令,macOS / Linux / Windows 都能用。 */
export function unzipTools(platform = process.platform) {
if (platform === "win32") return ["tar", "powershell", "node"];
return ["tar", "unzip", "python3", "bsdtar", "node"];
}
function safeZipName(name) {
const rel = String(name || "").replace(/\\/g, "/").replace(/^\.?\//, "").replace(/\/+$/, "");
if (!rel) return "";
if (rel.startsWith("/") || /^[A-Za-z]:/.test(rel)) throw new Error("Invalid zip path");
const parts = rel.split("/");
if (parts.some((part) => part === ".." || part === "")) throw new Error("Invalid zip path");
return parts.join("/");
}
function findEocd(buf) {
const min = Math.max(0, buf.length - 22 - 65535);
for (let i = buf.length - 22; i >= min; i -= 1) {
if (buf.readUInt32LE(i) === 0x06054b50) return i;
}
throw new Error("Not a zip file");
}
function readZipEntries(buf) {
if (!Buffer.isBuffer(buf) || buf.length < 22) throw new Error("Not a zip file");
const eocd = findEocd(buf);
const total = buf.readUInt16LE(eocd + 10);
let cursor = buf.readUInt32LE(eocd + 16);
const entries = [];
for (let n = 0; n < total; n += 1) {
if (cursor + 46 > buf.length || buf.readUInt32LE(cursor) !== 0x02014b50) {
throw new Error("Corrupt zip central directory");
}
const method = buf.readUInt16LE(cursor + 10);
const compSize = buf.readUInt32LE(cursor + 20);
const nameLen = buf.readUInt16LE(cursor + 28);
const extraLen = buf.readUInt16LE(cursor + 30);
const commentLen = buf.readUInt16LE(cursor + 32);
const localOffset = buf.readUInt32LE(cursor + 42);
const name = buf.toString("utf8", cursor + 46, cursor + 46 + nameLen);
entries.push({ name, method, compSize, localOffset });
cursor += 46 + nameLen + extraLen + commentLen;
}
return entries;
}
export function listZipEntries(buf) {
return readZipEntries(buf).map((entry) => entry.name.replace(/\\/g, "/"));
}
export function listZipFile(zipPath) {
return listZipEntries(fs.readFileSync(zipPath));
}
function localDataOffset(buf, localOffset) {
if (localOffset + 30 > buf.length || buf.readUInt32LE(localOffset) !== 0x04034b50) {
throw new Error("Corrupt zip local header");
}
const nameLen = buf.readUInt16LE(localOffset + 26);
const extraLen = buf.readUInt16LE(localOffset + 28);
return localOffset + 30 + nameLen + extraLen;
}
function isZipDirectoryEntry(name) {
return /[/\\]$/.test(String(name || ""));
}
export function extractZipBuffer(buf, dest) {
fs.mkdirSync(dest, { recursive: true });
const root = path.resolve(dest);
const prefix = root.endsWith(path.sep) ? root : root + path.sep;
for (const entry of readZipEntries(buf)) {
const rel = safeZipName(entry.name);
if (!rel) continue;
const out = path.resolve(root, rel);
if (out !== root && !out.startsWith(prefix)) throw new Error("Invalid zip path");
if (isZipDirectoryEntry(entry.name)) {
fs.mkdirSync(out, { recursive: true });
continue;
}
if (entry.compSize === 0xffffffff || entry.localOffset === 0xffffffff) {
throw new Error("zip64 is not supported");
}
if (entry.method !== 0 && entry.method !== 8) {
throw new Error(`Unsupported zip compression method ${entry.method}`);
}
const start = localDataOffset(buf, entry.localOffset);
const end = start + entry.compSize;
if (end > buf.length) throw new Error("Truncated zip data");
const compressed = buf.subarray(start, end);
const body = entry.method === 0 ? compressed : zlib.inflateRawSync(compressed);
fs.mkdirSync(path.dirname(out), { recursive: true });
fs.writeFileSync(out, body);
}
}
function attempt(file, args, extra = {}) {
try {
execFileSync(file, args, {
...HIDDEN,
stdio: ["ignore", "pipe", "pipe"],
...extra,
});
return "";
} catch (error) {
const detail = String((error && (error.stderr || error.stdout || error.message)) || "")
.trim()
.split(/\r?\n/)
.filter(Boolean)[0];
return detail ? `${file}: ${detail}` : file;
}
}
/** 按平台试外部工具,最后用 Node 解 zip。成功返回用到的方式。 */
export function extractZipArchive(zipPath, dest, tools = unzipTools()) {
fs.mkdirSync(dest, { recursive: true });
const notes = [];
/* 外部工具(tar/unzip/python/powershell)不看条目名:压缩包里写 ../ 或绝对路径
就能把文件落到 dest 之外(zip-slip)。先用 Node 读中央目录把每个条目过一遍
safeZipName,校验不过就不交给外部工具。 */
let entriesSafe = false;
try {
const buf = fs.readFileSync(zipPath);
const isZip = buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b;
if (isZip) {
for (const name of listZipEntries(buf)) safeZipName(name);
entriesSafe = true;
} else {
entriesSafe = true; /* 非 zip(如 tar.gz)交给外部工具 */
}
} catch (error) {
notes.push(`entries: ${error && error.message ? error.message : error}`);
}
for (const tool of tools) {
if (!entriesSafe) continue;
if (tool === "node") {
try {
extractZipBuffer(fs.readFileSync(zipPath), dest);
return "node";
} catch (error) {
notes.push(`node: ${error && error.message ? error.message : error}`);
break;
}
}
if (tool === "tar" || tool === "bsdtar") {
const note = attempt(tool, ["-xf", zipPath, "-C", dest]);
if (!note) return tool;
notes.push(note);
continue;
}
if (tool === "unzip") {
const note = attempt("unzip", ["-o", "-q", zipPath, "-d", dest]);
if (!note) return "unzip";
notes.push(note);
continue;
}
if (tool === "python3") {
const note = attempt("python3", ["-m", "zipfile", "-e", zipPath, dest]);
if (!note) return "python3";
notes.push(note);
continue;
}
if (tool === "powershell") {
const note = attempt(
"powershell",
["-NoProfile", "-Command", "Expand-Archive -LiteralPath $env:DSH_UPD_ZIP -DestinationPath $env:DSH_UPD_DIR -Force"],
{ env: { ...process.env, DSH_UPD_ZIP: zipPath, DSH_UPD_DIR: dest } },
);
if (!note) return "powershell";
notes.push(note);
}
}
throw new Error(`Extraction failed: ${notes.join(" | ") || "no extractor available"}`);
}