@openclaw/fs-safe 0.2.6 → 0.3.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 +17 -0
- package/README.md +21 -7
- package/dist/archive-deadline.d.ts +9 -0
- package/dist/archive-deadline.d.ts.map +1 -0
- package/dist/archive-deadline.js +67 -0
- package/dist/archive-file-io.d.ts +9 -0
- package/dist/archive-file-io.d.ts.map +1 -0
- package/dist/archive-file-io.js +11 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +43 -3
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +213 -105
- package/dist/deny-mutations.d.ts +11 -0
- package/dist/deny-mutations.d.ts.map +1 -0
- package/dist/deny-mutations.js +102 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +5 -0
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +51 -4
- package/dist/opened-realpath.d.ts +3 -0
- package/dist/opened-realpath.d.ts.map +1 -0
- package/dist/opened-realpath.js +79 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +1 -0
- package/dist/pinned-write.js +3 -9
- package/dist/read-opened-file.d.ts +18 -0
- package/dist/read-opened-file.d.ts.map +1 -0
- package/dist/read-opened-file.js +15 -0
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +23 -3
- package/dist/root-impl.d.ts +18 -15
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +60 -100
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +16 -5
- package/dist/root.d.ts +1 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/secret-file.d.ts +1 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +23 -3
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +9 -3
- package/dist/symlink-parents.js +2 -2
- package/docs/errors.md +2 -0
- package/docs/python-helper.md +3 -3
- package/docs/root.md +19 -9
- package/docs/secret-file.md +3 -2
- package/docs/security-model.md +4 -0
- package/docs/timing.md +1 -1
- package/docs/types.md +21 -9
- package/docs/writing.md +19 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 - 2026-05-21
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Add opt-in `denyMutations` policies with exact `paths` and subtree `prefixes` so callers can protect application-sensitive files from root write, copy, move, remove, mkdir, and writable-open operations. (#20; thanks @amknight)
|
|
8
|
+
|
|
9
|
+
### Security and Correctness
|
|
10
|
+
|
|
11
|
+
- Retry async JSON reads (`readJson`, `readJsonIfExists`, `tryReadJson`) up to five attempts with 50ms exponential backoff when the file is rotated mid-read by an atomic rename, and tag the underlying race as `FsSafeError("path-mismatch")` so callers can distinguish transient swaps from corruption. (#19; thanks @yetval)
|
|
12
|
+
|
|
13
|
+
## 0.2.7 - 2026-05-20
|
|
14
|
+
|
|
15
|
+
### Security and Correctness
|
|
16
|
+
|
|
17
|
+
- Restore best-effort Node write fallbacks when the Python helper is disabled or unavailable, preserving the `mode: "off"` contract while documenting the weaker POSIX same-UID race resistance compared with fd-relative helper commits.
|
|
18
|
+
- Harden DeepSec-reported archive staging and input pinning, secret and queue hardlink rejection, absolute output file validation, sidecar lock read failures, ClawSweeper dispatch trust checks, and scoped path defaults.
|
|
19
|
+
|
|
3
20
|
## 0.2.6 - 2026-05-17
|
|
4
21
|
|
|
5
22
|
### Security and Correctness
|
package/README.md
CHANGED
|
@@ -66,15 +66,18 @@ before first use when you need a strict environment policy:
|
|
|
66
66
|
import { configureFsSafePython } from "@openclaw/fs-safe";
|
|
67
67
|
|
|
68
68
|
configureFsSafePython({ mode: "auto" }); // default: use helper, fall back if unavailable
|
|
69
|
-
configureFsSafePython({ mode: "off" }); // never spawn Python;
|
|
69
|
+
configureFsSafePython({ mode: "off" }); // never spawn Python; use best-effort Node fallbacks
|
|
70
70
|
configureFsSafePython({ mode: "require" }); // fail closed if helper cannot start
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
Equivalent env vars: `FS_SAFE_PYTHON_MODE=auto|off|require` and
|
|
74
|
-
`FS_SAFE_PYTHON=/path/to/python3`.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
`FS_SAFE_PYTHON=/path/to/python3`. Without Python, `fs-safe` keeps lexical and
|
|
75
|
+
canonical root checks, no-follow opens, atomic temp+rename writes, and
|
|
76
|
+
post-write identity verification. What you lose is the strongest POSIX
|
|
77
|
+
fd-relative protection against a same-process-user racer swapping parent
|
|
78
|
+
directories between validation and mutation. Windows already uses the Node
|
|
79
|
+
fallback path. See the [Python helper policy](docs/python-helper.md) for
|
|
80
|
+
deployment guidance.
|
|
78
81
|
|
|
79
82
|
## Quick start
|
|
80
83
|
|
|
@@ -152,7 +155,18 @@ await using opened = await fs.openWritable("logs/current.log", { writeMode: "app
|
|
|
152
155
|
}
|
|
153
156
|
```
|
|
154
157
|
|
|
155
|
-
`nonBlockingRead` is the only I/O scheduling knob in `RootDefaults`; it applies to read/open operations because it changes how file descriptors are opened. Filesystem safety policy remains explicit through `hardlinks` and `
|
|
158
|
+
`nonBlockingRead` is the only I/O scheduling knob in `RootDefaults`; it applies to read/open operations because it changes how file descriptors are opened. Filesystem safety policy remains explicit through `hardlinks`, `symlinks`, and `denyMutations`.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
const locked = await root("/srv/workspace", {
|
|
162
|
+
denyMutations: {
|
|
163
|
+
paths: ["/srv/workspace/.env"],
|
|
164
|
+
prefixes: ["/srv/workspace/.ssh"],
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await locked.write(".env", "token"); // FsSafeError code "denied-path"
|
|
169
|
+
```
|
|
156
170
|
|
|
157
171
|
`stat()`, `exists()`, and `list()` are boundary-checked, but they cannot pin a later operation to the same filesystem object. Use `read()`, `open()`, `write()`, `create()`, `copyIn()`, `move()`, or `remove()` for operations that must be race-resistant at the point of use.
|
|
158
172
|
|
|
@@ -432,7 +446,7 @@ Current `FsSafeErrorCode` values are `already-exists`, `hardlink`, `helper-faile
|
|
|
432
446
|
- root-bounded APIs resolve paths against a configured root and reject canonical escapes
|
|
433
447
|
- reads open with `O_NOFOLLOW` where available, then verify fd identity matches the path identity before returning the buffer or handle
|
|
434
448
|
- writes use pinned parent-directory helpers and atomic replacement on POSIX, with verified post-write identity
|
|
435
|
-
- `remove`, `mkdir`, `move`, `stat`, `list`, and parent-fd writes use one persistent fd-relative Python helper on POSIX
|
|
449
|
+
- `remove`, `mkdir`, `move`, `stat`, `list`, and parent-fd writes use one persistent fd-relative Python helper on POSIX, with Node fallbacks when the helper is disabled or unavailable
|
|
436
450
|
- archive extraction stages into a private directory and merges through the same boundary checks used by direct writes
|
|
437
451
|
|
|
438
452
|
## Limitations
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type ExtractionDeadline = {
|
|
2
|
+
signal: AbortSignal;
|
|
3
|
+
check: () => void;
|
|
4
|
+
dispose: () => void;
|
|
5
|
+
};
|
|
6
|
+
export declare function createPipelineTimeoutError(err: unknown, deadline: ExtractionDeadline): unknown;
|
|
7
|
+
export declare function waitForDeadline<T>(promise: Promise<T>, deadline: ExtractionDeadline): Promise<T>;
|
|
8
|
+
export declare function withExtractionDeadline<T>(timeoutMs: number, label: string, run: (deadline: ExtractionDeadline) => Promise<T>): Promise<T>;
|
|
9
|
+
//# sourceMappingURL=archive-deadline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-deadline.d.ts","sourceRoot":"","sources":["../src/archive-deadline.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAWF,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,kBAAkB,GAC3B,OAAO,CAST;AAED,wBAAsB,eAAe,CAAC,CAAC,EACrC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,QAAQ,EAAE,kBAAkB,GAC3B,OAAO,CAAC,CAAC,CAAC,CAgBZ;AA4BD,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,OAAO,CAAC,CAAC,CAAC,GAChD,OAAO,CAAC,CAAC,CAAC,CAQZ"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function signalReason(signal, fallback) {
|
|
2
|
+
const reason = signal.reason;
|
|
3
|
+
return reason instanceof Error ? reason : fallback ?? new Error(String(reason));
|
|
4
|
+
}
|
|
5
|
+
function deadlineReason(deadline) {
|
|
6
|
+
return signalReason(deadline.signal);
|
|
7
|
+
}
|
|
8
|
+
export function createPipelineTimeoutError(err, deadline) {
|
|
9
|
+
if (deadline.signal.aborted &&
|
|
10
|
+
err instanceof Error &&
|
|
11
|
+
(err.name === "AbortError" || err.message === "The operation was aborted")) {
|
|
12
|
+
return deadlineReason(deadline);
|
|
13
|
+
}
|
|
14
|
+
return err;
|
|
15
|
+
}
|
|
16
|
+
export async function waitForDeadline(promise, deadline) {
|
|
17
|
+
deadline.check();
|
|
18
|
+
if (deadline.signal.aborted) {
|
|
19
|
+
throw deadlineReason(deadline);
|
|
20
|
+
}
|
|
21
|
+
return await Promise.race([
|
|
22
|
+
promise,
|
|
23
|
+
new Promise((_, reject) => {
|
|
24
|
+
const abort = () => reject(deadlineReason(deadline));
|
|
25
|
+
deadline.signal.addEventListener("abort", abort, { once: true });
|
|
26
|
+
const cleanup = () => {
|
|
27
|
+
deadline.signal.removeEventListener("abort", abort);
|
|
28
|
+
};
|
|
29
|
+
promise.then(cleanup, cleanup);
|
|
30
|
+
}),
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
function createExtractionDeadline(timeoutMs, label) {
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
36
|
+
return {
|
|
37
|
+
signal: controller.signal,
|
|
38
|
+
check: () => undefined,
|
|
39
|
+
dispose: () => undefined,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const timeoutError = new Error(`${label} timed out after ${timeoutMs}ms`);
|
|
43
|
+
const timeoutId = setTimeout(() => {
|
|
44
|
+
controller.abort(timeoutError);
|
|
45
|
+
}, timeoutMs);
|
|
46
|
+
return {
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
check: () => {
|
|
49
|
+
if (controller.signal.aborted) {
|
|
50
|
+
throw signalReason(controller.signal, timeoutError);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
dispose: () => {
|
|
54
|
+
clearTimeout(timeoutId);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export async function withExtractionDeadline(timeoutMs, label, run) {
|
|
59
|
+
const deadline = createExtractionDeadline(timeoutMs, label);
|
|
60
|
+
try {
|
|
61
|
+
deadline.check();
|
|
62
|
+
return await waitForDeadline(run(deadline), deadline);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
deadline.dispose();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FileHandle } from "node:fs/promises";
|
|
2
|
+
import type { ExtractionDeadline } from "./archive-deadline.js";
|
|
3
|
+
export declare function writeFileHandleFully(params: {
|
|
4
|
+
handle: FileHandle;
|
|
5
|
+
buffer: Buffer;
|
|
6
|
+
bytes: number;
|
|
7
|
+
deadline: ExtractionDeadline;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=archive-file-io.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-file-io.d.ts","sourceRoot":"","sources":["../src/archive-file-io.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,kBAAkB,CAAC;CAC9B,GAAG,OAAO,CAAC,IAAI,CAAC,CAchB"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export async function writeFileHandleFully(params) {
|
|
2
|
+
let offset = 0;
|
|
3
|
+
while (offset < params.bytes) {
|
|
4
|
+
params.deadline.check();
|
|
5
|
+
const { bytesWritten } = await params.handle.write(params.buffer, offset, params.bytes - offset);
|
|
6
|
+
if (bytesWritten <= 0) {
|
|
7
|
+
throw new Error("archive staging write made no progress");
|
|
8
|
+
}
|
|
9
|
+
offset += bytesWritten;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,wBAAwB,GAChC,2BAA2B,GAC3B,qBAAqB,GACrB,+BAA+B,CAAC;AAEpC,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,IAAI,EAAE,wBAAwB,CAAC;gBAEnB,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;CAKpF;AAkCD,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA4BnF;AA6CD,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;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmChB;
|
|
1
|
+
{"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,wBAAwB,GAChC,2BAA2B,GAC3B,qBAAqB,GACrB,+BAA+B,CAAC;AAEpC,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,IAAI,EAAE,wBAAwB,CAAC;gBAEnB,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;CAKpF;AAkCD,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA4BnF;AA6CD,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;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmChB;AAyHD,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;CAC5B,GAAG,OAAO,CAAC,IAAI,CAAC,CAgFhB;AAED,wBAAgB,kCAAkC,CAAC,YAAY,EAAE,MAAM,GAAG,oBAAoB,CAE7F"}
|
package/dist/archive-staging.js
CHANGED
|
@@ -195,6 +195,16 @@ async function assertExtractedFileHasNoHardlinkAlias(params) {
|
|
|
195
195
|
}
|
|
196
196
|
async function removeExtractedDestinationFile(params) {
|
|
197
197
|
const destinationPath = path.join(params.destinationRealDir, params.relPath);
|
|
198
|
+
let stat;
|
|
199
|
+
try {
|
|
200
|
+
stat = await fs.lstat(destinationPath);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!stat.isFile()) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
198
208
|
let resolved;
|
|
199
209
|
try {
|
|
200
210
|
resolved = await fs.realpath(destinationPath);
|
|
@@ -205,7 +215,19 @@ async function removeExtractedDestinationFile(params) {
|
|
|
205
215
|
if (!isPathInside(params.destinationRealDir, resolved)) {
|
|
206
216
|
return;
|
|
207
217
|
}
|
|
208
|
-
|
|
218
|
+
const targetRoot = await root(params.destinationRealDir);
|
|
219
|
+
await targetRoot.remove(params.relPath).catch(() => undefined);
|
|
220
|
+
}
|
|
221
|
+
function assertSafeArchiveStagingPrefix(prefix) {
|
|
222
|
+
if (!prefix ||
|
|
223
|
+
prefix === "." ||
|
|
224
|
+
prefix === ".." ||
|
|
225
|
+
prefix.includes("/") ||
|
|
226
|
+
prefix.includes("\\") ||
|
|
227
|
+
path.basename(prefix) !== prefix) {
|
|
228
|
+
throw new Error("archive staging prefix must be a single path segment");
|
|
229
|
+
}
|
|
230
|
+
return prefix;
|
|
209
231
|
}
|
|
210
232
|
export async function withStagedArchiveDestination(params) {
|
|
211
233
|
const stagingRoot = resolveSecureTempRoot({
|
|
@@ -216,20 +238,34 @@ export async function withStagedArchiveDestination(params) {
|
|
|
216
238
|
if (isPathInside(params.destinationRealDir, stagingRoot)) {
|
|
217
239
|
throw new Error(`archive staging root must be outside destination: ${stagingRoot}`);
|
|
218
240
|
}
|
|
219
|
-
const
|
|
241
|
+
const stagingPrefix = assertSafeArchiveStagingPrefix(params.stagingDirPrefix ?? "fs-safe-archive-");
|
|
242
|
+
const stagingDir = await fs.mkdtemp(path.join(stagingRoot, stagingPrefix));
|
|
243
|
+
const stagingGuard = await createDirectoryIdentityGuard(stagingDir);
|
|
220
244
|
try {
|
|
221
245
|
await fs.chmod(stagingDir, ARCHIVE_STAGING_MODE).catch(() => undefined);
|
|
246
|
+
await assertDirectoryIdentityGuard(stagingGuard);
|
|
222
247
|
return await params.run(stagingDir);
|
|
223
248
|
}
|
|
224
249
|
finally {
|
|
225
|
-
|
|
250
|
+
try {
|
|
251
|
+
await assertDirectoryIdentityGuard(stagingGuard);
|
|
252
|
+
await fs.rm(stagingDir, { recursive: true, force: true }).catch(() => undefined);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// The staging path identity changed; deleting by name could target data
|
|
256
|
+
// outside the private temp tree, so fail closed and leave it for OS cleanup.
|
|
257
|
+
}
|
|
226
258
|
}
|
|
227
259
|
}
|
|
228
260
|
export async function mergeExtractedTreeIntoDestination(params) {
|
|
229
261
|
const targetRoot = await root(params.destinationRealDir);
|
|
262
|
+
const sourceRootGuard = await createDirectoryIdentityGuard(params.sourceDir);
|
|
263
|
+
const sourceRootReal = sourceRootGuard.realPath;
|
|
230
264
|
const walk = async (currentSourceDir) => {
|
|
265
|
+
await assertDirectoryIdentityGuard(sourceRootGuard);
|
|
231
266
|
const entries = await fs.readdir(currentSourceDir, { withFileTypes: true });
|
|
232
267
|
for (const entry of entries) {
|
|
268
|
+
await assertDirectoryIdentityGuard(sourceRootGuard);
|
|
233
269
|
const sourcePath = path.join(currentSourceDir, entry.name);
|
|
234
270
|
const relPath = path.relative(params.sourceDir, sourcePath);
|
|
235
271
|
const originalPath = relPath.split(path.sep).join("/");
|
|
@@ -238,6 +274,10 @@ export async function mergeExtractedTreeIntoDestination(params) {
|
|
|
238
274
|
if (sourceStat.isSymbolicLink()) {
|
|
239
275
|
throw symlinkTraversalError(originalPath);
|
|
240
276
|
}
|
|
277
|
+
const sourceReal = await fs.realpath(sourcePath);
|
|
278
|
+
if (!isPathInside(sourceRootReal, sourceReal)) {
|
|
279
|
+
throw symlinkTraversalError(originalPath);
|
|
280
|
+
}
|
|
241
281
|
if (sourceStat.isDirectory()) {
|
|
242
282
|
await prepareArchiveOutputPath({
|
|
243
283
|
destinationDir: params.destinationDir,
|
package/dist/archive.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AAkBA,OAAO,EAOL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAiBzE,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC/F,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,6BAA6B,EAC7B,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,EACvB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAC3F,OAAO,EACL,kCAAkC,EAClC,iCAAiC,EACjC,4BAA4B,EAC5B,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,8BAA8B,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EACL,2BAA2B,EAC3B,iCAAiC,EACjC,KAAK,mBAAmB,GACzB,MAAM,4BAA4B,CAAC;AAuTpC,wBAAsB,cAAc,CAAC,MAAM,EAAE;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6EhB"}
|