@openclaw/fs-safe 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/README.md +7 -0
- package/dist/archive-deadline.d.ts +3 -0
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +44 -8
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +1 -0
- package/dist/archive-errors.d.ts +1 -0
- package/dist/archive-errors.d.ts.map +1 -1
- package/dist/archive-errors.js +3 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +26 -16
- package/dist/archive-limits.d.ts +11 -3
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +24 -0
- package/dist/archive-native.d.ts +3 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +24 -9
- package/dist/archive-policy.d.ts +2 -0
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +9 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +69 -41
- package/dist/archive-staging.d.ts +3 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +91 -43
- package/dist/archive-tar-admission.d.ts +7 -0
- package/dist/archive-tar-admission.d.ts.map +1 -0
- package/dist/archive-tar-admission.js +43 -0
- package/dist/archive-tar-gnu.d.ts +2 -0
- package/dist/archive-tar-gnu.d.ts.map +1 -0
- package/dist/archive-tar-gnu.js +20 -0
- package/dist/archive-tar-header.d.ts +8 -0
- package/dist/archive-tar-header.d.ts.map +1 -0
- package/dist/archive-tar-header.js +47 -0
- package/dist/archive-tar-meta.d.ts +29 -1
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +104 -35
- package/dist/archive-tar-runtime.d.ts +3 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-tar-runtime.js +3 -0
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +8 -2
- package/dist/archive-zip-names.js +2 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +38 -26
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +2 -4
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +3 -10
- package/dist/byte-budget.d.ts +5 -0
- package/dist/byte-budget.d.ts.map +1 -0
- package/dist/byte-budget.js +9 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +10 -5
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +9 -5
- package/dist/file-store-limit.d.ts +2 -0
- package/dist/file-store-limit.d.ts.map +1 -0
- package/dist/file-store-limit.js +8 -0
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +38 -8
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +26 -26
- package/dist/json-durable-queue-directory.d.ts +2 -0
- package/dist/json-durable-queue-directory.d.ts.map +1 -0
- package/dist/json-durable-queue-directory.js +20 -0
- package/dist/json-durable-queue-ownership.d.ts +14 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -0
- package/dist/json-durable-queue-ownership.js +168 -0
- package/dist/json-durable-queue-retirement.d.ts +9 -0
- package/dist/json-durable-queue-retirement.d.ts.map +1 -0
- package/dist/json-durable-queue-retirement.js +126 -0
- package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
- package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
- package/dist/json-durable-queue-transfer-lock.js +19 -0
- package/dist/json-durable-queue.d.ts +1 -0
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +90 -57
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +27 -8
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +4 -2
- package/dist/native-binding.d.ts +12 -3
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +11 -110
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +4 -2
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +27 -48
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +8 -9
- package/dist/private-temp-workspace.d.ts +3 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +81 -56
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +2 -4
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +6 -4
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +136 -82
- package/dist/replace-file-copy-fallback.d.ts +3 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +21 -36
- package/dist/replace-file-copy-source.d.ts +21 -0
- package/dist/replace-file-copy-source.d.ts.map +1 -0
- package/dist/replace-file-copy-source.js +112 -0
- package/dist/replace-file-descriptor.d.ts +13 -3
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +32 -7
- package/dist/replace-file-rename-policy.d.ts +7 -0
- package/dist/replace-file-rename-policy.d.ts.map +1 -0
- package/dist/replace-file-rename-policy.js +30 -0
- package/dist/replace-file-temp-owner.d.ts +46 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -0
- package/dist/replace-file-temp-owner.js +346 -0
- package/dist/replace-file.d.ts +6 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +72 -58
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +40 -26
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +6 -3
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +6 -5
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +17 -13
- package/dist/sibling-staged-file.d.ts +15 -0
- package/dist/sibling-staged-file.d.ts.map +1 -0
- package/dist/sibling-staged-file.js +148 -0
- package/dist/sibling-temp.d.ts +3 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +30 -74
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +48 -27
- package/dist/sidecar-lock-handle.d.ts +6 -2
- package/dist/sidecar-lock-handle.d.ts.map +1 -1
- package/dist/sidecar-lock-handle.js +17 -3
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +29 -7
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +28 -16
- package/dist/suppressed-error.d.ts +6 -0
- package/dist/suppressed-error.d.ts.map +1 -0
- package/dist/suppressed-error.js +15 -0
- package/dist/temp-cleanup.d.ts +2 -0
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +25 -10
- package/dist/temp-workspace-owner.d.ts +23 -0
- package/dist/temp-workspace-owner.d.ts.map +1 -0
- package/dist/temp-workspace-owner.js +320 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +5 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/docs/advanced.md +8 -1
- package/docs/archive.md +194 -26
- package/docs/atomic.md +11 -2
- package/docs/durability.md +1 -1
- package/docs/file-store.md +2 -0
- package/docs/install.md +8 -4
- package/docs/json.md +8 -4
- package/docs/native-helper.md +15 -3
- package/docs/native.md +33 -6
- package/docs/output.md +10 -0
- package/docs/public-api.md +6 -3
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +1 -1
- package/docs/regular-file.md +9 -2
- package/docs/root.md +2 -0
- package/docs/secret-file.md +5 -1
- package/docs/secure-file.md +4 -2
- package/docs/security-model.md +18 -0
- package/docs/sidecar-lock.md +4 -0
- package/docs/store.md +13 -0
- package/docs/temp.md +128 -14
- package/package.json +10 -10
package/dist/archive-read.js
CHANGED
|
@@ -1,29 +1,32 @@
|
|
|
1
|
-
import fsSync from "node:fs";
|
|
2
1
|
import fs from "node:fs/promises";
|
|
3
2
|
import { Readable } from "node:stream";
|
|
4
3
|
import { readFileHandleBounded } from "./bounded-read.js";
|
|
5
|
-
import { ArchiveFormatError, ArchiveSecurityError, isArchiveFormatErrorMessage, } from "./archive-errors.js";
|
|
4
|
+
import { ArchiveFormatError, ArchiveSecurityError, isArchiveFormatErrorMessage, isArchiveTarPathErrorMessage, } from "./archive-errors.js";
|
|
6
5
|
import { formatErrorDetail } from "./error-detail.js";
|
|
7
|
-
import {
|
|
6
|
+
import { stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
8
7
|
import { resolveArchiveKind } from "./archive-kind.js";
|
|
9
|
-
import { DEFAULT_MAX_ARCHIVE_BYTES_ZIP,
|
|
8
|
+
import { DEFAULT_MAX_ARCHIVE_BYTES_ZIP, DEFAULT_MAX_META_ENTRY_BYTES, ArchiveLimitError, ARCHIVE_LIMIT_ERROR_CODE, } from "./archive-limits.js";
|
|
10
9
|
import { readTarEntryInfo } from "./archive-tar.js";
|
|
11
10
|
import { preflightTarMetadata } from "./archive-tar-meta.js";
|
|
12
|
-
import { importOptionalTar, normalizeTarParserError } from "./archive-tar-runtime.js";
|
|
11
|
+
import { importOptionalTar, NODE_TAR_RATIO_DISABLED, normalizeTarParserError } from "./archive-tar-runtime.js";
|
|
13
12
|
import { loadZipArchiveWithPreflight } from "./archive-zip-preflight.js";
|
|
14
13
|
import { createZipIntegrityTransform, normalizeZipIntegrityError, } from "./archive-zip-integrity.js";
|
|
15
14
|
import { FsSafeError } from "./errors.js";
|
|
16
|
-
import {
|
|
15
|
+
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
16
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
17
17
|
import { getNativeBinding } from "./native.js";
|
|
18
18
|
import { admitZipBuffer } from "./archive-zip-admission.js";
|
|
19
|
-
import { resolveExtractLimits } from "./archive-limits.js";
|
|
19
|
+
import { resolveExtractLimits, resolveTarMeterLimits } from "./archive-limits.js";
|
|
20
20
|
import { tempFile } from "./temp-target.js";
|
|
21
21
|
const ZIP_UNIX_FILE_TYPE_MASK = 0o170000;
|
|
22
22
|
const ZIP_UNIX_SYMLINK_TYPE = 0o120000;
|
|
23
|
-
function
|
|
23
|
+
function canonicalEntryPath(entryPath) {
|
|
24
24
|
validateArchiveEntryPath(entryPath, { escapeLabel: "archive root" });
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
return stripArchivePath(entryPath, 0) ?? "";
|
|
26
|
+
}
|
|
27
|
+
function normalizedRequestedEntry(entryPath) {
|
|
28
|
+
const normalized = canonicalEntryPath(entryPath);
|
|
29
|
+
if (!normalized || /[/\\]$/.test(entryPath)) {
|
|
27
30
|
throw new Error(`archive entry is not a file: ${formatErrorDetail(entryPath)}`);
|
|
28
31
|
}
|
|
29
32
|
return normalized;
|
|
@@ -62,30 +65,37 @@ async function readStreamBounded(stream, maxBytes) {
|
|
|
62
65
|
}
|
|
63
66
|
async function stageArchiveInput(archivePath) {
|
|
64
67
|
const resolved = await fs.realpath(archivePath);
|
|
65
|
-
const before = await
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const handle = await fs.open(resolved,
|
|
68
|
+
const before = await inspectFileIdentity(async () => {
|
|
69
|
+
const stat = await fs.lstat(archivePath, { bigint: true });
|
|
70
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
71
|
+
throw new Error(`archive is not a regular file: ${archivePath}`);
|
|
72
|
+
}
|
|
73
|
+
return stat;
|
|
74
|
+
});
|
|
75
|
+
const handle = await fs.open(resolved, resolveReadOpenFlags());
|
|
73
76
|
const staged = await tempFile({ prefix: "fs-safe-archive-read", fileName: "archive.bin" });
|
|
74
77
|
try {
|
|
75
|
-
const opened = await
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
const opened = await inspectFileIdentity(async () => {
|
|
79
|
+
const stat = await handle.stat({ bigint: true });
|
|
80
|
+
if (!stat.isFile())
|
|
81
|
+
throw new Error("archive changed during validation");
|
|
82
|
+
return stat;
|
|
83
|
+
}, before);
|
|
84
|
+
await inspectFileIdentity(async () => {
|
|
85
|
+
const stat = await fs.lstat(resolved, { bigint: true });
|
|
86
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
87
|
+
throw new Error("archive changed during validation");
|
|
88
|
+
return stat;
|
|
89
|
+
}, opened);
|
|
83
90
|
const buffer = await readFileHandleBounded(handle, DEFAULT_MAX_ARCHIVE_BYTES_ZIP);
|
|
84
91
|
await fs.writeFile(staged.path, buffer, { flag: "wx", mode: 0o600 });
|
|
85
92
|
return { path: staged.path, buffer, cleanup: staged.cleanup };
|
|
86
93
|
}
|
|
87
94
|
catch (error) {
|
|
88
95
|
await staged.cleanup().catch(() => undefined);
|
|
96
|
+
if (error instanceof FsSafeError && error.code === "path-mismatch") {
|
|
97
|
+
throw new FsSafeError("path-mismatch", "archive changed during validation", { cause: error });
|
|
98
|
+
}
|
|
89
99
|
throw error;
|
|
90
100
|
}
|
|
91
101
|
finally {
|
|
@@ -98,7 +108,17 @@ async function readZipEntry(buffer, entryPath, maxBytes) {
|
|
|
98
108
|
maxEntryBytes: maxBytes,
|
|
99
109
|
maxExtractedBytes: maxBytes,
|
|
100
110
|
});
|
|
101
|
-
|
|
111
|
+
let entry;
|
|
112
|
+
// JSZip keys retain some aliases and may use Unicode Path metadata. Scan the
|
|
113
|
+
// effective entries once, after raw ZIP admission has rejected collisions.
|
|
114
|
+
for (const candidate of Object.values(archive.files)) {
|
|
115
|
+
if (canonicalEntryPath(candidate.name) !== entryPath)
|
|
116
|
+
continue;
|
|
117
|
+
if (entry) {
|
|
118
|
+
throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(entryPath)}`);
|
|
119
|
+
}
|
|
120
|
+
entry = candidate;
|
|
121
|
+
}
|
|
102
122
|
if (!entry || entry.dir) {
|
|
103
123
|
throw new Error(`archive entry not found: ${formatErrorDetail(entryPath)}`);
|
|
104
124
|
}
|
|
@@ -115,22 +135,34 @@ async function readZipEntry(buffer, entryPath, maxBytes) {
|
|
|
115
135
|
}
|
|
116
136
|
async function readTarEntry(archivePath, entryPath, maxBytes) {
|
|
117
137
|
const tar = await importOptionalTar();
|
|
138
|
+
const seenPaths = new Set();
|
|
118
139
|
await preflightTarMetadata({
|
|
119
140
|
archivePath,
|
|
120
|
-
|
|
141
|
+
limits: resolveTarMeterLimits(),
|
|
142
|
+
onMember(info) {
|
|
143
|
+
const normalized = canonicalEntryPath(info.path);
|
|
144
|
+
if (seenPaths.has(normalized)) {
|
|
145
|
+
throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
|
|
146
|
+
}
|
|
147
|
+
seenPaths.add(normalized);
|
|
148
|
+
if (normalized === entryPath && !["File", "OldFile", "ContiguousFile"].includes(info.type)) {
|
|
149
|
+
throw new Error(`archive entry is not a file: ${formatErrorDetail(entryPath)}`);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
121
152
|
});
|
|
122
153
|
let matched;
|
|
123
154
|
let entryError;
|
|
124
|
-
const seenPaths = new Set();
|
|
125
155
|
try {
|
|
126
156
|
await tar.t({
|
|
127
157
|
file: archivePath,
|
|
128
158
|
strict: true,
|
|
129
159
|
maxMetaEntrySize: DEFAULT_MAX_META_ENTRY_BYTES,
|
|
160
|
+
maxDecompressionRatio: NODE_TAR_RATIO_DISABLED,
|
|
130
161
|
onReadEntry(entry) {
|
|
131
162
|
const info = readTarEntryInfo(entry);
|
|
163
|
+
let normalized;
|
|
132
164
|
try {
|
|
133
|
-
|
|
165
|
+
normalized = canonicalEntryPath(info.path);
|
|
134
166
|
}
|
|
135
167
|
catch (error) {
|
|
136
168
|
// Throws escaping node-tar's callback do not reject its promise.
|
|
@@ -138,13 +170,6 @@ async function readTarEntry(archivePath, entryPath, maxBytes) {
|
|
|
138
170
|
entry.resume();
|
|
139
171
|
return;
|
|
140
172
|
}
|
|
141
|
-
const normalized = normalizeArchiveEntryPath(info.path).replace(/^\.\//, "");
|
|
142
|
-
if (seenPaths.has(normalized)) {
|
|
143
|
-
entryError ??= new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
|
|
144
|
-
entry.resume();
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
seenPaths.add(normalized);
|
|
148
173
|
if (normalized !== entryPath) {
|
|
149
174
|
entry.resume();
|
|
150
175
|
return;
|
|
@@ -192,15 +217,15 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
|
|
|
192
217
|
if (native) {
|
|
193
218
|
try {
|
|
194
219
|
const signal = new AbortController().signal;
|
|
195
|
-
const
|
|
220
|
+
const limits = resolveTarMeterLimits();
|
|
221
|
+
const manifest = await native.inspectArchiveNative(staged.path, kind, limits, signal);
|
|
196
222
|
let rawEntryPath;
|
|
197
223
|
if (physicalCount !== undefined && manifest.length !== physicalCount) {
|
|
198
224
|
throw new ArchiveSecurityError("entry-path", "zip decoder collapsed entry names");
|
|
199
225
|
}
|
|
200
226
|
const seenPaths = new Set();
|
|
201
227
|
for (const entry of manifest) {
|
|
202
|
-
|
|
203
|
-
const normalized = normalizeArchiveEntryPath(entry.path).replace(/^\.\//, "");
|
|
228
|
+
const normalized = canonicalEntryPath(entry.path);
|
|
204
229
|
if (seenPaths.has(normalized)) {
|
|
205
230
|
throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
|
|
206
231
|
}
|
|
@@ -215,9 +240,12 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
|
|
|
215
240
|
if (!rawEntryPath) {
|
|
216
241
|
throw new Error(`archive entry not found: ${formatErrorDetail(entryPath)}`);
|
|
217
242
|
}
|
|
218
|
-
return await native.readArchiveEntryNative(staged.path, kind, rawEntryPath, options.maxBytes,
|
|
243
|
+
return await native.readArchiveEntryNative(staged.path, kind, rawEntryPath, options.maxBytes, limits, signal);
|
|
219
244
|
}
|
|
220
245
|
catch (error) {
|
|
246
|
+
if (error instanceof Error && isArchiveTarPathErrorMessage(error.message)) {
|
|
247
|
+
throw new ArchiveSecurityError("entry-path", error.message, { cause: error });
|
|
248
|
+
}
|
|
221
249
|
if (error instanceof Error) {
|
|
222
250
|
for (const code of Object.values(ARCHIVE_LIMIT_ERROR_CODE)) {
|
|
223
251
|
if (error.message.includes(code))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ExtractionDeadline } from "./archive-deadline.js";
|
|
1
2
|
import { ArchiveSecurityError } from "./archive-errors.js";
|
|
2
3
|
export { ArchiveSecurityError, type ArchiveSecurityErrorCode } from "./archive-errors.js";
|
|
3
4
|
export declare function prepareArchiveDestinationDir(destDir: string): Promise<string>;
|
|
@@ -8,6 +9,7 @@ export declare function prepareArchiveOutputPath(params: {
|
|
|
8
9
|
outPath: string;
|
|
9
10
|
originalPath: string;
|
|
10
11
|
isDirectory: boolean;
|
|
12
|
+
deadline?: ExtractionDeadline;
|
|
11
13
|
}): Promise<void>;
|
|
12
14
|
export declare function withStagedArchiveDestination<T>(params: {
|
|
13
15
|
destinationRealDir: string;
|
|
@@ -18,6 +20,7 @@ export declare function mergeExtractedTreeIntoDestination(params: {
|
|
|
18
20
|
sourceDir: string;
|
|
19
21
|
destinationDir: string;
|
|
20
22
|
destinationRealDir: string;
|
|
23
|
+
deadline?: ExtractionDeadline;
|
|
21
24
|
}): Promise<void>;
|
|
22
25
|
export declare function createArchiveSymlinkTraversalError(originalPath: string): ArchiveSecurityError;
|
|
23
26
|
//# sourceMappingURL=archive-staging.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"AAOA,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,oBAAoB,EAErB,MAAM,qBAAqB,CAAC;AAgB7B,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAkC1F,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA4BnF;AA4DD,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,GAAG,OAAO,CAAC,IAAI,CAAC,CA6DhB;AAsID,wBAAsB,4BAA4B,CAAC,CAAC,EAAE,MAAM,EAAE;IAC5D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,GAAG,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CACzC,GAAG,OAAO,CAAC,CAAC,CAAC,CA6Bb;AAED,wBAAsB,iCAAiC,CAAC,MAAM,EAAE;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,GAAG,OAAO,CAAC,IAAI,CAAC,CA8FhB;AAED,wBAAgB,kCAAkC,CAAC,YAAY,EAAE,MAAM,GAAG,oBAAoB,CAE7F"}
|
package/dist/archive-staging.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
-
import fsSync from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, } from "./directory-guard.js";
|
|
4
|
+
import { ownExtractionDestinationMutation, } from "./archive-deadline.js";
|
|
5
5
|
import { ArchiveSecurityError, } from "./archive-errors.js";
|
|
6
6
|
import { FsSafeError } from "./errors.js";
|
|
7
7
|
import { formatErrorDetail } from "./error-detail.js";
|
|
8
8
|
import { resolveOpenedFileRealPathForHandle, root } from "./root.js";
|
|
9
9
|
import { isNotFoundPathError, isPathInside } from "./path.js";
|
|
10
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
10
11
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
11
12
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
12
13
|
const ERROR_ARCHIVE_ENTRY_TRAVERSES_SYMLINK = "archive entry traverses symlink in destination";
|
|
13
14
|
const ARCHIVE_STAGING_MODE = 0o700;
|
|
15
|
+
function checkExtractionDeadline(deadline) {
|
|
16
|
+
deadline?.check();
|
|
17
|
+
}
|
|
14
18
|
export { ArchiveSecurityError } from "./archive-errors.js";
|
|
15
19
|
function symlinkTraversalError(originalPath) {
|
|
16
20
|
return new ArchiveSecurityError("destination-symlink-traversal", `${ERROR_ARCHIVE_ENTRY_TRAVERSES_SYMLINK}: ${formatErrorDetail(originalPath)}`);
|
|
@@ -105,55 +109,79 @@ async function mkdirArchiveOutput(params) {
|
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
111
|
export async function prepareArchiveOutputPath(params) {
|
|
112
|
+
checkExtractionDeadline(params.deadline);
|
|
108
113
|
const targetRoot = await root(params.destinationRealDir);
|
|
114
|
+
checkExtractionDeadline(params.deadline);
|
|
109
115
|
const destinationGuard = await createDirectoryIdentityGuard(params.destinationRealDir);
|
|
116
|
+
checkExtractionDeadline(params.deadline);
|
|
110
117
|
const relPath = params.relPath.split(path.sep).join(path.posix.sep);
|
|
111
118
|
await assertNoSymlinkTraversal({
|
|
112
119
|
rootDir: params.destinationDir,
|
|
113
120
|
relPath,
|
|
114
121
|
originalPath: params.originalPath,
|
|
115
122
|
});
|
|
123
|
+
checkExtractionDeadline(params.deadline);
|
|
116
124
|
if (params.isDirectory) {
|
|
117
125
|
await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("mkdir", params.outPath);
|
|
118
|
-
|
|
119
|
-
await
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
checkExtractionDeadline(params.deadline);
|
|
127
|
+
await ownExtractionDestinationMutation(params.deadline, async () => {
|
|
128
|
+
await assertDirectoryIdentityGuard(destinationGuard);
|
|
129
|
+
checkExtractionDeadline(params.deadline);
|
|
130
|
+
await mkdirArchiveOutput({
|
|
131
|
+
targetRoot,
|
|
132
|
+
relativePath: relPath,
|
|
133
|
+
originalPath: params.originalPath,
|
|
134
|
+
});
|
|
135
|
+
checkExtractionDeadline(params.deadline);
|
|
136
|
+
await assertDirectoryIdentityGuard(destinationGuard);
|
|
137
|
+
checkExtractionDeadline(params.deadline);
|
|
138
|
+
await assertResolvedInsideDestination({
|
|
139
|
+
destinationRealDir: params.destinationRealDir,
|
|
140
|
+
targetPath: params.outPath,
|
|
141
|
+
originalPath: params.originalPath,
|
|
142
|
+
});
|
|
143
|
+
checkExtractionDeadline(params.deadline);
|
|
125
144
|
});
|
|
126
145
|
return;
|
|
127
146
|
}
|
|
128
147
|
const parentRel = path.posix.dirname(relPath);
|
|
129
148
|
if (parentRel !== ".") {
|
|
130
149
|
await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("mkdir", path.dirname(params.outPath));
|
|
131
|
-
|
|
132
|
-
await
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
150
|
+
checkExtractionDeadline(params.deadline);
|
|
151
|
+
await ownExtractionDestinationMutation(params.deadline, async () => {
|
|
152
|
+
await assertDirectoryIdentityGuard(destinationGuard);
|
|
153
|
+
checkExtractionDeadline(params.deadline);
|
|
154
|
+
await mkdirArchiveOutput({
|
|
155
|
+
targetRoot,
|
|
156
|
+
relativePath: parentRel,
|
|
157
|
+
originalPath: params.originalPath,
|
|
158
|
+
});
|
|
159
|
+
checkExtractionDeadline(params.deadline);
|
|
160
|
+
await assertDirectoryIdentityGuard(destinationGuard);
|
|
161
|
+
checkExtractionDeadline(params.deadline);
|
|
136
162
|
});
|
|
137
|
-
await assertDirectoryIdentityGuard(destinationGuard);
|
|
138
163
|
}
|
|
139
164
|
await assertResolvedInsideDestination({
|
|
140
165
|
destinationRealDir: params.destinationRealDir,
|
|
141
166
|
targetPath: path.dirname(params.outPath),
|
|
142
167
|
originalPath: params.originalPath,
|
|
143
168
|
});
|
|
169
|
+
checkExtractionDeadline(params.deadline);
|
|
144
170
|
}
|
|
145
171
|
async function chmodInsideDestinationBestEffort(params) {
|
|
146
172
|
await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("chmod", params.destinationPath);
|
|
173
|
+
checkExtractionDeadline(params.deadline);
|
|
147
174
|
const destinationGuard = await createDirectoryIdentityGuard(params.destinationRealDir);
|
|
175
|
+
checkExtractionDeadline(params.deadline);
|
|
148
176
|
await assertDirectoryIdentityGuard(destinationGuard);
|
|
149
|
-
|
|
150
|
-
? fsSync.constants.O_NOFOLLOW
|
|
151
|
-
: 0;
|
|
177
|
+
checkExtractionDeadline(params.deadline);
|
|
152
178
|
const handle = await fs
|
|
153
|
-
.open(params.destinationPath,
|
|
179
|
+
.open(params.destinationPath, resolveReadOpenFlags())
|
|
154
180
|
.catch(() => null);
|
|
181
|
+
checkExtractionDeadline(params.deadline);
|
|
155
182
|
if (!handle) {
|
|
156
183
|
const stat = await fs.lstat(params.destinationPath).catch(() => null);
|
|
184
|
+
checkExtractionDeadline(params.deadline);
|
|
157
185
|
if (stat?.isSymbolicLink()) {
|
|
158
186
|
throw symlinkTraversalError(params.originalPath);
|
|
159
187
|
}
|
|
@@ -161,35 +189,45 @@ async function chmodInsideDestinationBestEffort(params) {
|
|
|
161
189
|
}
|
|
162
190
|
try {
|
|
163
191
|
const stat = await handle.stat();
|
|
192
|
+
checkExtractionDeadline(params.deadline);
|
|
164
193
|
if (!stat.isDirectory() && !stat.isFile()) {
|
|
165
194
|
return;
|
|
166
195
|
}
|
|
167
196
|
const realPath = await resolveOpenedFileRealPathForHandle(handle, params.destinationPath);
|
|
197
|
+
checkExtractionDeadline(params.deadline);
|
|
168
198
|
if (!isPathInside(params.destinationRealDir, realPath)) {
|
|
169
199
|
throw symlinkTraversalError(params.originalPath);
|
|
170
200
|
}
|
|
171
|
-
await
|
|
172
|
-
|
|
201
|
+
await ownExtractionDestinationMutation(params.deadline, async () => {
|
|
202
|
+
await handle.chmod(params.mode).catch(() => undefined);
|
|
203
|
+
checkExtractionDeadline(params.deadline);
|
|
204
|
+
await assertDirectoryIdentityGuard(destinationGuard);
|
|
205
|
+
checkExtractionDeadline(params.deadline);
|
|
206
|
+
});
|
|
173
207
|
}
|
|
174
208
|
finally {
|
|
175
209
|
await handle.close().catch(() => undefined);
|
|
176
210
|
}
|
|
177
211
|
}
|
|
178
212
|
async function applyStagedEntryMode(params) {
|
|
213
|
+
checkExtractionDeadline(params.deadline);
|
|
179
214
|
const destinationPath = path.join(params.destinationRealDir, params.relPath);
|
|
180
215
|
await assertResolvedInsideDestination({
|
|
181
216
|
destinationRealDir: params.destinationRealDir,
|
|
182
217
|
targetPath: destinationPath,
|
|
183
218
|
originalPath: params.originalPath,
|
|
184
219
|
});
|
|
220
|
+
checkExtractionDeadline(params.deadline);
|
|
185
221
|
if (params.mode !== 0) {
|
|
186
222
|
await chmodInsideDestinationBestEffort({
|
|
187
223
|
destinationRealDir: params.destinationRealDir,
|
|
188
224
|
destinationPath,
|
|
189
225
|
mode: params.mode,
|
|
190
226
|
originalPath: params.originalPath,
|
|
227
|
+
deadline: params.deadline,
|
|
191
228
|
});
|
|
192
229
|
}
|
|
230
|
+
checkExtractionDeadline(params.deadline);
|
|
193
231
|
}
|
|
194
232
|
async function assertExtractedFileHasNoHardlinkAlias(params) {
|
|
195
233
|
const destinationPath = path.join(params.destinationRealDir, params.relPath);
|
|
@@ -296,6 +334,7 @@ export async function mergeExtractedTreeIntoDestination(params) {
|
|
|
296
334
|
outPath: destinationPath,
|
|
297
335
|
originalPath,
|
|
298
336
|
isDirectory: true,
|
|
337
|
+
deadline: params.deadline,
|
|
299
338
|
});
|
|
300
339
|
await walk(sourcePath);
|
|
301
340
|
await applyStagedEntryMode({
|
|
@@ -303,6 +342,7 @@ export async function mergeExtractedTreeIntoDestination(params) {
|
|
|
303
342
|
relPath,
|
|
304
343
|
mode: sourceStat.mode & 0o777,
|
|
305
344
|
originalPath,
|
|
345
|
+
deadline: params.deadline,
|
|
306
346
|
});
|
|
307
347
|
continue;
|
|
308
348
|
}
|
|
@@ -316,31 +356,39 @@ export async function mergeExtractedTreeIntoDestination(params) {
|
|
|
316
356
|
outPath: destinationPath,
|
|
317
357
|
originalPath,
|
|
318
358
|
isDirectory: false,
|
|
359
|
+
deadline: params.deadline,
|
|
319
360
|
});
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
});
|
|
339
|
-
if (err instanceof FsSafeError && (err.code === "hardlink" || err.code === "path-alias")) {
|
|
340
|
-
throw symlinkTraversalError(originalPath);
|
|
361
|
+
await ownExtractionDestinationMutation(params.deadline, async () => {
|
|
362
|
+
try {
|
|
363
|
+
await targetRoot.copyIn(relPath, sourcePath, { mkdir: true });
|
|
364
|
+
checkExtractionDeadline(params.deadline);
|
|
365
|
+
await assertExtractedFileHasNoHardlinkAlias({
|
|
366
|
+
destinationRealDir: params.destinationRealDir,
|
|
367
|
+
relPath,
|
|
368
|
+
originalPath,
|
|
369
|
+
});
|
|
370
|
+
checkExtractionDeadline(params.deadline);
|
|
371
|
+
await applyStagedEntryMode({
|
|
372
|
+
destinationRealDir: params.destinationRealDir,
|
|
373
|
+
relPath,
|
|
374
|
+
mode: sourceStat.mode & 0o777,
|
|
375
|
+
originalPath,
|
|
376
|
+
deadline: params.deadline,
|
|
377
|
+
});
|
|
378
|
+
checkExtractionDeadline(params.deadline);
|
|
341
379
|
}
|
|
342
|
-
|
|
343
|
-
|
|
380
|
+
catch (err) {
|
|
381
|
+
await removeExtractedDestinationFile({
|
|
382
|
+
destinationRealDir: params.destinationRealDir,
|
|
383
|
+
relPath,
|
|
384
|
+
});
|
|
385
|
+
if (err instanceof FsSafeError &&
|
|
386
|
+
(err.code === "hardlink" || err.code === "path-alias")) {
|
|
387
|
+
throw symlinkTraversalError(originalPath);
|
|
388
|
+
}
|
|
389
|
+
throw err;
|
|
390
|
+
}
|
|
391
|
+
});
|
|
344
392
|
}
|
|
345
393
|
};
|
|
346
394
|
await walk(params.sourceDir);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TarEntryInfo } from "./archive-tar.js";
|
|
2
|
+
export declare function rawTarMember(header: Buffer, size: number, effectivePath?: string): TarEntryInfo;
|
|
3
|
+
export declare function createTarAdmissionPlan(manifest: readonly TarEntryInfo[], check: (entry: TarEntryInfo) => boolean, strip: number): {
|
|
4
|
+
consume(entry: TarEntryInfo): string | null;
|
|
5
|
+
finish(): void;
|
|
6
|
+
};
|
|
7
|
+
//# sourceMappingURL=archive-tar-admission.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-tar-admission.d.ts","sourceRoot":"","sources":["../src/archive-tar-admission.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAYrD,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,YAAY,CAS/F;AAED,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,YAAY,EAAE,EACjC,KAAK,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,EACvC,KAAK,EAAE,MAAM,GACZ;IAAE,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,IAAI,IAAI,CAAA;CAAE,CAmBjE"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ArchiveFormatError } from "./archive-errors.js";
|
|
2
|
+
import { stripArchivePath, validateArchiveEntryPath } from "./archive-entry.js";
|
|
3
|
+
import { readTarHeaderPaths } from "./archive-tar-header.js";
|
|
4
|
+
// node-tar's normalFsTypes/ReadEntry switch. All other non-metadata types
|
|
5
|
+
// bypass both its filter and entry event; they still belong to our manifest.
|
|
6
|
+
// Header.decode normalizes NUL to "0" before choosing the emitted File type.
|
|
7
|
+
const visibleTypes = new Map([
|
|
8
|
+
[0, "File"], [0x30, "File"], [0x31, "Link"], [0x32, "SymbolicLink"],
|
|
9
|
+
[0x33, "CharacterDevice"], [0x34, "BlockDevice"], [0x35, "Directory"],
|
|
10
|
+
[0x36, "FIFO"], [0x37, "ContiguousFile"], [0x44, "GNUDumpDir"],
|
|
11
|
+
]);
|
|
12
|
+
export function rawTarMember(header, size, effectivePath) {
|
|
13
|
+
const { name, prefix } = readTarHeaderPaths(header);
|
|
14
|
+
validateArchiveEntryPath(name);
|
|
15
|
+
validateArchiveEntryPath(prefix);
|
|
16
|
+
const rawPath = prefix ? `${prefix}/${name}` : name;
|
|
17
|
+
validateArchiveEntryPath(rawPath);
|
|
18
|
+
const path = effectivePath ?? rawPath;
|
|
19
|
+
validateArchiveEntryPath(path);
|
|
20
|
+
return { path, size, type: visibleTypes.get(header[156]) ?? "Unsupported" };
|
|
21
|
+
}
|
|
22
|
+
export function createTarAdmissionPlan(manifest, check, strip) {
|
|
23
|
+
const visible = [];
|
|
24
|
+
for (const entry of manifest) {
|
|
25
|
+
const accepted = check(entry);
|
|
26
|
+
if (entry.type !== "Unsupported") {
|
|
27
|
+
visible.push({ entry, output: accepted ? stripArchivePath(entry.path, strip) : null });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
let index = 0;
|
|
31
|
+
const mismatch = () => new ArchiveFormatError("invalid TAR header: parser disagrees with raw admission");
|
|
32
|
+
return {
|
|
33
|
+
consume(actual) {
|
|
34
|
+
const expected = visible[index++];
|
|
35
|
+
if (!expected || expected.entry.type !== actual.type || expected.entry.size !== actual.size ||
|
|
36
|
+
stripArchivePath(expected.entry.path, 0) !== stripArchivePath(actual.path, 0))
|
|
37
|
+
throw mismatch();
|
|
38
|
+
return expected.output;
|
|
39
|
+
},
|
|
40
|
+
finish() { if (index !== visible.length)
|
|
41
|
+
throw mismatch(); },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-tar-gnu.d.ts","sourceRoot":"","sources":["../src/archive-tar-gnu.ts"],"names":[],"mappings":"AAKA,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,GAAG,GAAG,MAAM,CAazE"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ArchiveFormatError } from "./archive-errors.js";
|
|
2
|
+
import { validateArchiveEntryPath } from "./archive-entry.js";
|
|
3
|
+
// Keep the byte grammar aligned with native/src/tar_meter.rs. Parsers differ
|
|
4
|
+
// on embedded NULs and malformed UTF-8, so validate before either sees a name.
|
|
5
|
+
export function validateGnuMetadata(body, type) {
|
|
6
|
+
const value = body.at(-1) === 0 ? body.subarray(0, -1) : body;
|
|
7
|
+
if (!value.length || value.includes(0)) {
|
|
8
|
+
throw new ArchiveFormatError("invalid GNU metadata: empty name or embedded NUL");
|
|
9
|
+
}
|
|
10
|
+
let name;
|
|
11
|
+
try {
|
|
12
|
+
name = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(value);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw new ArchiveFormatError("invalid GNU metadata: name is not valid UTF-8");
|
|
16
|
+
}
|
|
17
|
+
if (type === "L")
|
|
18
|
+
validateArchiveEntryPath(name);
|
|
19
|
+
return name;
|
|
20
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function validateTarChecksum(header: Buffer): void;
|
|
2
|
+
export declare function readTarHeaderPaths(header: Buffer): {
|
|
3
|
+
name: string;
|
|
4
|
+
prefix: string;
|
|
5
|
+
linkname: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function validateTarHeader(header: Buffer): void;
|
|
8
|
+
//# sourceMappingURL=archive-tar-header.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-tar-header.d.ts","sourceRoot":"","sources":["../src/archive-tar-header.ts"],"names":[],"mappings":"AAGA,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAWxD;AAcD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CASrG;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAUtD"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ArchiveFormatError, ArchiveSecurityError } from "./archive-errors.js";
|
|
2
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
3
|
+
export function validateTarChecksum(header) {
|
|
4
|
+
const field = header.subarray(148, 156);
|
|
5
|
+
const end = field.indexOf(0);
|
|
6
|
+
const digits = field.subarray(0, end < 0 ? field.length : end).toString("latin1").replace(/^ +| +$/g, "");
|
|
7
|
+
const sum = header.reduce((total, byte, index) => total + (index >= 148 && index < 156 ? 32 : byte), 0);
|
|
8
|
+
// Require an in-field delimiter: node-tar otherwise reads into the typeflag.
|
|
9
|
+
if (!/^[0-7]+$/.test(digits) || ![0, 32].includes(field[7]) ||
|
|
10
|
+
(end >= 0 && field.subarray(end).some((byte) => byte !== 0 && byte !== 32)) ||
|
|
11
|
+
Number.parseInt(digits, 8) !== sum) {
|
|
12
|
+
throw new ArchiveFormatError("invalid TAR header: checksum failure");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function fixedField(field) {
|
|
16
|
+
const zero = field.indexOf(0);
|
|
17
|
+
if (zero >= 0 && field.subarray(zero).some((byte) => byte !== 0)) {
|
|
18
|
+
throw new ArchiveSecurityError("entry-path", "tar entry path contains bytes after NUL");
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return utf8.decode(field.subarray(0, zero < 0 ? field.length : zero));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new ArchiveSecurityError("entry-path", "tar entry path is not valid UTF-8");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function readTarHeaderPaths(header) {
|
|
28
|
+
// node-tar's star layout uses a 130-byte prefix followed by atime/ctime.
|
|
29
|
+
const prefixEnd = header[475] === 0 ? 475 : 500;
|
|
30
|
+
return {
|
|
31
|
+
name: fixedField(header.subarray(0, 100)),
|
|
32
|
+
prefix: header.subarray(257, 265).equals(Buffer.from("ustar\0" + "00"))
|
|
33
|
+
? fixedField(header.subarray(345, prefixEnd)) : "",
|
|
34
|
+
linkname: fixedField(header.subarray(157, 257)),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function validateTarHeader(header) {
|
|
38
|
+
validateTarChecksum(header);
|
|
39
|
+
const { linkname } = readTarHeaderPaths(header);
|
|
40
|
+
const isLink = header[156] === 0x31 || header[156] === 0x32;
|
|
41
|
+
if (isLink && !linkname) {
|
|
42
|
+
throw new ArchiveFormatError("invalid TAR header: linkname required on a link header");
|
|
43
|
+
}
|
|
44
|
+
if (!isLink && linkname) {
|
|
45
|
+
throw new ArchiveFormatError("invalid TAR header: linkname forbidden on a non-link header");
|
|
46
|
+
}
|
|
47
|
+
}
|