@cortexkit/claustrum-client 0.6.0 → 0.6.1
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/dist/ancestor-permissions.d.ts +20 -0
- package/dist/ancestor-permissions.js +44 -0
- package/dist/enrollment.d.ts +2 -2
- package/dist/enrollment.js +4 -3
- package/dist/handles.d.ts +1 -0
- package/dist/handles.js +10 -11
- package/dist/manifest-lock.js +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
export type AncestorIdentity = {
|
|
3
|
+
euid: number | undefined;
|
|
4
|
+
egid: number | undefined;
|
|
5
|
+
passwdPath: string;
|
|
6
|
+
groupPath: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const ancestorIdentityContext: AsyncLocalStorage<AncestorIdentity>;
|
|
9
|
+
/**
|
|
10
|
+
* Debian OpenSSH's user-group-modes rule: ancestors without group/world-write, or with sticky, are safe.
|
|
11
|
+
* Without sticky, group-write requires effective uid/gid ownership and local passwd/group
|
|
12
|
+
* files proving the group contains only this user, with no other primary-group users.
|
|
13
|
+
* World-write never qualifies; anything the local account files cannot prove refuses.
|
|
14
|
+
* The twin accepts_ancestor in opencode_files.rs must keep the identical permission rule.
|
|
15
|
+
*/
|
|
16
|
+
export declare function acceptsAncestor(metadata: {
|
|
17
|
+
mode: number;
|
|
18
|
+
uid?: number;
|
|
19
|
+
gid?: number;
|
|
20
|
+
}, identity?: AncestorIdentity): Promise<boolean>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
export const ancestorIdentityContext = new AsyncLocalStorage();
|
|
4
|
+
function records(source, count) {
|
|
5
|
+
return source.split('\n').filter((line) => line.trim() !== '' && !line.trimStart().startsWith('#')).map((line) => {
|
|
6
|
+
const fields = line.split(':');
|
|
7
|
+
if (fields.length !== count || !fields[0] || /\s/.test(fields[0]) || !/^\d+$/.test(fields[2]) || !Number.isSafeInteger(Number(fields[2])) || Number(fields[2]) > 0xffffffff || (count === 7 && (!/^\d+$/.test(fields[3]) || !Number.isSafeInteger(Number(fields[3])) || Number(fields[3]) > 0xffffffff))) {
|
|
8
|
+
throw new Error('malformed account record');
|
|
9
|
+
}
|
|
10
|
+
if (count === 4 && fields[3] !== '' && fields[3].split(',').some((name) => name === '' || /\s/.test(name)))
|
|
11
|
+
throw new Error('malformed group members');
|
|
12
|
+
return fields;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Debian OpenSSH's user-group-modes rule: ancestors without group/world-write, or with sticky, are safe.
|
|
17
|
+
* Without sticky, group-write requires effective uid/gid ownership and local passwd/group
|
|
18
|
+
* files proving the group contains only this user, with no other primary-group users.
|
|
19
|
+
* World-write never qualifies; anything the local account files cannot prove refuses.
|
|
20
|
+
* The twin accepts_ancestor in opencode_files.rs must keep the identical permission rule.
|
|
21
|
+
*/
|
|
22
|
+
export async function acceptsAncestor(metadata, identity = ancestorIdentityContext.getStore() ?? {
|
|
23
|
+
euid: process.geteuid?.(), egid: process.getegid?.(), passwdPath: '/etc/passwd', groupPath: '/etc/group',
|
|
24
|
+
}) {
|
|
25
|
+
if ((metadata.mode & 0o022) === 0 || (metadata.mode & 0o1000) !== 0)
|
|
26
|
+
return true;
|
|
27
|
+
if ((metadata.mode & 0o002) !== 0 || identity.euid === undefined || identity.egid === undefined || metadata.uid !== identity.euid || metadata.gid !== identity.egid)
|
|
28
|
+
return false;
|
|
29
|
+
try {
|
|
30
|
+
const passwd = records(new TextDecoder('utf-8', { fatal: true }).decode(await readFile(identity.passwdPath)), 7);
|
|
31
|
+
const groups = records(new TextDecoder('utf-8', { fatal: true }).decode(await readFile(identity.groupPath)), 4);
|
|
32
|
+
const users = passwd.filter((entry) => Number(entry[2]) === identity.euid);
|
|
33
|
+
const matchingGroups = groups.filter((entry) => Number(entry[2]) === identity.egid);
|
|
34
|
+
if (users.length !== 1 || matchingGroups.length !== 1)
|
|
35
|
+
return false;
|
|
36
|
+
const name = users[0][0];
|
|
37
|
+
return (matchingGroups[0][3] === '' || matchingGroups[0][3].split(',').every((member) => member === name)) &&
|
|
38
|
+
!passwd.some((entry) => Number(entry[2]) !== identity.euid && Number(entry[3]) === identity.egid);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Local files must prove exclusivity; unavailable or malformed NSS data cannot grant an exception.
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/enrollment.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ export type EnrollmentTokenFile = {
|
|
|
5
5
|
/**
|
|
6
6
|
* Atomically replace a consumer's enrollment credential.
|
|
7
7
|
*
|
|
8
|
-
* The file is always mode 0600
|
|
9
|
-
*
|
|
8
|
+
* The file is always mode 0600. Writable canonical ancestors require sticky or a
|
|
9
|
+
* proven user-private group. This is the same rule implemented by the
|
|
10
10
|
* shipped `refuse_writable_ancestor` symbol in `opencode_files.rs`.
|
|
11
11
|
*/
|
|
12
12
|
export declare function writeEnrollmentTokenFile(path: string, value: EnrollmentTokenFile): Promise<void>;
|
package/dist/enrollment.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { acceptsAncestor } from './ancestor-permissions.js';
|
|
1
2
|
import { randomBytes } from 'node:crypto';
|
|
2
3
|
import { chmod, mkdir, open, realpath, rename, stat, unlink } from 'node:fs/promises';
|
|
3
4
|
import { basename, dirname, join } from 'node:path';
|
|
@@ -23,7 +24,7 @@ async function refuseWritableAncestor(parent) {
|
|
|
23
24
|
const metadata = await stat(component).catch(() => undefined);
|
|
24
25
|
// Canonicalisation already traversed this component. If a later stat races with a
|
|
25
26
|
// rename, let the create/rename operation below report the concrete failure.
|
|
26
|
-
if (metadata && (
|
|
27
|
+
if (metadata && !(await acceptsAncestor(metadata))) {
|
|
27
28
|
throw new Error(`enrollment token ancestor ${component} is group- or world-writable without sticky bit`);
|
|
28
29
|
}
|
|
29
30
|
const next = dirname(component);
|
|
@@ -35,8 +36,8 @@ async function refuseWritableAncestor(parent) {
|
|
|
35
36
|
/**
|
|
36
37
|
* Atomically replace a consumer's enrollment credential.
|
|
37
38
|
*
|
|
38
|
-
* The file is always mode 0600
|
|
39
|
-
*
|
|
39
|
+
* The file is always mode 0600. Writable canonical ancestors require sticky or a
|
|
40
|
+
* proven user-private group. This is the same rule implemented by the
|
|
40
41
|
* shipped `refuse_writable_ancestor` symbol in `opencode_files.rs`.
|
|
41
42
|
*/
|
|
42
43
|
export async function writeEnrollmentTokenFile(path, value) {
|
package/dist/handles.d.ts
CHANGED
package/dist/handles.js
CHANGED
|
@@ -3,6 +3,7 @@ import { constants } from 'node:fs';
|
|
|
3
3
|
import { lstat as nodeLstat, open as nodeOpen, readFile, realpath, stat as nodeStat } from 'node:fs/promises';
|
|
4
4
|
import { userInfo } from 'node:os';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
|
+
import { acceptsAncestor } from './ancestor-permissions.js';
|
|
6
7
|
export const HANDLE_FILE_CONTRACT = {
|
|
7
8
|
maxBytes: 256 * 1024,
|
|
8
9
|
mode: 0o600,
|
|
@@ -200,16 +201,14 @@ async function readHandleSnapshot(path = defaultHandleFilePath(), io = {}) {
|
|
|
200
201
|
if (expectedUid !== undefined && parent.uid !== undefined && parent.uid !== expectedUid) {
|
|
201
202
|
invalid('handle file parent is not owned by the current uid');
|
|
202
203
|
}
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
// they own. Mirrors the Rust check in opencode_files.rs; the two must not drift.
|
|
212
|
-
if ((parent.mode & 0o022) !== 0 && (parent.mode & 0o1000) === 0) {
|
|
204
|
+
// Group-write counts as well as world-write: a directory writer can unlink and
|
|
205
|
+
// replace a mode-0600 file regardless of its file permissions or owner. Owning
|
|
206
|
+
// the directory does not exclude other group members. Sticky exempts both bits
|
|
207
|
+
// because a writer may unlink only files they own. Without sticky, group-write
|
|
208
|
+
// is safe only when local account files prove a user-private effective group;
|
|
209
|
+
// world-write is never exempt. Keep this rule identical to Rust's accepts_ancestor
|
|
210
|
+
// and parent check in opencode_files.rs.
|
|
211
|
+
if (!(await acceptsAncestor(parent))) {
|
|
213
212
|
invalid('handle file parent is group- or world-writable without sticky bit');
|
|
214
213
|
}
|
|
215
214
|
// EVERY ANCESTOR, NOT JUST THIS ONE, OR THE GUARANTEE DOES NOT COMPOSE. The immediate
|
|
@@ -244,7 +243,7 @@ async function readHandleSnapshot(path = defaultHandleFilePath(), io = {}) {
|
|
|
244
243
|
catch {
|
|
245
244
|
ancestor = undefined;
|
|
246
245
|
}
|
|
247
|
-
if (ancestor && (
|
|
246
|
+
if (ancestor && !(await acceptsAncestor(ancestor))) {
|
|
248
247
|
invalid(`handle file ancestor ${component} is group- or world-writable without sticky bit`);
|
|
249
248
|
}
|
|
250
249
|
const next = dirname(component);
|
package/dist/manifest-lock.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { acceptsAncestor } from './ancestor-permissions.js';
|
|
1
2
|
import { constants as fsConstants } from 'node:fs';
|
|
2
3
|
import { chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, unlink } from 'node:fs/promises';
|
|
3
4
|
import { randomBytes, randomInt } from 'node:crypto';
|
|
@@ -263,7 +264,7 @@ async function refuseWritableAncestors(parent) {
|
|
|
263
264
|
}
|
|
264
265
|
for (;;) {
|
|
265
266
|
const metadata = await stat(component);
|
|
266
|
-
if ((
|
|
267
|
+
if (!(await acceptsAncestor(metadata)))
|
|
267
268
|
throw new Error('handle file ancestor is group- or world-writable without sticky bit');
|
|
268
269
|
const next = dirname(component);
|
|
269
270
|
if (next === component)
|