forked from doctly/switchboard
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpath-guard.js
More file actions
95 lines (84 loc) · 2.6 KB
/
Copy pathpath-guard.js
File metadata and controls
95 lines (84 loc) · 2.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
const path = require('path');
const os = require('os');
const fs = require('fs');
const HOME = os.homedir();
const CLAUDE_DIR = path.join(HOME, '.claude');
// Files/paths under the user's home that must never be read or written
// via IPC, even though the containing directory may be otherwise allowed.
// Patterns are checked against the resolved absolute path (case-insensitive
// on win32/darwin).
const DENY_PATTERNS = [
/[\\/]\.credentials\.json$/i,
/[\\/]\.ssh([\\/]|$)/i,
/[\\/]\.aws[\\/]credentials$/i,
/[\\/]\.netrc$/i,
/[\\/]\.gnupg([\\/]|$)/i,
/[\\/]id_rsa(\.pub)?$/i,
/[\\/]id_ed25519(\.pub)?$/i,
];
function normalizeCase(p) {
if (process.platform === 'win32' || process.platform === 'darwin') return p.toLowerCase();
return p;
}
function isWithin(child, parent) {
if (!child || !parent) return false;
const rel = path.relative(normalizeCase(parent), normalizeCase(child));
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
}
// The registry of roots callers may read/write under.
// Populate dynamically as projects/sessions are opened.
const allowedRoots = new Set();
function addAllowedRoot(root) {
if (!root || typeof root !== 'string') return;
try {
const abs = path.resolve(root);
allowedRoots.add(abs);
} catch {}
}
function removeAllowedRoot(root) {
if (!root || typeof root !== 'string') return;
try {
allowedRoots.delete(path.resolve(root));
} catch {}
}
function listAllowedRoots() {
return Array.from(allowedRoots);
}
// Built-in roots that are always allowed (read+write, minus DENY_PATTERNS).
function builtinRoots() {
return [CLAUDE_DIR];
}
// Returns { ok: true, resolved } on success, or { ok: false, error } on rejection.
// mode is 'read' or 'write' — currently behave the same but reserved for future
// tightening (e.g. read-only roots).
function assertPathAllowed(rawPath, mode = 'read') {
if (!rawPath || typeof rawPath !== 'string') {
return { ok: false, error: 'path required' };
}
let resolved;
try {
resolved = path.resolve(rawPath);
} catch (e) {
return { ok: false, error: 'invalid path' };
}
for (const pat of DENY_PATTERNS) {
if (pat.test(resolved)) {
return { ok: false, error: 'path denied (sensitive location)' };
}
}
const roots = [...builtinRoots(), ...allowedRoots];
for (const r of roots) {
if (isWithin(resolved, r)) return { ok: true, resolved, mode };
}
return { ok: false, error: 'path outside allowed roots' };
}
module.exports = {
assertPathAllowed,
addAllowedRoot,
removeAllowedRoot,
listAllowedRoots,
isWithin,
CLAUDE_DIR,
HOME,
DENY_PATTERNS,
};