






















@@ -15,8 +15,10 @@ const FILE_FETCH_DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
1515const FILE_FETCH_HARD_MAX_BYTES = 16 * 1024 * 1024;
1616const DIR_FETCH_DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
1717const DIR_FETCH_HARD_MAX_BYTES = 16 * 1024 * 1024;
18+const DIR_FETCH_MAX_ENTRIES = 5000;
1819const DIR_FETCH_ARCHIVE_LIST_TIMEOUT_MS = 30_000;
1920const DIR_FETCH_ARCHIVE_LIST_MAX_OUTPUT_BYTES = 32 * 1024 * 1024;
21+const DIR_FETCH_ARCHIVE_LIST_STDERR_TAIL_CHARS = 4096;
20222123type FileTransferCommand = FileTransferNodeInvokeCommand;
2224@@ -26,6 +28,11 @@ function asRecord(value: unknown): Record<string, unknown> {
2628 : {};
2729}
283031+function appendBoundedTextTail(current: string, chunk: Buffer, maxChars: number): string {
32+const next = current + chunk.toString();
33+return next.length > maxChars ? next.slice(-maxChars) : next;
34+}
35+2936function readPath(params: Record<string, unknown>): string {
3037return typeof params.path === "string" ? params.path.trim() : "";
3138}
@@ -307,87 +314,115 @@ async function listDirFetchArchiveEntries(
307314>((resolve) => {
308315const tarBin = process.platform !== "win32" ? "/usr/bin/tar" : "tar";
309316const child = spawn(tarBin, ["-tzf", "-"], { stdio: ["pipe", "pipe", "pipe"] });
310-let stdout = "";
317+const entries: string[] = [];
318+let pending = "";
319+let outputBytes = 0;
311320let stderr = "";
312-let aborted = false;
313-const watchdog = setTimeout(() => {
314-aborted = true;
321+let settled = false;
322+const finish = (
323+result: { ok: true; entries: string[] } | { ok: false; code: string; reason: string },
324+): void => {
325+if (settled) {
326+return;
327+}
328+settled = true;
329+clearTimeout(watchdog);
330+resolve(result);
331+};
332+const stopChild = (): void => {
315333try {
316334child.kill("SIGKILL");
317335} catch {
318336/* gone */
319337}
320-resolve({
338+};
339+const appendLine = (line: string): boolean => {
340+if (settled) {
341+return false;
342+}
343+const entry = normalizeTarEntryPath(line);
344+if (entry !== null) {
345+entries.push(entry);
346+if (entries.length > DIR_FETCH_MAX_ENTRIES) {
347+stopChild();
348+finish({
349+ok: false,
350+code: "ARCHIVE_ENTRIES_TOO_MANY",
351+reason: `dir.fetch archive contains more than ${DIR_FETCH_MAX_ENTRIES} entries`,
352+});
353+return false;
354+}
355+}
356+return true;
357+};
358+const watchdog = setTimeout(() => {
359+stopChild();
360+finish({
321361ok: false,
322362code: "ARCHIVE_ENTRIES_UNREADABLE",
323363reason: "tar -tzf timed out",
324364});
325365}, DIR_FETCH_ARCHIVE_LIST_TIMEOUT_MS);
326366child.stdout.on("data", (chunk: Buffer) => {
327-stdout += chunk.toString();
328-if (stdout.length > DIR_FETCH_ARCHIVE_LIST_MAX_OUTPUT_BYTES) {
329-aborted = true;
330-clearTimeout(watchdog);
331-try {
332-child.kill("SIGKILL");
333-} catch {
334-/* gone */
335-}
336-resolve({
367+if (settled) {
368+return;
369+}
370+outputBytes += chunk.byteLength;
371+if (outputBytes > DIR_FETCH_ARCHIVE_LIST_MAX_OUTPUT_BYTES) {
372+stopChild();
373+finish({
337374ok: false,
338375code: "ARCHIVE_ENTRIES_UNREADABLE",
339376reason: "tar -tzf output too large",
340377});
378+return;
379+}
380+const lines = `${pending}${chunk.toString()}`.split("\n");
381+pending = lines.pop() ?? "";
382+for (const line of lines) {
383+if (!appendLine(line)) {
384+return;
385+}
341386}
342387});
343388child.stderr.on("data", (chunk: Buffer) => {
344-stderr += chunk.toString();
389+stderr = appendBoundedTextTail(stderr, chunk, DIR_FETCH_ARCHIVE_LIST_STDERR_TAIL_CHARS);
345390});
346391child.on("close", (code) => {
347-clearTimeout(watchdog);
348-if (aborted) {
392+if (settled) {
349393return;
350394}
351395if (code !== 0) {
352-resolve({
396+finish({
353397ok: false,
354398code: "ARCHIVE_ENTRIES_UNREADABLE",
355-reason: `tar -tzf exited ${code}: ${stderr.slice(0, 200)}`,
399+reason: `tar -tzf exited ${code}: ${stderr.slice(-200)}`,
356400});
357401return;
358402}
359-resolve({
360-ok: true,
361-entries: stdout
362-.split("\n")
363-.map(normalizeTarEntryPath)
364-.filter((entry): entry is string => entry !== null),
365-});
403+if (pending) {
404+if (!appendLine(pending)) {
405+return;
406+}
407+}
408+finish({ ok: true, entries });
366409});
367410child.on("error", (error) => {
368-clearTimeout(watchdog);
369-if (!aborted) {
370-aborted = true;
371-resolve({
372-ok: false,
373-code: "ARCHIVE_ENTRIES_UNREADABLE",
374-reason: `tar -tzf error: ${String(error)}`,
375-});
376-}
411+finish({
412+ok: false,
413+code: "ARCHIVE_ENTRIES_UNREADABLE",
414+reason: `tar -tzf error: ${String(error)}`,
415+});
377416});
378417child.stdin.on("error", (error: NodeJS.ErrnoException) => {
379-if (aborted && error.code === "EPIPE") {
418+if (settled && error.code === "EPIPE") {
380419return;
381420}
382-clearTimeout(watchdog);
383-if (!aborted) {
384-aborted = true;
385-resolve({
386-ok: false,
387-code: "ARCHIVE_ENTRIES_UNREADABLE",
388-reason: `tar -tzf input error: ${String(error)}`,
389-});
390-}
421+finish({
422+ok: false,
423+code: "ARCHIVE_ENTRIES_UNREADABLE",
424+reason: `tar -tzf input error: ${String(error)}`,
425+});
391426});
392427child.stdin.end(tarBuffer);
393428});
@@ -407,6 +442,8 @@ async function validateDirFetchEntries(input: {
407442input.phase === "preflight" ? "PREFLIGHT_ENTRIES_MISSING" : "ARCHIVE_ENTRIES_MISSING";
408443const invalidCode =
409444input.phase === "preflight" ? "PREFLIGHT_ENTRY_INVALID" : "ARCHIVE_ENTRY_INVALID";
445+const tooManyCode =
446+input.phase === "preflight" ? "PREFLIGHT_ENTRIES_TOO_MANY" : "ARCHIVE_ENTRIES_TOO_MANY";
410447if (!Array.isArray(input.entries)) {
411448await appendFileTransferAudit({
412449op: input.op,
@@ -426,6 +463,26 @@ async function validateDirFetchEntries(input: {
426463details: { path: input.canonicalPath },
427464});
428465}
466+if (input.entries.length > DIR_FETCH_MAX_ENTRIES) {
467+const reason = `dir.fetch ${input.phase} contains ${input.entries.length} entries; limit ${DIR_FETCH_MAX_ENTRIES}`;
468+await appendFileTransferAudit({
469+op: input.op,
470+nodeId: input.ctx.nodeId,
471+ nodeDisplayName,
472+requestedPath: input.requestedPath,
473+canonicalPath: input.canonicalPath,
474+decision: "denied:policy",
475+errorCode: tooManyCode,
476+ reason,
477+durationMs: Date.now() - input.startedAt,
478+});
479+return policyDeniedResult({
480+op: input.op,
481+code: tooManyCode,
482+message: `${reason}; refusing archive transfer`,
483+details: { path: input.canonicalPath, reason },
484+});
485+}
429486430487const entries: string[] = [];
431488for (const entry of input.entries) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。