@openclaw/fs-safe 0.12.0 → 0.13.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 +61 -0
- package/README.md +12 -7
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +32 -11
- package/dist/advanced.d.ts +2 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +2 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +6 -2
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +15 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +7 -19
- package/dist/archive-plan.js +1 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +18 -13
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +43 -10
- package/dist/archive-tar-inspect.d.ts.map +1 -1
- package/dist/archive-tar-inspect.js +4 -1
- package/dist/archive-zip-admission.d.ts +1 -1
- package/dist/archive-zip-admission.d.ts.map +1 -1
- package/dist/archive-zip-admission.js +2 -2
- package/dist/archive-zip-directory.d.ts +3 -0
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +13 -2
- package/dist/archive-zip-loader.d.ts +3 -2
- package/dist/archive-zip-loader.d.ts.map +1 -1
- package/dist/archive-zip-loader.js +30 -4
- package/dist/archive-zip-manifest.d.ts +5 -0
- package/dist/archive-zip-manifest.d.ts.map +1 -0
- package/dist/archive-zip-manifest.js +22 -0
- package/dist/archive-zip-names.d.ts +6 -1
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +35 -14
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +3 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +9 -4
- package/dist/darwin-acl.d.ts +4 -0
- package/dist/darwin-acl.d.ts.map +1 -0
- package/dist/darwin-acl.js +24 -0
- package/dist/deny-mutations.d.ts.map +1 -1
- package/dist/deny-mutations.js +8 -2
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +67 -23
- package/dist/directory-entry-path.d.ts +3 -0
- package/dist/directory-entry-path.d.ts.map +1 -0
- package/dist/directory-entry-path.js +21 -0
- package/dist/directory-guard.d.ts +17 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +134 -48
- package/dist/directory-mode-node.d.ts +12 -0
- package/dist/directory-mode-node.d.ts.map +1 -1
- package/dist/directory-mode-node.js +102 -4
- package/dist/effective-uid.d.ts +2 -0
- package/dist/effective-uid.d.ts.map +1 -0
- package/dist/effective-uid.js +25 -0
- package/dist/file-handle-transfer.d.ts.map +1 -1
- package/dist/file-handle-transfer.js +98 -27
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +3 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +36 -6
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +29 -9
- package/dist/file-observation.d.ts +1 -1
- package/dist/file-observation.d.ts.map +1 -1
- package/dist/file-store-boundary.d.ts +6 -2
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +20 -65
- package/dist/file-store-copy-source.d.ts +5 -0
- package/dist/file-store-copy-source.d.ts.map +1 -0
- package/dist/file-store-copy-source.js +31 -0
- package/dist/file-store-path.d.ts.map +1 -1
- package/dist/file-store-path.js +4 -1
- package/dist/file-store-sync-directory.d.ts +16 -0
- package/dist/file-store-sync-directory.d.ts.map +1 -0
- package/dist/file-store-sync-directory.js +349 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +11 -28
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +65 -20
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +3 -2
- package/dist/guarded-mkdir.d.ts +8 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +176 -27
- package/dist/guest.d.ts.map +1 -1
- package/dist/guest.js +4 -1
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +73 -10
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +54 -19
- package/dist/json-durable-queue-ownership.d.ts +6 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +89 -42
- package/dist/json-durable-queue-paths.d.ts +11 -0
- package/dist/json-durable-queue-paths.d.ts.map +1 -0
- package/dist/json-durable-queue-paths.js +42 -0
- package/dist/json-durable-queue-read.d.ts.map +1 -1
- package/dist/json-durable-queue-read.js +2 -0
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +50 -58
- package/dist/json-store.d.ts.map +1 -1
- package/dist/json-store.js +5 -1
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +54 -22
- package/dist/local-file-access.d.ts.map +1 -1
- package/dist/local-file-access.js +4 -0
- package/dist/local-file-descriptor.d.ts +17 -0
- package/dist/local-file-descriptor.d.ts.map +1 -0
- package/dist/local-file-descriptor.js +84 -0
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +35 -7
- package/dist/move-path-cleanup.d.ts +2 -0
- package/dist/move-path-cleanup.d.ts.map +1 -1
- package/dist/move-path-cleanup.js +44 -18
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +31 -6
- package/dist/native-binding.d.ts +14 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-directory-observation.d.ts +17 -0
- package/dist/native-directory-observation.d.ts.map +1 -0
- package/dist/native-directory-observation.js +37 -0
- package/dist/native-parent-admission.d.ts +31 -0
- package/dist/native-parent-admission.d.ts.map +1 -0
- package/dist/native-parent-admission.js +124 -0
- package/dist/native-pinned-write-windows.d.ts +0 -1
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +0 -3
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +309 -40
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +15 -5
- package/dist/overwrite-file-handle.d.ts.map +1 -1
- package/dist/overwrite-file-handle.js +5 -1
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +2 -0
- package/dist/path-policy.d.ts.map +1 -1
- package/dist/path-policy.js +7 -2
- package/dist/path-prefix.d.ts +7 -0
- package/dist/path-prefix.d.ts.map +1 -0
- package/dist/path-prefix.js +82 -0
- package/dist/path-scope-lexical.d.ts.map +1 -1
- package/dist/path-scope-lexical.js +18 -8
- package/dist/path-segment-route.d.ts +7 -0
- package/dist/path-segment-route.d.ts.map +1 -0
- package/dist/path-segment-route.js +24 -0
- package/dist/path-suffix-aliases.d.ts +10 -0
- package/dist/path-suffix-aliases.d.ts.map +1 -0
- package/dist/path-suffix-aliases.js +386 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +16 -5
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +14 -3
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +37 -8
- package/dist/pinned-mutation-admission.d.ts +25 -0
- package/dist/pinned-mutation-admission.d.ts.map +1 -0
- package/dist/pinned-mutation-admission.js +425 -0
- package/dist/pinned-mutation-observation.d.ts +34 -0
- package/dist/pinned-mutation-observation.d.ts.map +1 -0
- package/dist/pinned-mutation-observation.js +142 -0
- package/dist/pinned-mutation-shared-route.d.ts +24 -0
- package/dist/pinned-mutation-shared-route.d.ts.map +1 -0
- package/dist/pinned-mutation-shared-route.js +70 -0
- package/dist/pinned-open.d.ts +6 -0
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +28 -10
- package/dist/pinned-write-types.d.ts +75 -0
- package/dist/pinned-write-types.d.ts.map +1 -0
- package/dist/pinned-write-types.js +1 -0
- package/dist/pinned-write.d.ts +7 -33
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +151 -17
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +2 -0
- package/dist/private-producer-handoff.d.ts +16 -0
- package/dist/private-producer-handoff.d.ts.map +1 -0
- package/dist/private-producer-handoff.js +272 -0
- package/dist/private-temp-workspace.d.ts +2 -39
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +183 -77
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +33 -29
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +56 -45
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +12 -5
- package/dist/replace-file-temp-owner.d.ts +1 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +12 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +24 -24
- package/dist/root-boundary.d.ts +4 -0
- package/dist/root-boundary.d.ts.map +1 -1
- package/dist/root-boundary.js +6 -1
- package/dist/root-context.d.ts +12 -1
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +57 -11
- package/dist/root-directory-creation.d.ts +13 -0
- package/dist/root-directory-creation.d.ts.map +1 -0
- package/dist/root-directory-creation.js +212 -0
- package/dist/root-directory-list.d.ts +16 -4
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +180 -39
- package/dist/root-directory.d.ts +23 -0
- package/dist/root-directory.d.ts.map +1 -0
- package/dist/root-directory.js +141 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +12 -1
- package/dist/root-file-final-admission.d.ts +21 -0
- package/dist/root-file-final-admission.d.ts.map +1 -0
- package/dist/root-file-final-admission.js +83 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +103 -24
- package/dist/root-impl.d.ts +1 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +289 -317
- package/dist/root-move-noreplace.d.ts +14 -0
- package/dist/root-move-noreplace.d.ts.map +1 -0
- package/dist/root-move-noreplace.js +202 -0
- package/dist/root-observed-path.d.ts +9 -0
- package/dist/root-observed-path.d.ts.map +1 -0
- package/dist/root-observed-path.js +95 -0
- package/dist/root-path-errors.d.ts +12 -0
- package/dist/root-path-errors.d.ts.map +1 -0
- package/dist/root-path-errors.js +13 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +53 -20
- package/dist/root-path-observation.d.ts +63 -0
- package/dist/root-path-observation.d.ts.map +1 -0
- package/dist/root-path-observation.js +180 -0
- package/dist/root-path-stat.d.ts +5 -0
- package/dist/root-path-stat.d.ts.map +1 -0
- package/dist/root-path-stat.js +101 -0
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +23 -4
- package/dist/root-path.d.ts +11 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +215 -53
- package/dist/root-paths-lexical.d.ts +9 -0
- package/dist/root-paths-lexical.d.ts.map +1 -0
- package/dist/root-paths-lexical.js +22 -0
- package/dist/root-paths.d.ts +3 -25
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +77 -154
- package/dist/root-read-admission.d.ts +26 -0
- package/dist/root-read-admission.d.ts.map +1 -0
- package/dist/root-read-admission.js +94 -0
- package/dist/root-remove-identity.d.ts +15 -0
- package/dist/root-remove-identity.d.ts.map +1 -0
- package/dist/root-remove-identity.js +89 -0
- package/dist/root-remove-receipt.d.ts +17 -0
- package/dist/root-remove-receipt.d.ts.map +1 -0
- package/dist/root-remove-receipt.js +37 -0
- package/dist/root-remove.d.ts +2 -1
- package/dist/root-remove.d.ts.map +1 -1
- package/dist/root-remove.js +172 -10
- package/dist/root-write-admission.d.ts +68 -0
- package/dist/root-write-admission.d.ts.map +1 -0
- package/dist/root-write-admission.js +322 -0
- package/dist/root-write-compatibility.d.ts +13 -0
- package/dist/root-write-compatibility.d.ts.map +1 -0
- package/dist/root-write-compatibility.js +74 -0
- package/dist/root-write-complete-parent.d.ts +42 -0
- package/dist/root-write-complete-parent.d.ts.map +1 -0
- package/dist/root-write-complete-parent.js +195 -0
- package/dist/root-write-publication.d.ts +24 -0
- package/dist/root-write-publication.d.ts.map +1 -0
- package/dist/root-write-publication.js +85 -0
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +6 -4
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +62 -24
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +18 -13
- package/dist/secret-read-policy.d.ts.map +1 -1
- package/dist/secret-read-policy.js +5 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +92 -7
- package/dist/secure-temp-dir.d.ts +6 -3
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +121 -101
- package/dist/secure-temp-repair.d.ts +35 -0
- package/dist/secure-temp-repair.d.ts.map +1 -0
- package/dist/secure-temp-repair.js +106 -0
- package/dist/sibling-staged-file.d.ts +3 -2
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +179 -55
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +27 -13
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +44 -14
- package/dist/sidecar-lock-root.d.ts.map +1 -1
- package/dist/sidecar-lock-root.js +4 -2
- package/dist/staged-directory.d.ts +14 -5
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +54 -3
- package/dist/standalone-publication-path.d.ts +2 -0
- package/dist/standalone-publication-path.d.ts.map +1 -0
- package/dist/standalone-publication-path.js +6 -0
- package/dist/stat-observation.d.ts +11 -0
- package/dist/stat-observation.d.ts.map +1 -0
- package/dist/stat-observation.js +64 -0
- package/dist/strict-file-identity.d.ts.map +1 -1
- package/dist/strict-file-identity.js +39 -2
- package/dist/symlink-parents.d.ts.map +1 -1
- package/dist/symlink-parents.js +7 -2
- package/dist/temp-target.d.ts +2 -0
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +130 -3
- package/dist/temp-workspace-admission.d.ts +22 -0
- package/dist/temp-workspace-admission.d.ts.map +1 -0
- package/dist/temp-workspace-admission.js +374 -0
- package/dist/temp-workspace-child-admission.d.ts +13 -0
- package/dist/temp-workspace-child-admission.d.ts.map +1 -0
- package/dist/temp-workspace-child-admission.js +95 -0
- package/dist/temp-workspace-descriptor.d.ts +48 -0
- package/dist/temp-workspace-descriptor.d.ts.map +1 -0
- package/dist/temp-workspace-descriptor.js +363 -0
- package/dist/temp-workspace-identity.d.ts +15 -0
- package/dist/temp-workspace-identity.d.ts.map +1 -0
- package/dist/temp-workspace-identity.js +41 -0
- package/dist/temp-workspace-owner.d.ts +7 -6
- package/dist/temp-workspace-owner.d.ts.map +1 -1
- package/dist/temp-workspace-owner.js +121 -61
- package/dist/temp-workspace-permissions.d.ts +4 -0
- package/dist/temp-workspace-permissions.d.ts.map +1 -0
- package/dist/temp-workspace-permissions.js +32 -0
- package/dist/temp-workspace-types.d.ts +40 -0
- package/dist/temp-workspace-types.d.ts.map +1 -0
- package/dist/temp-workspace-types.js +1 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/temp.js +1 -1
- package/dist/test-hooks.d.ts +7 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/text-atomic.d.ts.map +1 -1
- package/dist/text-atomic.js +3 -1
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +54 -8
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +9 -6
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +5 -0
- package/dist/windows-path-alias.d.ts +39 -0
- package/dist/windows-path-alias.d.ts.map +1 -0
- package/dist/windows-path-alias.js +153 -0
- package/docs/advanced.md +25 -0
- package/docs/archive.md +23 -2
- package/docs/atomic.md +27 -3
- package/docs/copy.md +3 -0
- package/docs/durability.md +14 -0
- package/docs/errors.md +14 -6
- package/docs/file-store.md +24 -3
- package/docs/filename.md +14 -7
- package/docs/install-path.md +2 -2
- package/docs/install.md +8 -7
- package/docs/json-store.md +5 -1
- package/docs/json.md +11 -0
- package/docs/mutation-policy-proof.md +65 -0
- package/docs/native-helper.md +26 -9
- package/docs/native.md +32 -8
- package/docs/output.md +33 -11
- package/docs/path-prefix.md +64 -0
- package/docs/path-suffix-aliases.md +159 -0
- package/docs/path.md +11 -0
- package/docs/private-file-store.md +14 -0
- package/docs/public-api.md +9 -0
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +8 -1
- package/docs/root.md +18 -3
- package/docs/secret-file.md +3 -0
- package/docs/secure-file.md +6 -2
- package/docs/security-model.md +75 -8
- package/docs/sidecar-lock.md +28 -1
- package/docs/store.md +5 -1
- package/docs/temp.md +260 -36
- package/docs/test-hooks.md +14 -0
- package/docs/writing.md +38 -8
- package/package.json +10 -10
|
@@ -5,38 +5,43 @@ import { assertNoUnsafeDeviceReadPath } from "./device-path.js";
|
|
|
5
5
|
import { FsSafeError } from "./errors.js";
|
|
6
6
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
7
7
|
import { realpathSync } from "./realpath.js";
|
|
8
|
-
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
8
|
+
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
9
9
|
import { assertSecretFilePreview, resolveSecretReadPolicy, secretPathErrorCode, secretReadError, trimSecretFileContent, } from "./secret-read-policy.js";
|
|
10
|
+
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
10
11
|
export async function readSecretFile(filePath, label, options = {}) {
|
|
11
12
|
const { resolvedPath, maxBytes } = resolveSecretReadPolicy(filePath, label, options);
|
|
12
|
-
|
|
13
|
-
const stat = options.rejectSymlink
|
|
14
|
-
? fsSync.lstatSync(resolvedPath, { bigint: true })
|
|
15
|
-
: fsSync.statSync(resolvedPath, { bigint: true });
|
|
16
|
-
if (options.rejectSymlink && stat.isSymbolicLink()) {
|
|
17
|
-
throw new FsSafeError("symlink", symlinkMessage);
|
|
18
|
-
}
|
|
19
|
-
return stat;
|
|
20
|
-
}
|
|
13
|
+
let rejectSymlink;
|
|
21
14
|
let previewStat;
|
|
22
15
|
try {
|
|
23
16
|
assertNoUnsafeDeviceReadPath(resolvedPath);
|
|
24
|
-
|
|
17
|
+
rejectSymlink = Boolean(options.rejectSymlink);
|
|
18
|
+
previewStat = inspectFileIdentitySync(() => inspectInput(`${label} file at ${resolvedPath} must not be a symlink.`));
|
|
25
19
|
}
|
|
26
20
|
catch (error) {
|
|
27
21
|
throw secretReadError(error instanceof FsSafeError ? error.code : secretPathErrorCode(error), "inspect", label, resolvedPath, error);
|
|
28
22
|
}
|
|
29
|
-
|
|
23
|
+
function inspectInput(symlinkMessage) {
|
|
24
|
+
const stat = rejectSymlink
|
|
25
|
+
? fsSync.lstatSync(resolvedPath, { bigint: true })
|
|
26
|
+
: fsSync.statSync(resolvedPath, { bigint: true });
|
|
27
|
+
if (rejectSymlink && stat.isSymbolicLink()) {
|
|
28
|
+
throw new FsSafeError("symlink", symlinkMessage);
|
|
29
|
+
}
|
|
30
|
+
return stat;
|
|
31
|
+
}
|
|
32
|
+
const rejectHardlinks = options.rejectHardlinks !== false;
|
|
33
|
+
assertSecretFilePreview(previewStat, label, resolvedPath, maxBytes, rejectHardlinks);
|
|
30
34
|
let handle;
|
|
31
35
|
let raw;
|
|
32
36
|
try {
|
|
33
37
|
const realPath = realpathSync.native(resolvedPath);
|
|
38
|
+
assertNoWindowsPathAlias(realPath, "filesystem", `${label} file path uses a Windows filesystem namespace alias`);
|
|
34
39
|
assertNoUnsafeDeviceReadPath(realPath);
|
|
35
40
|
handle = await fs.open(realPath, resolveReadOpenFlags());
|
|
36
41
|
const openedHandle = handle;
|
|
37
42
|
const openedStat = await inspectFileIdentity(async () => {
|
|
38
43
|
const stat = fsSync.fstatSync(openedHandle.fd, { bigint: true });
|
|
39
|
-
if (!stat.isFile() || (
|
|
44
|
+
if (!stat.isFile() || (rejectHardlinks && stat.nlink > 1n)) {
|
|
40
45
|
throw new FsSafeError("path-mismatch", "security validation failed");
|
|
41
46
|
}
|
|
42
47
|
return stat;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-read-policy.d.ts","sourceRoot":"","sources":["../src/secret-read-policy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"secret-read-policy.d.ts","sourceRoot":"","sources":["../src/secret-read-policy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAGhE,eAAO,MAAM,6BAA6B,QAAY,CAAC;AAEvD,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAGnE;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,SAAS,GAAG,MAAM,EAC1B,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,OAAO,GACb,WAAW,CAGb;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,OAAO,GACvB,IAAI,CAUN;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAM9F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG;IACxG,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAYA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
2
2
|
import { resolveHomeRelativePath } from "./home-dir.js";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
4
5
|
export const DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024;
|
|
5
6
|
export function secretPathErrorCode(error) {
|
|
6
7
|
const code = error.code;
|
|
@@ -29,10 +30,13 @@ export function trimSecretFileContent(raw, label, resolvedPath) {
|
|
|
29
30
|
return secret;
|
|
30
31
|
}
|
|
31
32
|
export function resolveSecretReadPolicy(filePath, label, options) {
|
|
32
|
-
const
|
|
33
|
+
const trimmedPath = filePath.trim();
|
|
34
|
+
assertNoWindowsPathAlias(trimmedPath, "filesystem", `${label} file path uses a Windows filesystem namespace alias`);
|
|
35
|
+
const resolvedPath = resolveHomeRelativePath(trimmedPath);
|
|
33
36
|
if (!resolvedPath) {
|
|
34
37
|
throw new FsSafeError("invalid-path", `${label} file path is empty.`, { cause: undefined });
|
|
35
38
|
}
|
|
39
|
+
assertNoWindowsPathAlias(resolvedPath, "filesystem", `${label} file path uses a Windows filesystem namespace alias`);
|
|
36
40
|
const maxBytes = normalizeMaxBytes(options.maxBytes, {
|
|
37
41
|
defaultValue: DEFAULT_SECRET_FILE_MAX_BYTES,
|
|
38
42
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secure-file.d.ts","sourceRoot":"","sources":["../src/secure-file.ts"],"names":[],"mappings":"AAAA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"secure-file.d.ts","sourceRoot":"","sources":["../src/secure-file.ts"],"names":[],"mappings":"AAAA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAe/D,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;AAU1B,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,WAAW,CAAC,EAAE,2BAA2B,CAAC;IAC1C,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,EAAE,CAAC,EAAE,mBAAmB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,sBAAsB,CAAC;AAE7D,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;IACZ,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;AAgTF,wBAAsB,cAAc,CAClC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAgC/B"}
|
package/dist/secure-file.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { readFileHandleBounded } from "./bounded-read.js";
|
|
5
5
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
6
6
|
import { assertNoUnsafeDeviceReadPath } from "./device-path.js";
|
|
7
|
+
import { resolveEffectiveUid } from "./effective-uid.js";
|
|
7
8
|
import { FsSafeError } from "./errors.js";
|
|
8
9
|
import { isWindowsDriveLetterPath, isWindowsNetworkPath } from "./local-file-access.js";
|
|
9
10
|
import { isPathInside, isSymlinkOpenError } from "./path.js";
|
|
@@ -14,6 +15,75 @@ import { inspectPathPermissions, isGroupReadable, isGroupWritable, isWorldReadab
|
|
|
14
15
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
15
16
|
import { inspectSecureWindowsDescriptor } from "./secure-file-windows.js";
|
|
16
17
|
import { scheduleTimeout } from "./timing.js";
|
|
18
|
+
import { anchorWindowsDriveRelativePath, assertNoWindowsPathAlias, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
19
|
+
function snapshotTrustedDirs(dirs) {
|
|
20
|
+
if (dirs === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
if (!Array.isArray(dirs)) {
|
|
23
|
+
throw new FsSafeError("invalid-path", "trustedDirs must be an array of path strings.");
|
|
24
|
+
}
|
|
25
|
+
const trustedDirs = [];
|
|
26
|
+
const length = dirs.length;
|
|
27
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > 0xffff_ffff) {
|
|
28
|
+
throw new FsSafeError("invalid-path", "trustedDirs must have a valid array length.");
|
|
29
|
+
}
|
|
30
|
+
for (let index = 0; index < length; index += 1) {
|
|
31
|
+
if (!Object.hasOwn(dirs, index)) {
|
|
32
|
+
throw new FsSafeError("invalid-path", "trustedDirs must not contain sparse entries.");
|
|
33
|
+
}
|
|
34
|
+
const dir = dirs[index];
|
|
35
|
+
if (typeof dir !== "string" || dir.includes("\0")) {
|
|
36
|
+
throw new FsSafeError("invalid-path", "trustedDirs must contain only path strings without null bytes.");
|
|
37
|
+
}
|
|
38
|
+
// Admit the raw suffix before normalization can erase an alternate stream,
|
|
39
|
+
// while retaining Node's per-drive cwd semantics for D:credentials.
|
|
40
|
+
const anchored = anchorWindowsDriveRelativePath(dir);
|
|
41
|
+
assertNoWindowsPathAlias(anchored, "filesystem", "trusted directory uses a Windows filesystem namespace alias");
|
|
42
|
+
const resolved = resolvePathPreservingWindowsRoot(anchored);
|
|
43
|
+
assertNoWindowsPathAlias(resolved, "filesystem", "trusted directory uses a Windows filesystem namespace alias");
|
|
44
|
+
trustedDirs.push(resolved);
|
|
45
|
+
}
|
|
46
|
+
return trustedDirs;
|
|
47
|
+
}
|
|
48
|
+
function snapshotPermissionEnv(env) {
|
|
49
|
+
if (env === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
const snapshot = { ...env };
|
|
52
|
+
// Windows command lookup first reads these exact names, even when inherited
|
|
53
|
+
// or non-enumerable, then considers only enumerable own case variants.
|
|
54
|
+
// Reuse copied values so enumerable getters are not invoked a second time.
|
|
55
|
+
if (!Object.hasOwn(snapshot, "SystemRoot"))
|
|
56
|
+
snapshot.SystemRoot = env.SystemRoot;
|
|
57
|
+
if (!Object.hasOwn(snapshot, "WINDIR"))
|
|
58
|
+
snapshot.WINDIR = env.WINDIR;
|
|
59
|
+
return snapshot;
|
|
60
|
+
}
|
|
61
|
+
function snapshotSecureFileOptions(options, io, maxBytes) {
|
|
62
|
+
const { filePath, label, trust, permissions, inject } = options;
|
|
63
|
+
const env = inject?.env;
|
|
64
|
+
return {
|
|
65
|
+
filePath,
|
|
66
|
+
label,
|
|
67
|
+
trust: {
|
|
68
|
+
trustedDirs: snapshotTrustedDirs(trust?.trustedDirs),
|
|
69
|
+
allowSymlink: trust?.allowSymlink,
|
|
70
|
+
allowNetworkPath: trust?.allowNetworkPath,
|
|
71
|
+
},
|
|
72
|
+
permissions: {
|
|
73
|
+
allowInsecure: permissions?.allowInsecure,
|
|
74
|
+
allowReadableByOthers: permissions?.allowReadableByOthers,
|
|
75
|
+
},
|
|
76
|
+
inject: {
|
|
77
|
+
platform: inject?.platform,
|
|
78
|
+
env: snapshotPermissionEnv(env),
|
|
79
|
+
exec: inject?.exec,
|
|
80
|
+
},
|
|
81
|
+
io: {
|
|
82
|
+
maxBytes,
|
|
83
|
+
timeoutMs: io?.timeoutMs,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
17
87
|
function isAbsolutePathname(value) {
|
|
18
88
|
return (path.isAbsolute(value) ||
|
|
19
89
|
(process.platform === "win32" &&
|
|
@@ -82,6 +152,7 @@ async function openSecureHandle(options, maxBytes) {
|
|
|
82
152
|
return pathStat;
|
|
83
153
|
}, openedIdentity);
|
|
84
154
|
const realPath = realpathSync.native(options.filePath);
|
|
155
|
+
assertNoWindowsPathAlias(realPath, "filesystem", `${label(options)} resolved path uses a Windows filesystem namespace alias`);
|
|
85
156
|
await inspectFileIdentity(() => {
|
|
86
157
|
const realPathStat = fsSync.statSync(realPath, { bigint: true });
|
|
87
158
|
assertNotHardlinked(options, realPathStat);
|
|
@@ -102,13 +173,15 @@ async function assertTrustedDirs(options, realPath) {
|
|
|
102
173
|
return;
|
|
103
174
|
}
|
|
104
175
|
const trusted = await Promise.all(options.trust.trustedDirs.map(async (dir) => {
|
|
105
|
-
|
|
176
|
+
let realPath;
|
|
106
177
|
try {
|
|
107
|
-
|
|
178
|
+
realPath = realpathSync.native(dir);
|
|
108
179
|
}
|
|
109
180
|
catch {
|
|
110
|
-
return
|
|
181
|
+
return dir;
|
|
111
182
|
}
|
|
183
|
+
assertNoWindowsPathAlias(realPath, "filesystem", "trusted directory uses a Windows filesystem namespace alias");
|
|
184
|
+
return realPath;
|
|
112
185
|
}));
|
|
113
186
|
if (!trusted.some((dir) => isPathInside(dir, realPath))) {
|
|
114
187
|
throw new FsSafeError("outside-workspace", `${label(options)} is outside trustedDirs: ${realPath}`);
|
|
@@ -163,10 +236,19 @@ async function assertSecurePermissions(options, stat, realPath, identity, fd) {
|
|
|
163
236
|
if (writableByOthers || (!options.permissions?.allowReadableByOthers && readableByOthers)) {
|
|
164
237
|
throw new FsSafeError("insecure-permissions", `${label(options)} permissions are too open: ${realPath}`);
|
|
165
238
|
}
|
|
166
|
-
if (platform !== "win32"
|
|
167
|
-
|
|
239
|
+
if (platform !== "win32") {
|
|
240
|
+
let uid;
|
|
241
|
+
try {
|
|
242
|
+
uid = resolveEffectiveUid();
|
|
243
|
+
}
|
|
244
|
+
catch (cause) {
|
|
245
|
+
throw new FsSafeError("permission-unverified", `${label(options)} owner identity could not be verified for the effective user: ${realPath}`, { cause });
|
|
246
|
+
}
|
|
247
|
+
if (uid === undefined || !Number.isSafeInteger(stat.uid) || stat.uid < 0) {
|
|
248
|
+
throw new FsSafeError("permission-unverified", `${label(options)} owner identity could not be verified for the effective user: ${realPath}`);
|
|
249
|
+
}
|
|
168
250
|
if (stat.uid !== uid) {
|
|
169
|
-
throw new FsSafeError("not-owned", `${label(options)} must be owned by the
|
|
251
|
+
throw new FsSafeError("not-owned", `${label(options)} must be owned by the effective user (uid=${uid}): ${realPath}`);
|
|
170
252
|
}
|
|
171
253
|
}
|
|
172
254
|
return permissions;
|
|
@@ -193,7 +275,10 @@ async function readHandleWithTimeout(handle, timeoutMs, maxBytes) {
|
|
|
193
275
|
}
|
|
194
276
|
}
|
|
195
277
|
export async function readSecureFile(options) {
|
|
196
|
-
const
|
|
278
|
+
const io = options.io;
|
|
279
|
+
const maxBytes = normalizeMaxBytes(io?.maxBytes);
|
|
280
|
+
options = snapshotSecureFileOptions(options, io, maxBytes);
|
|
281
|
+
assertNoWindowsPathAlias(options.filePath, "filesystem", `${label(options)} path uses a Windows filesystem namespace alias`);
|
|
197
282
|
const opened = await openSecureHandle(options, maxBytes);
|
|
198
283
|
try {
|
|
199
284
|
await assertTrustedDirs(options, opened.realPath);
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import { type SecureTempRootDescriptorAdapter } from "./secure-temp-repair.js";
|
|
2
|
+
export type { SecureTempRootDescriptorAdapter } from "./secure-temp-repair.js";
|
|
1
3
|
type SecureDirStat = {
|
|
2
4
|
isDirectory(): boolean;
|
|
3
5
|
isSymbolicLink(): boolean;
|
|
4
|
-
mode?: number;
|
|
5
|
-
uid?: number;
|
|
6
|
+
mode?: number | bigint;
|
|
7
|
+
uid?: number | bigint;
|
|
6
8
|
};
|
|
7
9
|
export type ResolveSecureTempRootOptions = {
|
|
8
10
|
accessSync?: (path: string, mode?: number) => void;
|
|
11
|
+
/** @deprecated Unused. Repairs require descriptor-bound chmod. */
|
|
9
12
|
chmodSync?: (path: string, mode: number) => void;
|
|
13
|
+
descriptor?: SecureTempRootDescriptorAdapter;
|
|
10
14
|
fallbackPrefix: string;
|
|
11
15
|
getuid?: () => number | undefined;
|
|
12
16
|
lstatSync?: (path: string) => SecureDirStat;
|
|
@@ -23,5 +27,4 @@ export type ResolveSecureTempRootOptions = {
|
|
|
23
27
|
warningPrefix?: string;
|
|
24
28
|
};
|
|
25
29
|
export declare function resolveSecureTempRoot(options: ResolveSecureTempRootOptions): string;
|
|
26
|
-
export {};
|
|
27
30
|
//# sourceMappingURL=secure-temp-dir.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secure-temp-dir.d.ts","sourceRoot":"","sources":["../src/secure-temp-dir.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"secure-temp-dir.d.ts","sourceRoot":"","sources":["../src/secure-temp-dir.ts"],"names":[],"mappings":"AAOA,OAAO,EAKL,KAAK,+BAA+B,EACrC,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,+BAA+B,EAAE,MAAM,yBAAyB,CAAC;AAI/E,KAAK,aAAa,GAAG;IACnB,WAAW,IAAI,OAAO,CAAC;IACvB,cAAc,IAAI,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,kEAAkE;IAClE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,UAAU,CAAC,EAAE,+BAA+B,CAAC;IAC7C,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAClC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,aAAa,CAAC;IAC5C,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAWF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,MAAM,CA6LnF"}
|
package/dist/secure-temp-dir.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { tmpdir as getOsTmpDir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { directoryEntryPath } from "./directory-entry-path.js";
|
|
4
5
|
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
5
6
|
import { assertSafePathSegment } from "./safe-path-segment.js";
|
|
7
|
+
import { assertNoWindowsPathAlias, pathForWindowsFilesystem } from "./windows-path-alias.js";
|
|
8
|
+
import { captureSecureTempRepairAdapter, repairSecureTempDirectory, secureTempDirectoryReceipt, } from "./secure-temp-repair.js";
|
|
6
9
|
function isNodeErrorWithCode(err, code) {
|
|
7
10
|
return (typeof err === "object" &&
|
|
8
11
|
err !== null &&
|
|
@@ -10,152 +13,169 @@ function isNodeErrorWithCode(err, code) {
|
|
|
10
13
|
err.code === code);
|
|
11
14
|
}
|
|
12
15
|
export function resolveSecureTempRoot(options) {
|
|
13
|
-
const fallbackPrefix =
|
|
16
|
+
const { fallbackPrefix: prefix, accessSync: suppliedAccess, lstatSync: suppliedLstat, mkdirSync: suppliedMkdir, descriptor: suppliedDescriptor, getuid: suppliedGetuid, tmpdir: suppliedTmpdir, platform: suppliedPlatform, preferredDir, skipPreferredOnWindows, warn: suppliedWarn, warningPrefix: suppliedWarningPrefix, unsafeFallbackLabel: suppliedFallbackLabel, } = options;
|
|
17
|
+
const platform = suppliedPlatform ?? process.platform;
|
|
18
|
+
const windows = process.platform === "win32" || platform === "win32";
|
|
19
|
+
// A platform adapter cannot disable the host's Windows pathname policy.
|
|
20
|
+
const admissionPlatform = process.platform === "win32" ? process.platform : platform;
|
|
21
|
+
const capturedDescriptor = captureSecureTempRepairAdapter(suppliedDescriptor, suppliedAccess !== undefined || suppliedLstat !== undefined || suppliedMkdir !== undefined);
|
|
22
|
+
const descriptor = windows ? undefined : capturedDescriptor;
|
|
23
|
+
const hostLstat = fs.lstatSync;
|
|
24
|
+
const hostAccess = fs.accessSync;
|
|
25
|
+
const hostMkdir = fs.mkdirSync;
|
|
26
|
+
const hostGetuid = process.getuid;
|
|
27
|
+
const accessSync = suppliedAccess ?? (process.platform === "win32"
|
|
28
|
+
? (candidate, mode) => hostAccess(pathForWindowsFilesystem(candidate), mode)
|
|
29
|
+
: hostAccess);
|
|
30
|
+
const lstatSync = descriptor
|
|
31
|
+
? (candidate) => descriptor.lstatSync(candidate, { bigint: true })
|
|
32
|
+
: suppliedLstat ?? ((candidate) => windows
|
|
33
|
+
? hostLstat(pathForWindowsFilesystem(candidate))
|
|
34
|
+
: hostLstat(candidate, { bigint: true }));
|
|
35
|
+
// Virtual observations cannot authorize host creation. A supplied mkdir hook
|
|
36
|
+
// is explicit authority; chmodSync is deprecated and deliberately not read.
|
|
37
|
+
const mkdirSync = suppliedMkdir ?? (suppliedLstat !== undefined || suppliedAccess !== undefined || suppliedDescriptor !== undefined
|
|
38
|
+
? undefined
|
|
39
|
+
: (directory, mkdirOptions) => hostMkdir(recursiveMkdirPath(pathForWindowsFilesystem(directory)), mkdirOptions));
|
|
40
|
+
const warn = suppliedWarn ?? console.warn.bind(console);
|
|
41
|
+
const warningPrefix = suppliedWarningPrefix ?? "[fs-safe]";
|
|
42
|
+
const unsafeFallbackLabel = suppliedFallbackLabel ?? "secure temp dir";
|
|
43
|
+
const tmpdir = typeof suppliedTmpdir === "function" ? suppliedTmpdir : getOsTmpDir;
|
|
44
|
+
const getuid = suppliedGetuid ?? (() => {
|
|
45
|
+
try {
|
|
46
|
+
return typeof hostGetuid === "function" ? hostGetuid.call(process) : undefined;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const fallbackPrefix = assertSafePathSegment(prefix, {
|
|
14
53
|
allowDotPrefix: true,
|
|
15
54
|
label: "fallback temp prefix",
|
|
16
55
|
});
|
|
17
56
|
const TMP_DIR_ACCESS_MODE = fs.constants.W_OK | fs.constants.X_OK;
|
|
18
|
-
const accessSync = options.accessSync ?? fs.accessSync;
|
|
19
|
-
const chmodSync = options.chmodSync ?? fs.chmodSync;
|
|
20
|
-
const lstatSync = options.lstatSync ?? fs.lstatSync;
|
|
21
|
-
const mkdirSync = options.mkdirSync ?? ((directory, mkdirOptions) => fs.mkdirSync(recursiveMkdirPath(directory), mkdirOptions));
|
|
22
|
-
const warn = options.warn ?? ((message) => console.warn(message));
|
|
23
|
-
const warningPrefix = options.warningPrefix ?? "[fs-safe]";
|
|
24
|
-
const unsafeFallbackLabel = options.unsafeFallbackLabel ?? "secure temp dir";
|
|
25
|
-
const getuid = options.getuid ??
|
|
26
|
-
(() => {
|
|
27
|
-
try {
|
|
28
|
-
return typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
const tmpdir = typeof options.tmpdir === "function" ? options.tmpdir : getOsTmpDir;
|
|
35
|
-
const platform = options.platform ?? process.platform;
|
|
36
57
|
const uid = getuid();
|
|
58
|
+
if (uid !== undefined && (!Number.isSafeInteger(uid) || uid < 0)) {
|
|
59
|
+
throw new Error("Secure temp directory user identity is invalid.");
|
|
60
|
+
}
|
|
61
|
+
if (preferredDir !== undefined) {
|
|
62
|
+
assertNoWindowsPathAlias(preferredDir, "filesystem", "preferred temp directory uses a Windows filesystem namespace alias", admissionPlatform);
|
|
63
|
+
}
|
|
37
64
|
const isSecureDirForUser = (st) => {
|
|
38
|
-
if (uid === undefined)
|
|
65
|
+
if (windows || uid === undefined)
|
|
39
66
|
return true;
|
|
40
|
-
}
|
|
41
|
-
if (
|
|
67
|
+
const { uid: owner, mode } = st;
|
|
68
|
+
if (owner !== undefined && owner !== uid && owner !== BigInt(uid))
|
|
42
69
|
return false;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
return true;
|
|
70
|
+
if (typeof mode === "bigint")
|
|
71
|
+
return mode >= 0n && (mode & 18n) === 0n;
|
|
72
|
+
return mode === undefined || (Number.isSafeInteger(mode) && mode >= 0 && (mode & 0o022) === 0);
|
|
48
73
|
};
|
|
49
74
|
const fallback = () => {
|
|
50
75
|
const base = tmpdir();
|
|
76
|
+
assertNoWindowsPathAlias(base, "filesystem", "system temp directory uses a Windows filesystem namespace alias", admissionPlatform);
|
|
51
77
|
const suffix = uid === undefined ? fallbackPrefix : `${fallbackPrefix}-${uid}`;
|
|
52
78
|
const joiner = platform === "win32" ? path.win32.join : path.join;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return st.isDirectory() && !st.isSymbolicLink() && isSecureDirForUser(st);
|
|
79
|
+
const fallbackPath = joiner(base, suffix);
|
|
80
|
+
assertNoWindowsPathAlias(fallbackPath, "filesystem", "fallback temp directory uses a Windows filesystem namespace alias", admissionPlatform);
|
|
81
|
+
return fallbackPath;
|
|
57
82
|
};
|
|
58
83
|
const resolveDirState = (candidatePath) => {
|
|
84
|
+
assertNoWindowsPathAlias(candidatePath, "filesystem", "temp directory uses a Windows filesystem namespace alias", admissionPlatform);
|
|
85
|
+
let candidate;
|
|
59
86
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
87
|
+
candidate = lstatSync(candidatePath);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return { kind: isNodeErrorWithCode(error, "ENOENT") ? "missing" : "invalid", error };
|
|
91
|
+
}
|
|
92
|
+
let receipt;
|
|
93
|
+
try {
|
|
94
|
+
if (descriptor && uid !== undefined) {
|
|
95
|
+
receipt = secureTempDirectoryReceipt(candidate, uid);
|
|
96
|
+
}
|
|
97
|
+
if (receipt ? (receipt.mode & 18n) !== 0n :
|
|
98
|
+
candidate.isDirectory() !== true || candidate.isSymbolicLink() !== false || !isSecureDirForUser(candidate)) {
|
|
99
|
+
return { kind: "invalid", receipt };
|
|
63
100
|
}
|
|
64
101
|
accessSync(candidatePath, TMP_DIR_ACCESS_MODE);
|
|
65
|
-
return "available";
|
|
102
|
+
return { kind: "available", receipt };
|
|
66
103
|
}
|
|
67
|
-
catch (
|
|
68
|
-
|
|
69
|
-
return "missing";
|
|
70
|
-
}
|
|
71
|
-
return "invalid";
|
|
104
|
+
catch (error) {
|
|
105
|
+
return { kind: "invalid", receipt, error };
|
|
72
106
|
}
|
|
73
107
|
};
|
|
74
|
-
const
|
|
108
|
+
const tryRepair = (candidatePath, state, finalize = false) => {
|
|
109
|
+
assertNoWindowsPathAlias(candidatePath, "filesystem", "temp directory uses a Windows filesystem namespace alias", admissionPlatform);
|
|
110
|
+
if (!descriptor || uid === undefined || !state.receipt) {
|
|
111
|
+
return finalize ? { kind: "invalid", error: new Error("Secure temp descriptor finalization is unavailable.") } : state;
|
|
112
|
+
}
|
|
75
113
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
if (uid !== undefined && typeof st.uid === "number" && st.uid !== uid) {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
if (typeof st.mode !== "number") {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
if ((st.mode & 0o022) === 0) {
|
|
87
|
-
return resolveDirState(candidatePath) === "available";
|
|
88
|
-
}
|
|
89
|
-
try {
|
|
90
|
-
chmodSync(candidatePath, 0o700);
|
|
91
|
-
}
|
|
92
|
-
catch (chmodErr) {
|
|
93
|
-
if (isNodeErrorWithCode(chmodErr, "EPERM") ||
|
|
94
|
-
isNodeErrorWithCode(chmodErr, "EACCES") ||
|
|
95
|
-
isNodeErrorWithCode(chmodErr, "ENOENT")) {
|
|
96
|
-
return resolveDirState(candidatePath) === "available";
|
|
97
|
-
}
|
|
98
|
-
throw chmodErr;
|
|
99
|
-
}
|
|
100
|
-
warn(`${warningPrefix} tightened permissions on temp dir: ${candidatePath}`);
|
|
101
|
-
return resolveDirState(candidatePath) === "available";
|
|
114
|
+
repairSecureTempDirectory(candidatePath, state.receipt, uid, descriptor, () => accessSync(candidatePath, TMP_DIR_ACCESS_MODE), () => warn(`${warningPrefix} tightened permissions on temp dir: ${candidatePath}`), finalize);
|
|
115
|
+
return { kind: "available" };
|
|
102
116
|
}
|
|
103
|
-
catch {
|
|
104
|
-
return
|
|
117
|
+
catch (error) {
|
|
118
|
+
return { kind: "invalid", error };
|
|
105
119
|
}
|
|
106
120
|
};
|
|
107
121
|
const ensureTrustedFallbackDir = () => {
|
|
108
122
|
const fallbackPath = fallback();
|
|
109
|
-
const
|
|
110
|
-
|
|
123
|
+
const entryPath = directoryEntryPath(fallbackPath, windows ? "win32" : platform);
|
|
124
|
+
let state = resolveDirState(entryPath);
|
|
125
|
+
let finalize = false;
|
|
126
|
+
if (state.kind === "available")
|
|
111
127
|
return fallbackPath;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
128
|
+
if (state.kind === "missing") {
|
|
129
|
+
try {
|
|
130
|
+
if (!mkdirSync)
|
|
131
|
+
throw new Error("Secure temp creation requires an explicit filesystem adapter.");
|
|
132
|
+
const created = mkdirSync(entryPath, { recursive: true, mode: 0o700 });
|
|
133
|
+
finalize = !windows && (suppliedMkdir !== undefined || created !== undefined);
|
|
116
134
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
throw new Error(`Unable to create fallback ${unsafeFallbackLabel}: ${fallbackPath}`);
|
|
135
|
+
catch (cause) {
|
|
136
|
+
throw new Error(`Unable to create fallback ${unsafeFallbackLabel}: ${fallbackPath}`, { cause });
|
|
137
|
+
}
|
|
138
|
+
// A concurrent mkdir winner has no implicit ownership or mode authority.
|
|
139
|
+
state = resolveDirState(entryPath);
|
|
140
|
+
if (state.kind === "available" && !finalize)
|
|
141
|
+
return fallbackPath;
|
|
125
142
|
}
|
|
126
|
-
|
|
127
|
-
|
|
143
|
+
state = tryRepair(entryPath, state, finalize);
|
|
144
|
+
if (state.kind !== "available") {
|
|
145
|
+
throw new Error(`Unsafe fallback ${unsafeFallbackLabel}: ${fallbackPath}`, { cause: state.error });
|
|
128
146
|
}
|
|
129
147
|
return fallbackPath;
|
|
130
148
|
};
|
|
131
|
-
if (
|
|
149
|
+
if (skipPreferredOnWindows === true && platform === "win32") {
|
|
132
150
|
return ensureTrustedFallbackDir();
|
|
133
151
|
}
|
|
134
|
-
if (!
|
|
152
|
+
if (!preferredDir) {
|
|
135
153
|
return ensureTrustedFallbackDir();
|
|
136
154
|
}
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
155
|
+
const entryPath = directoryEntryPath(preferredDir, windows ? "win32" : platform);
|
|
156
|
+
const existingPreferredState = resolveDirState(entryPath);
|
|
157
|
+
if (existingPreferredState.kind === "available") {
|
|
158
|
+
return preferredDir;
|
|
140
159
|
}
|
|
141
|
-
if (existingPreferredState === "invalid") {
|
|
142
|
-
if (
|
|
143
|
-
return
|
|
160
|
+
if (existingPreferredState.kind === "invalid") {
|
|
161
|
+
if (tryRepair(entryPath, existingPreferredState).kind === "available") {
|
|
162
|
+
return preferredDir;
|
|
144
163
|
}
|
|
145
164
|
return ensureTrustedFallbackDir();
|
|
146
165
|
}
|
|
166
|
+
let preferredAvailable = false;
|
|
147
167
|
try {
|
|
148
|
-
const preferredParentDir = path.dirname(
|
|
168
|
+
const preferredParentDir = (windows ? path.win32 : path).dirname(entryPath);
|
|
149
169
|
accessSync(preferredParentDir, TMP_DIR_ACCESS_MODE);
|
|
150
|
-
mkdirSync
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
170
|
+
if (mkdirSync) {
|
|
171
|
+
const created = mkdirSync(entryPath, { recursive: true, mode: 0o700 });
|
|
172
|
+
const finalize = !windows && (suppliedMkdir !== undefined || created !== undefined);
|
|
173
|
+
const state = resolveDirState(entryPath);
|
|
174
|
+
preferredAvailable = (!finalize && state.kind === "available") || tryRepair(entryPath, state, finalize).kind === "available";
|
|
155
175
|
}
|
|
156
|
-
return options.preferredDir;
|
|
157
176
|
}
|
|
158
177
|
catch {
|
|
159
|
-
|
|
178
|
+
// A failed preferred path never relaxes fallback admission.
|
|
160
179
|
}
|
|
180
|
+
return preferredAvailable ? preferredDir : ensureTrustedFallbackDir();
|
|
161
181
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
2
|
+
type ExactDirectoryStat = Pick<BigIntStats, "dev" | "ino" | "uid" | "mode" | "isDirectory" | "isSymbolicLink">;
|
|
3
|
+
/** Complete, synchronous descriptor authority for secure-temp admission and repair. */
|
|
4
|
+
export type SecureTempRootDescriptorAdapter = {
|
|
5
|
+
lstatSync(path: string, options: {
|
|
6
|
+
bigint: true;
|
|
7
|
+
}): ExactDirectoryStat;
|
|
8
|
+
fstatSync(fd: number, options: {
|
|
9
|
+
bigint: true;
|
|
10
|
+
}): ExactDirectoryStat;
|
|
11
|
+
openSync(path: string, flags: number): number;
|
|
12
|
+
fchmodSync(fd: number, mode: number): void;
|
|
13
|
+
closeSync(fd: number): void;
|
|
14
|
+
constants: {
|
|
15
|
+
O_RDONLY: number;
|
|
16
|
+
O_DIRECTORY: number;
|
|
17
|
+
O_NOFOLLOW: number;
|
|
18
|
+
O_NONBLOCK: number;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export type SecureTempDirectoryReceipt = Readonly<{
|
|
22
|
+
dev: bigint;
|
|
23
|
+
ino: bigint;
|
|
24
|
+
uid: bigint;
|
|
25
|
+
mode: bigint;
|
|
26
|
+
}>;
|
|
27
|
+
export type SecureTempRepairAdapter = Omit<SecureTempRootDescriptorAdapter, "constants"> & {
|
|
28
|
+
flags: number;
|
|
29
|
+
};
|
|
30
|
+
export declare function captureSecureTempRepairAdapter(supplied: SecureTempRootDescriptorAdapter | undefined, hasLegacyFsHooks: boolean): SecureTempRepairAdapter | undefined;
|
|
31
|
+
export declare function secureTempDirectoryReceipt(stat: ExactDirectoryStat, uid: number): SecureTempDirectoryReceipt;
|
|
32
|
+
/** No pathname chmod, search-only descriptors, procfs, or reopening fallback. */
|
|
33
|
+
export declare function repairSecureTempDirectory(candidate: string, expected: SecureTempDirectoryReceipt, uid: number, adapter: SecureTempRepairAdapter, access: () => void, repaired: () => void, finalize?: boolean): void;
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=secure-temp-repair.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secure-temp-repair.d.ts","sourceRoot":"","sources":["../src/secure-temp-repair.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAE/C,KAAK,kBAAkB,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,aAAa,GAAG,gBAAgB,CAAC,CAAC;AAE/G,uFAAuF;AACvF,MAAM,MAAM,+BAA+B,GAAG;IAC5C,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE,IAAI,CAAA;KAAE,GAAG,kBAAkB,CAAC;IACvE,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE,IAAI,CAAA;KAAE,GAAG,kBAAkB,CAAC;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9C,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9F,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAC3G,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,+BAA+B,EAAE,WAAW,CAAC,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7G,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,+BAA+B,GAAG,SAAS,EACrD,gBAAgB,EAAE,OAAO,GACxB,uBAAuB,GAAG,SAAS,CAarC;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,GAAG,0BAA0B,CAS5G;AAgBD,iFAAiF;AACjF,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,0BAA0B,EACpC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,uBAAuB,EAChC,MAAM,EAAE,MAAM,IAAI,EAClB,QAAQ,EAAE,MAAM,IAAI,EACpB,QAAQ,UAAQ,GACf,IAAI,CAuDN"}
|