@sysid/sandbox-runtime-improved 0.0.56-sysid.1 → 0.0.64-sysid.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/README.md +106 -4
- package/dist/cli.js +12 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/sandbox/credential-mask-files.d.ts +157 -0
- package/dist/sandbox/credential-mask-files.d.ts.map +1 -0
- package/dist/sandbox/credential-mask-files.js +298 -0
- package/dist/sandbox/credential-mask-files.js.map +1 -0
- package/dist/sandbox/credential-sentinel.d.ts +72 -0
- package/dist/sandbox/credential-sentinel.d.ts.map +1 -0
- package/dist/sandbox/credential-sentinel.js +130 -0
- package/dist/sandbox/credential-sentinel.js.map +1 -0
- package/dist/sandbox/domain-pattern.d.ts +36 -0
- package/dist/sandbox/domain-pattern.d.ts.map +1 -0
- package/dist/sandbox/domain-pattern.js +60 -0
- package/dist/sandbox/domain-pattern.js.map +1 -0
- package/dist/sandbox/http-proxy.d.ts +33 -2
- package/dist/sandbox/http-proxy.d.ts.map +1 -1
- package/dist/sandbox/http-proxy.js +30 -2
- package/dist/sandbox/http-proxy.js.map +1 -1
- package/dist/sandbox/linux-sandbox-utils.d.ts +20 -0
- package/dist/sandbox/linux-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/linux-sandbox-utils.js +134 -65
- package/dist/sandbox/linux-sandbox-utils.js.map +1 -1
- package/dist/sandbox/linux-violation-monitor.d.ts +48 -0
- package/dist/sandbox/linux-violation-monitor.d.ts.map +1 -0
- package/dist/sandbox/linux-violation-monitor.js +156 -0
- package/dist/sandbox/linux-violation-monitor.js.map +1 -0
- package/dist/sandbox/listen-in-range.d.ts +12 -0
- package/dist/sandbox/listen-in-range.d.ts.map +1 -0
- package/dist/sandbox/listen-in-range.js +43 -0
- package/dist/sandbox/listen-in-range.js.map +1 -0
- package/dist/sandbox/macos-sandbox-utils.d.ts +11 -0
- package/dist/sandbox/macos-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/macos-sandbox-utils.js +72 -17
- package/dist/sandbox/macos-sandbox-utils.js.map +1 -1
- package/dist/sandbox/mitm-ca.d.ts +69 -2
- package/dist/sandbox/mitm-ca.d.ts.map +1 -1
- package/dist/sandbox/mitm-ca.js +188 -16
- package/dist/sandbox/mitm-ca.js.map +1 -1
- package/dist/sandbox/mitm-leaf.d.ts +1 -1
- package/dist/sandbox/mitm-leaf.d.ts.map +1 -1
- package/dist/sandbox/mitm-leaf.js +26 -19
- package/dist/sandbox/mitm-leaf.js.map +1 -1
- package/dist/sandbox/mux-proxy.d.ts +59 -0
- package/dist/sandbox/mux-proxy.d.ts.map +1 -0
- package/dist/sandbox/mux-proxy.js +159 -0
- package/dist/sandbox/mux-proxy.js.map +1 -0
- package/dist/sandbox/request-filter.d.ts +11 -1
- package/dist/sandbox/request-filter.d.ts.map +1 -1
- package/dist/sandbox/request-filter.js.map +1 -1
- package/dist/sandbox/sandbox-config.d.ts +461 -41
- package/dist/sandbox/sandbox-config.d.ts.map +1 -1
- package/dist/sandbox/sandbox-config.js +342 -26
- package/dist/sandbox/sandbox-config.js.map +1 -1
- package/dist/sandbox/sandbox-manager.d.ts +5 -1
- package/dist/sandbox/sandbox-manager.d.ts.map +1 -1
- package/dist/sandbox/sandbox-manager.js +697 -211
- package/dist/sandbox/sandbox-manager.js.map +1 -1
- package/dist/sandbox/sandbox-schemas.d.ts +15 -0
- package/dist/sandbox/sandbox-schemas.d.ts.map +1 -1
- package/dist/sandbox/sandbox-utils.d.ts +55 -6
- package/dist/sandbox/sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/sandbox-utils.js +143 -30
- package/dist/sandbox/sandbox-utils.js.map +1 -1
- package/dist/sandbox/socks-proxy.d.ts +11 -5
- package/dist/sandbox/socks-proxy.d.ts.map +1 -1
- package/dist/sandbox/socks-proxy.js +13 -81
- package/dist/sandbox/socks-proxy.js.map +1 -1
- package/dist/sandbox/tls-terminate-proxy.d.ts +2 -2
- package/dist/sandbox/tls-terminate-proxy.d.ts.map +1 -1
- package/dist/sandbox/tls-terminate-proxy.js +31 -5
- package/dist/sandbox/tls-terminate-proxy.js.map +1 -1
- package/dist/sandbox/windows-sandbox-utils.d.ts +508 -116
- package/dist/sandbox/windows-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/windows-sandbox-utils.js +721 -209
- package/dist/sandbox/windows-sandbox-utils.js.map +1 -1
- package/dist/utils/shell-quote.d.ts +27 -0
- package/dist/utils/shell-quote.d.ts.map +1 -0
- package/dist/utils/shell-quote.js +47 -0
- package/dist/utils/shell-quote.js.map +1 -0
- package/package.json +7 -6
- package/vendor/seccomp/arm64/apply-seccomp +0 -0
- package/vendor/seccomp/build.ts +8 -30
- package/vendor/seccomp/x64/apply-seccomp +0 -0
- package/vendor/srt-win/build.ts +21 -0
- package/dist/vendor/seccomp/Dockerfile.build +0 -6
- package/dist/vendor/seccomp/arm64/apply-seccomp +0 -0
- package/dist/vendor/seccomp/build.ts +0 -91
- package/dist/vendor/seccomp/x64/apply-seccomp +0 -0
- package/dist/vendor/seccomp-src/apply-seccomp.c +0 -280
- package/dist/vendor/seccomp-src/seccomp-unix-block.c +0 -148
- package/dist/vendor/srt-win/Cargo.lock +0 -361
- package/dist/vendor/srt-win/Cargo.toml +0 -46
- package/dist/vendor/srt-win/ci/cleanup.ps1 +0 -49
- package/dist/vendor/srt-win/ci/smoke-exec.ps1 +0 -446
- package/dist/vendor/srt-win/ci/smoke.ps1 +0 -307
- package/dist/vendor/srt-win/src/job.rs +0 -102
- package/dist/vendor/srt-win/src/launch.rs +0 -732
- package/dist/vendor/srt-win/src/lib.rs +0 -20
- package/dist/vendor/srt-win/src/main.rs +0 -669
- package/dist/vendor/srt-win/src/self_protect.rs +0 -169
- package/dist/vendor/srt-win/src/sid.rs +0 -296
- package/dist/vendor/srt-win/src/token.rs +0 -341
- package/dist/vendor/srt-win/src/util.rs +0 -42
- package/dist/vendor/srt-win/src/wfp.rs +0 -992
- package/dist/vendor/srt-win/src/winsta.rs +0 -209
- package/dist/vendor/srt-win/tests/sd_access_check_matrix.rs +0 -259
- package/vendor/seccomp-src/apply-seccomp.c +0 -280
- package/vendor/seccomp-src/seccomp-unix-block.c +0 -148
- package/vendor/srt-win/Cargo.lock +0 -361
- package/vendor/srt-win/Cargo.toml +0 -46
- package/vendor/srt-win/ci/cleanup.ps1 +0 -49
- package/vendor/srt-win/ci/smoke-exec.ps1 +0 -446
- package/vendor/srt-win/ci/smoke.ps1 +0 -307
- package/vendor/srt-win/src/job.rs +0 -102
- package/vendor/srt-win/src/launch.rs +0 -732
- package/vendor/srt-win/src/lib.rs +0 -20
- package/vendor/srt-win/src/main.rs +0 -669
- package/vendor/srt-win/src/self_protect.rs +0 -169
- package/vendor/srt-win/src/sid.rs +0 -296
- package/vendor/srt-win/src/token.rs +0 -341
- package/vendor/srt-win/src/util.rs +0 -42
- package/vendor/srt-win/src/wfp.rs +0 -992
- package/vendor/srt-win/src/winsta.rs +0 -209
- package/vendor/srt-win/tests/sd_access_check_matrix.rs +0 -259
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential file masking (Linux).
|
|
3
|
+
*
|
|
4
|
+
* For a `credentials.files` entry with `mode: "mask"`, srt reads the real
|
|
5
|
+
* file content on the host, registers one or more sentinels in the
|
|
6
|
+
* {@link SentinelRegistry}, and writes a fake file (sentinel-substituted)
|
|
7
|
+
* to a manager-owned temp directory. The Linux sandbox then `--ro-bind`s
|
|
8
|
+
* the fake over the real path, so the sandboxed process reads the
|
|
9
|
+
* sentinel(s). The proxy substitution from env-var masking already scans
|
|
10
|
+
* every header for any registered sentinel, so a tool that does
|
|
11
|
+
* `Authorization: Bearer $(cat <maskedFile>)` reaches the upstream with
|
|
12
|
+
* the real bytes — no proxy changes required.
|
|
13
|
+
*
|
|
14
|
+
* Without `extract`, masking is **whole-file**: one sentinel replaces the
|
|
15
|
+
* entire content. With `extract`, masking is **structured**: a regex picks
|
|
16
|
+
* out the credential value(s) and only those spans are replaced, so a tool
|
|
17
|
+
* that parses the file (JSON/YAML/.netrc) still sees valid syntax. See
|
|
18
|
+
* {@link extractAndSubstitute} and {@link CredentialFileConfigSchema}.
|
|
19
|
+
*
|
|
20
|
+
* On macOS, SBPL cannot redirect reads, so masked files degrade to
|
|
21
|
+
* `mode: "deny"` (see macos-sandbox-utils.ts).
|
|
22
|
+
*/
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import { tmpdir } from 'node:os';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { logForDebugging } from '../utils/debug.js';
|
|
27
|
+
import { normalizePathForSandbox } from './sandbox-utils.js';
|
|
28
|
+
/**
|
|
29
|
+
* Sentinel-registry key prefix for masked files. Keeps file keys disjoint
|
|
30
|
+
* from env-var names so a credential file at path `GH_TOKEN` cannot collide
|
|
31
|
+
* with the env var `GH_TOKEN`.
|
|
32
|
+
*/
|
|
33
|
+
const FILE_KEY_PREFIX = 'file:';
|
|
34
|
+
/**
|
|
35
|
+
* Apply `pattern` globally to `content` and return `content` with each
|
|
36
|
+
* matched capture-group-1 span replaced by `sentinelFor(capture, i)`,
|
|
37
|
+
* where `i` is the zero-based index of the distinct captured value in
|
|
38
|
+
* first-seen order.
|
|
39
|
+
*
|
|
40
|
+
* Offset-based: the regex `d` flag exposes capture-group offsets, so the
|
|
41
|
+
* output is built by slicing between spans and splicing the sentinel in
|
|
42
|
+
* at the exact `[start, end)` of group 1. By default only the
|
|
43
|
+
* regex-matched span is replaced — a captured value that coincidentally
|
|
44
|
+
* appears elsewhere in the file (outside any match) is left intact. No
|
|
45
|
+
* placeholder pass, no substring-ordering concern.
|
|
46
|
+
*
|
|
47
|
+
* With `maskDuplicates` (see {@link ExtractOptions}), an indexOf scan
|
|
48
|
+
* additionally collects every verbatim occurrence of each captured value
|
|
49
|
+
* elsewhere in the file. All spans are computed against the ORIGINAL
|
|
50
|
+
* content — never the partially substituted output — so an inserted
|
|
51
|
+
* sentinel can never be re-matched and corrupted, even when a captured
|
|
52
|
+
* value is a substring of the sentinel literal. Regex-match spans win
|
|
53
|
+
* over verbatim ones, and verbatim scans run longest-capture-first so a
|
|
54
|
+
* shorter capture that is a substring of a longer one cannot claim part
|
|
55
|
+
* of the longer secret's occurrence.
|
|
56
|
+
*
|
|
57
|
+
* Returns `null` when the pattern matches nothing — the caller routes
|
|
58
|
+
* that per the entry's `onExtractNoMatch` option (warn / deny / error;
|
|
59
|
+
* see {@link buildMaskedFileBinds}).
|
|
60
|
+
*
|
|
61
|
+
* Throws when a match has no group-1 capture. The schema already rejects
|
|
62
|
+
* patterns with zero groups, so this only fires when group 1 is optional
|
|
63
|
+
* and absent for some match (e.g. `"token: (\\S+)?"`); accepting that
|
|
64
|
+
* would silently mask nothing for that occurrence.
|
|
65
|
+
*
|
|
66
|
+
* Pure on `content`/`pattern`; the callback may close over a registry.
|
|
67
|
+
*/
|
|
68
|
+
export function extractAndSubstitute(content, pattern, sentinelFor, options = {}) {
|
|
69
|
+
// The schema validates `pattern` compiles; `g` makes matchAll iterate
|
|
70
|
+
// every occurrence and `d` populates `m.indices` with group offsets.
|
|
71
|
+
const re = new RegExp(pattern, 'gd');
|
|
72
|
+
const indexByCapture = new Map();
|
|
73
|
+
// First sentinel returned per capture — reused for verbatim spans so
|
|
74
|
+
// the duplicate pass never re-invokes the callback.
|
|
75
|
+
const sentinelByCapture = new Map();
|
|
76
|
+
const spans = [];
|
|
77
|
+
for (const m of content.matchAll(re)) {
|
|
78
|
+
const cap = m[1];
|
|
79
|
+
if (cap === undefined) {
|
|
80
|
+
throw new Error(`extract pattern /${pattern}/ matched at offset ${m.index} but ` +
|
|
81
|
+
`capture group 1 is undefined — group 1 must capture the ` +
|
|
82
|
+
`credential value on every match.`);
|
|
83
|
+
}
|
|
84
|
+
// Empty captures are skipped: a zero-width span has nothing to mask.
|
|
85
|
+
if (cap.length === 0)
|
|
86
|
+
continue;
|
|
87
|
+
let i = indexByCapture.get(cap);
|
|
88
|
+
if (i === undefined)
|
|
89
|
+
indexByCapture.set(cap, (i = indexByCapture.size));
|
|
90
|
+
const [start, end] = m.indices[1];
|
|
91
|
+
const sentinel = sentinelFor(cap, i);
|
|
92
|
+
if (!sentinelByCapture.has(cap))
|
|
93
|
+
sentinelByCapture.set(cap, sentinel);
|
|
94
|
+
spans.push({ start, end, sentinel });
|
|
95
|
+
}
|
|
96
|
+
if (indexByCapture.size === 0)
|
|
97
|
+
return null;
|
|
98
|
+
if (options.maskDuplicates) {
|
|
99
|
+
// Longest capture first: a shorter capture that is a substring of a
|
|
100
|
+
// longer one would otherwise claim part of the longer secret's
|
|
101
|
+
// occurrence and leave the remainder exposed.
|
|
102
|
+
const byLength = [...indexByCapture.keys()].sort((a, b) => b.length - a.length);
|
|
103
|
+
for (const cap of byLength) {
|
|
104
|
+
const sentinel = sentinelByCapture.get(cap);
|
|
105
|
+
for (let start = content.indexOf(cap); start !== -1; start = content.indexOf(cap, start + 1)) {
|
|
106
|
+
const end = start + cap.length;
|
|
107
|
+
// Spans already collected (regex matches, then earlier — longer —
|
|
108
|
+
// captures' verbatim occurrences) win over this occurrence.
|
|
109
|
+
if (spans.some(s => start < s.end && s.start < end))
|
|
110
|
+
continue;
|
|
111
|
+
spans.push({ start, end, sentinel });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
spans.sort((a, b) => a.start - b.start);
|
|
115
|
+
}
|
|
116
|
+
// Spans are non-overlapping and sorted by offset (matchAll yields
|
|
117
|
+
// matches in order; the verbatim pass re-sorts), so a single
|
|
118
|
+
// slice-and-concat pass over the original content is sound.
|
|
119
|
+
let out = '';
|
|
120
|
+
let pos = 0;
|
|
121
|
+
for (const s of spans) {
|
|
122
|
+
out += content.slice(pos, s.start) + s.sentinel;
|
|
123
|
+
pos = s.end;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
fakeContent: out + content.slice(pos),
|
|
127
|
+
captures: [...indexByCapture.keys()],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Manager-owned temp dir holding the fake files.
|
|
132
|
+
*
|
|
133
|
+
* INVARIANT: this directory must never be writable from inside the sandbox.
|
|
134
|
+
* The Linux layer enforces this by emitting `--ro-bind <dirPath> <dirPath>`
|
|
135
|
+
* after every other filesystem mount (see generateFilesystemArgs), so the
|
|
136
|
+
* store stays read-only even if a caller's allowWrite covers os.tmpdir() or
|
|
137
|
+
* the host's $TMPDIR points under a default-writable path. If the sandbox
|
|
138
|
+
* could write here it could replace a fake's content (the bind exposes the
|
|
139
|
+
* source file) or plant a symlink for a later host-side write() to follow.
|
|
140
|
+
*/
|
|
141
|
+
export class MaskedFileStore {
|
|
142
|
+
constructor() {
|
|
143
|
+
this.byKey = new Map();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Write `sentinel` to a fake file for `key` and return its path.
|
|
147
|
+
* Idempotent on `key`: a repeat call rewrites the same fake (so a
|
|
148
|
+
* changed sentinel after re-register propagates) instead of leaking a
|
|
149
|
+
* new file per wrapWithSandbox() call.
|
|
150
|
+
*/
|
|
151
|
+
write(key, sentinel) {
|
|
152
|
+
if (this.dir === undefined) {
|
|
153
|
+
this.dir = fs.mkdtempSync(join(tmpdir(), 'srt-credmask-'));
|
|
154
|
+
}
|
|
155
|
+
let fakePath = this.byKey.get(key);
|
|
156
|
+
if (fakePath === undefined) {
|
|
157
|
+
fakePath = join(this.dir, `${this.byKey.size}.fake`);
|
|
158
|
+
this.byKey.set(key, fakePath);
|
|
159
|
+
}
|
|
160
|
+
// Never follow a symlink at fakePath: a prior sandbox invocation may
|
|
161
|
+
// have planted one (the store dir is ro-bound now, but defence in
|
|
162
|
+
// depth). Unlink first so writeFileSync creates a fresh regular file.
|
|
163
|
+
fs.rmSync(fakePath, { force: true });
|
|
164
|
+
// 0600: owner rw so the idempotent rewrite above succeeds; the bind
|
|
165
|
+
// into the sandbox is --ro-bind so the sandboxed process sees it
|
|
166
|
+
// read-only regardless of the host mode. No group/other.
|
|
167
|
+
fs.writeFileSync(fakePath, sentinel, { mode: 0o600 });
|
|
168
|
+
return fakePath;
|
|
169
|
+
}
|
|
170
|
+
/** Remove the temp dir and every fake file in it. Idempotent. */
|
|
171
|
+
dispose() {
|
|
172
|
+
if (this.dir !== undefined) {
|
|
173
|
+
try {
|
|
174
|
+
fs.rmSync(this.dir, { recursive: true, force: true });
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
logForDebugging(`MaskedFileStore cleanup failed: ${err}`, {
|
|
178
|
+
level: 'error',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
this.dir = undefined;
|
|
183
|
+
this.byKey.clear();
|
|
184
|
+
}
|
|
185
|
+
/** Temp dir path, or undefined if no fake has been written yet. */
|
|
186
|
+
get dirPath() {
|
|
187
|
+
return this.dir;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* For each `mode: "mask"` file entry: resolve the path, read the real
|
|
192
|
+
* content, build the fake content (whole-file or structured per `extract`),
|
|
193
|
+
* register sentinels in `registry`, write the fake via `store`, and return
|
|
194
|
+
* the bind list plus any entries that degraded to deny.
|
|
195
|
+
*
|
|
196
|
+
* Whole-file mode (no `extract`): one sentinel keyed `file:<path>` whose
|
|
197
|
+
* real value is the entire file content; the fake file *is* the sentinel.
|
|
198
|
+
*
|
|
199
|
+
* Structured mode (`extract` set): one sentinel per distinct captured
|
|
200
|
+
* value, keyed `file:<path>#<i>`; the fake file is the real content with
|
|
201
|
+
* each captured span replaced by its sentinel. If the regex matches
|
|
202
|
+
* nothing, the entry's `onExtractNoMatch` decides:
|
|
203
|
+
* - `"warn"` (default): skip the entry with a loud stderr warning —
|
|
204
|
+
* fail-open, the file stays readable via the root mount;
|
|
205
|
+
* - `"deny"`: push the path to `degradeToDenyPaths` — fail-closed, the
|
|
206
|
+
* file becomes unreadable inside the sandbox;
|
|
207
|
+
* - `"error"`: throw, so nothing runs until the regex is fixed.
|
|
208
|
+
* With `maskDuplicates`, verbatim occurrences of each captured value
|
|
209
|
+
* outside the matched spans are also replaced (see {@link ExtractOptions}).
|
|
210
|
+
*
|
|
211
|
+
* Entries whose path does not exist, is unreadable, or resolves to a
|
|
212
|
+
* directory are skipped with a debug log — same posture as a masked env
|
|
213
|
+
* var that's unset on the host: nothing to protect, and surfacing a hard
|
|
214
|
+
* error would make a portable config brittle across machines.
|
|
215
|
+
*
|
|
216
|
+
* The directory check is the authoritative one (the schema only catches a
|
|
217
|
+
* trailing slash); whole-file masking has no meaning for a directory.
|
|
218
|
+
*/
|
|
219
|
+
export function buildMaskedFileBinds(files, allowedDomains, registry, store) {
|
|
220
|
+
const binds = [];
|
|
221
|
+
const degradeToDenyPaths = [];
|
|
222
|
+
for (const f of files) {
|
|
223
|
+
if (f.mode !== 'mask')
|
|
224
|
+
continue;
|
|
225
|
+
const realPath = normalizePathForSandbox(f.path);
|
|
226
|
+
let content;
|
|
227
|
+
try {
|
|
228
|
+
const stat = fs.statSync(realPath);
|
|
229
|
+
if (stat.isDirectory()) {
|
|
230
|
+
logForDebugging(`[credential-mask] Skipping masked file entry that resolves to ` +
|
|
231
|
+
`a directory: ${f.path} — use mode "deny" for directories.`, { level: 'warn' });
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
// Read as bytes first: a utf8 read silently maps invalid sequences
|
|
235
|
+
// to U+FFFD, so the sentinel would round-trip to corrupted bytes at
|
|
236
|
+
// the proxy. Masking (whole-file or extract) is for text credential
|
|
237
|
+
// files; reject binary.
|
|
238
|
+
const raw = fs.readFileSync(realPath);
|
|
239
|
+
content = raw.toString('utf8');
|
|
240
|
+
if (Buffer.byteLength(content, 'utf8') !== raw.length) {
|
|
241
|
+
logForDebugging(`[credential-mask] Skipping masked file with non-UTF-8 content ` +
|
|
242
|
+
`(binary credential files are not supported in mask mode): ` +
|
|
243
|
+
`${f.path}`, { level: 'warn' });
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
logForDebugging(`[credential-mask] Skipping masked file (unreadable on host): ` +
|
|
249
|
+
`${f.path} — ${err.message}`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const injectHosts = f.injectHosts ?? allowedDomains;
|
|
253
|
+
const key = FILE_KEY_PREFIX + realPath;
|
|
254
|
+
let fakeContent;
|
|
255
|
+
if (f.extract === undefined) {
|
|
256
|
+
// Whole-file: one sentinel for the entire content.
|
|
257
|
+
fakeContent = registry.register(key, content, injectHosts);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
const extracted = extractAndSubstitute(content, f.extract, (cap, i) => registry.register(`${key}#${i}`, cap, injectHosts), { maskDuplicates: f.maskDuplicates ?? false });
|
|
261
|
+
if (extracted === null) {
|
|
262
|
+
const onNoMatch = f.onExtractNoMatch ?? 'warn';
|
|
263
|
+
if (onNoMatch === 'error') {
|
|
264
|
+
throw new Error(`credentials.files entry "${f.path}": extract pattern ` +
|
|
265
|
+
`"${f.extract}" matched nothing (onExtractNoMatch: "error"). ` +
|
|
266
|
+
`Fix the regex, change to "warn"/"deny", or remove the entry.`);
|
|
267
|
+
}
|
|
268
|
+
if (onNoMatch === 'deny') {
|
|
269
|
+
// Fail-closed: the operator declared this file as containing a
|
|
270
|
+
// credential. Masking can't apply — degrade to deny so the
|
|
271
|
+
// sandboxed process cannot read the credential at all.
|
|
272
|
+
logForDebugging(`[credential-mask] extract pattern /${f.extract}/ matched ` +
|
|
273
|
+
`nothing in ${f.path} — degrading to mode "deny".`, { level: 'warn' });
|
|
274
|
+
degradeToDenyPaths.push(realPath);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
// 'warn' (default): fail-open. A non-matching pattern is a config
|
|
278
|
+
// error to surface, not a reason to block file access. Skip the
|
|
279
|
+
// entry (no bind, no deny) — the file stays readable via the root
|
|
280
|
+
// mount — and warn loudly on stderr so the operator fixes the regex.
|
|
281
|
+
const msg = `[sandbox-runtime] WARNING: credentials.files entry ` +
|
|
282
|
+
`"${f.path}" has extract pattern "${f.extract}" that matched ` +
|
|
283
|
+
`nothing in the file. The file is left UNPROTECTED (readable ` +
|
|
284
|
+
`as-is inside the sandbox). Fix the regex, set onExtractNoMatch ` +
|
|
285
|
+
`to "deny" or "error", or remove the entry.`;
|
|
286
|
+
console.warn(msg);
|
|
287
|
+
logForDebugging(msg, { level: 'warn' });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
fakeContent = extracted.fakeContent;
|
|
291
|
+
}
|
|
292
|
+
const fakePath = store.write(key, fakeContent);
|
|
293
|
+
binds.push({ realPath, fakePath });
|
|
294
|
+
}
|
|
295
|
+
return { binds, degradeToDenyPaths };
|
|
296
|
+
}
|
|
297
|
+
export const MASKED_FILE_STORE_PREFIX = 'srt-credmask-';
|
|
298
|
+
//# sourceMappingURL=credential-mask-files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-mask-files.js","sourceRoot":"","sources":["../../src/sandbox/credential-mask-files.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session sentinel registry for credential masking.
|
|
3
|
+
*
|
|
4
|
+
* A masked credential's real value is replaced inside the sandbox with a
|
|
5
|
+
* sentinel of the form `fake_value_<uuid4>`. The sandboxed process sees only
|
|
6
|
+
* the sentinel; the host-side proxy substitutes sentinel→real on egress to
|
|
7
|
+
* allowlisted destinations. The map lives only in process memory — it is
|
|
8
|
+
* never written to disk and never logged.
|
|
9
|
+
*/
|
|
10
|
+
import type { IncomingHttpHeaders } from 'node:http';
|
|
11
|
+
export declare const SENTINEL_PREFIX = "fake_value_";
|
|
12
|
+
/** Predicate matching a destination host against one injectHosts pattern. */
|
|
13
|
+
export type HostMatcher = (host: string, pattern: string) => boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Sentinel↔real-value map for one sandbox session, keyed by credential name.
|
|
16
|
+
*
|
|
17
|
+
* Each credential carries its own `injectHosts` list, and substitution is
|
|
18
|
+
* gated per sentinel: a sentinel is swapped to its real value only when the
|
|
19
|
+
* destination matches THAT credential's hosts. This prevents laundering
|
|
20
|
+
* credential A through credential B's allowlisted host by sending A's
|
|
21
|
+
* sentinel there — the proxy leaves A's sentinel intact on B's host.
|
|
22
|
+
*
|
|
23
|
+
* Keying on name (not value) means two env vars holding the same secret get
|
|
24
|
+
* distinct sentinels, so each can have an independent host list.
|
|
25
|
+
*/
|
|
26
|
+
export declare class SentinelRegistry {
|
|
27
|
+
private readonly byName;
|
|
28
|
+
private readonly bySentinel;
|
|
29
|
+
/**
|
|
30
|
+
* Return the sentinel for the credential named `name`, minting a fresh one
|
|
31
|
+
* on first use. The sentinel is `fake_value_<uuid4>`: long enough that an
|
|
32
|
+
* accidental collision with legitimate header content is negligible, and
|
|
33
|
+
* free of shell/URL metacharacters so it survives `--setenv` and
|
|
34
|
+
* `env NAME=value` unquoted.
|
|
35
|
+
*
|
|
36
|
+
* Idempotent on `name`: a repeat call returns the same sentinel and updates
|
|
37
|
+
* `realValue`/`injectHosts` in place so `updateConfig()` can change either
|
|
38
|
+
* without invalidating sentinels the sandboxed process has already read.
|
|
39
|
+
*/
|
|
40
|
+
register(name: string, realValue: string, injectHosts: readonly string[]): string;
|
|
41
|
+
/** Real value for `sentinel`, or undefined if not registered. */
|
|
42
|
+
lookupReal(sentinel: string): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Names of the registered credentials whose `injectHosts` cover
|
|
45
|
+
* `destHost`. Diagnostic helper: the proxy uses it to warn when a host is
|
|
46
|
+
* exempted from TLS termination (so substitution can never run there) but
|
|
47
|
+
* a masked credential is configured for injection at it.
|
|
48
|
+
*/
|
|
49
|
+
namesInjectableAt(destHost: string, matches: HostMatcher): string[];
|
|
50
|
+
/** Iterate registered `[sentinel, realValue]` pairs. */
|
|
51
|
+
entries(): IterableIterator<[string, string]>;
|
|
52
|
+
/** Number of registered sentinels. */
|
|
53
|
+
get size(): number;
|
|
54
|
+
/** Drop every mapping. Called on session teardown. */
|
|
55
|
+
clear(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Replace registered sentinels found in `headers` with their real values,
|
|
58
|
+
* in place. Each sentinel substitutes only when `destHost` matches one of
|
|
59
|
+
* THAT credential's `injectHosts` patterns (via `matches`); a sentinel
|
|
60
|
+
* whose host list does not cover `destHost` is left as the useless fake.
|
|
61
|
+
*
|
|
62
|
+
* Scans all header values rather than a fixed set — a sentinel showing up
|
|
63
|
+
* anywhere is the substitution trigger, regardless of header name
|
|
64
|
+
* (Authorization, X-Api-Key, Private-Token, ...).
|
|
65
|
+
*
|
|
66
|
+
* The caller remains responsible for transport gating (TLS-terminated path
|
|
67
|
+
* unless `allowPlaintextInject`).
|
|
68
|
+
*/
|
|
69
|
+
substituteInHeaders(headers: IncomingHttpHeaders, destHost: string, matches: HostMatcher): void;
|
|
70
|
+
private substituteInString;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=credential-sentinel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-sentinel.d.ts","sourceRoot":"","sources":["../../src/sandbox/credential-sentinel.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AAEpD,eAAO,MAAM,eAAe,gBAAgB,CAAA;AAE5C,6EAA6E;AAC7E,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAA;AASpE;;;;;;;;;;;GAWG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAC1D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAmC;IAE9D;;;;;;;;;;OAUG;IACH,QAAQ,CACN,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,MAAM;IAcT,iEAAiE;IACjE,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIhD;;;;;OAKG;IACH,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,MAAM,EAAE;IAQnE,wDAAwD;IACvD,OAAO,IAAI,gBAAgB,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAI9C,sCAAsC;IACtC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,sDAAsD;IACtD,KAAK,IAAI,IAAI;IAKb;;;;;;;;;;;;OAYG;IACH,mBAAmB,CACjB,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,WAAW,GACnB,IAAI;IAcP,OAAO,CAAC,kBAAkB;CAgB3B"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session sentinel registry for credential masking.
|
|
3
|
+
*
|
|
4
|
+
* A masked credential's real value is replaced inside the sandbox with a
|
|
5
|
+
* sentinel of the form `fake_value_<uuid4>`. The sandboxed process sees only
|
|
6
|
+
* the sentinel; the host-side proxy substitutes sentinel→real on egress to
|
|
7
|
+
* allowlisted destinations. The map lives only in process memory — it is
|
|
8
|
+
* never written to disk and never logged.
|
|
9
|
+
*/
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
11
|
+
export const SENTINEL_PREFIX = 'fake_value_';
|
|
12
|
+
/**
|
|
13
|
+
* Sentinel↔real-value map for one sandbox session, keyed by credential name.
|
|
14
|
+
*
|
|
15
|
+
* Each credential carries its own `injectHosts` list, and substitution is
|
|
16
|
+
* gated per sentinel: a sentinel is swapped to its real value only when the
|
|
17
|
+
* destination matches THAT credential's hosts. This prevents laundering
|
|
18
|
+
* credential A through credential B's allowlisted host by sending A's
|
|
19
|
+
* sentinel there — the proxy leaves A's sentinel intact on B's host.
|
|
20
|
+
*
|
|
21
|
+
* Keying on name (not value) means two env vars holding the same secret get
|
|
22
|
+
* distinct sentinels, so each can have an independent host list.
|
|
23
|
+
*/
|
|
24
|
+
export class SentinelRegistry {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.byName = new Map();
|
|
27
|
+
this.bySentinel = new Map();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Return the sentinel for the credential named `name`, minting a fresh one
|
|
31
|
+
* on first use. The sentinel is `fake_value_<uuid4>`: long enough that an
|
|
32
|
+
* accidental collision with legitimate header content is negligible, and
|
|
33
|
+
* free of shell/URL metacharacters so it survives `--setenv` and
|
|
34
|
+
* `env NAME=value` unquoted.
|
|
35
|
+
*
|
|
36
|
+
* Idempotent on `name`: a repeat call returns the same sentinel and updates
|
|
37
|
+
* `realValue`/`injectHosts` in place so `updateConfig()` can change either
|
|
38
|
+
* without invalidating sentinels the sandboxed process has already read.
|
|
39
|
+
*/
|
|
40
|
+
register(name, realValue, injectHosts) {
|
|
41
|
+
const existing = this.byName.get(name);
|
|
42
|
+
if (existing !== undefined) {
|
|
43
|
+
existing.realValue = realValue;
|
|
44
|
+
existing.injectHosts = injectHosts;
|
|
45
|
+
return existing.sentinel;
|
|
46
|
+
}
|
|
47
|
+
const sentinel = SENTINEL_PREFIX + randomUUID();
|
|
48
|
+
const entry = { name, sentinel, realValue, injectHosts };
|
|
49
|
+
this.byName.set(name, entry);
|
|
50
|
+
this.bySentinel.set(sentinel, entry);
|
|
51
|
+
return sentinel;
|
|
52
|
+
}
|
|
53
|
+
/** Real value for `sentinel`, or undefined if not registered. */
|
|
54
|
+
lookupReal(sentinel) {
|
|
55
|
+
return this.bySentinel.get(sentinel)?.realValue;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Names of the registered credentials whose `injectHosts` cover
|
|
59
|
+
* `destHost`. Diagnostic helper: the proxy uses it to warn when a host is
|
|
60
|
+
* exempted from TLS termination (so substitution can never run there) but
|
|
61
|
+
* a masked credential is configured for injection at it.
|
|
62
|
+
*/
|
|
63
|
+
namesInjectableAt(destHost, matches) {
|
|
64
|
+
const names = [];
|
|
65
|
+
for (const e of this.byName.values()) {
|
|
66
|
+
if (e.injectHosts.some(p => matches(destHost, p)))
|
|
67
|
+
names.push(e.name);
|
|
68
|
+
}
|
|
69
|
+
return names;
|
|
70
|
+
}
|
|
71
|
+
/** Iterate registered `[sentinel, realValue]` pairs. */
|
|
72
|
+
*entries() {
|
|
73
|
+
for (const e of this.bySentinel.values())
|
|
74
|
+
yield [e.sentinel, e.realValue];
|
|
75
|
+
}
|
|
76
|
+
/** Number of registered sentinels. */
|
|
77
|
+
get size() {
|
|
78
|
+
return this.bySentinel.size;
|
|
79
|
+
}
|
|
80
|
+
/** Drop every mapping. Called on session teardown. */
|
|
81
|
+
clear() {
|
|
82
|
+
this.byName.clear();
|
|
83
|
+
this.bySentinel.clear();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Replace registered sentinels found in `headers` with their real values,
|
|
87
|
+
* in place. Each sentinel substitutes only when `destHost` matches one of
|
|
88
|
+
* THAT credential's `injectHosts` patterns (via `matches`); a sentinel
|
|
89
|
+
* whose host list does not cover `destHost` is left as the useless fake.
|
|
90
|
+
*
|
|
91
|
+
* Scans all header values rather than a fixed set — a sentinel showing up
|
|
92
|
+
* anywhere is the substitution trigger, regardless of header name
|
|
93
|
+
* (Authorization, X-Api-Key, Private-Token, ...).
|
|
94
|
+
*
|
|
95
|
+
* The caller remains responsible for transport gating (TLS-terminated path
|
|
96
|
+
* unless `allowPlaintextInject`).
|
|
97
|
+
*/
|
|
98
|
+
substituteInHeaders(headers, destHost, matches) {
|
|
99
|
+
if (this.bySentinel.size === 0)
|
|
100
|
+
return;
|
|
101
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
102
|
+
if (value === undefined)
|
|
103
|
+
continue;
|
|
104
|
+
if (Array.isArray(value)) {
|
|
105
|
+
for (let i = 0; i < value.length; i++) {
|
|
106
|
+
value[i] = this.substituteInString(value[i], destHost, matches);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
headers[name] = this.substituteInString(value, destHost, matches);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
substituteInString(s, destHost, matches) {
|
|
115
|
+
// Fast path: the sentinel prefix is fixed, so a header value that
|
|
116
|
+
// doesn't contain it cannot contain any sentinel.
|
|
117
|
+
if (!s.includes(SENTINEL_PREFIX))
|
|
118
|
+
return s;
|
|
119
|
+
let out = s;
|
|
120
|
+
for (const e of this.bySentinel.values()) {
|
|
121
|
+
if (!out.includes(e.sentinel))
|
|
122
|
+
continue;
|
|
123
|
+
if (!e.injectHosts.some(p => matches(destHost, p)))
|
|
124
|
+
continue;
|
|
125
|
+
out = out.split(e.sentinel).join(e.realValue);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=credential-sentinel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-sentinel.js","sourceRoot":"","sources":["../../src/sandbox/credential-sentinel.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain-pattern matching shared between runtime host filtering
|
|
3
|
+
* (sandbox-manager) and config-time validation (sandbox-config).
|
|
4
|
+
* Lives in its own module so the schema can import it without pulling
|
|
5
|
+
* in sandbox-manager (which imports the schema — circular).
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Match a hostname against a domain pattern.
|
|
9
|
+
*
|
|
10
|
+
* Patterns:
|
|
11
|
+
* - `*` matches everything (deny-all; the schema only accepts this in
|
|
12
|
+
* deniedDomains).
|
|
13
|
+
* - `*.example.com` matches any strict subdomain of example.com.
|
|
14
|
+
* - anything else matches exactly (case-insensitive).
|
|
15
|
+
*
|
|
16
|
+
* Wildcard suffix matching is refused for IP literals so an IPv6 zone-ID
|
|
17
|
+
* payload like `::ffff:1.2.3.4%x.allowed.com` cannot pass `.endsWith()`
|
|
18
|
+
* while the OS connects to the bare IP. isValidHost already rejects `%`,
|
|
19
|
+
* but we refuse here too for defence in depth.
|
|
20
|
+
*/
|
|
21
|
+
export declare function matchesDomainPattern(hostname: string, pattern: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Decide whether a per-credential `injectHosts` entry is reachable via
|
|
24
|
+
* `network.allowedDomains` — i.e. every concrete host that could match
|
|
25
|
+
* `injectHost` is allowed by at least one entry in `allowedDomains`.
|
|
26
|
+
*
|
|
27
|
+
* For an exact `injectHost` (`api.github.com`) this is just
|
|
28
|
+
* `matchesDomainPattern` against each allowed pattern.
|
|
29
|
+
*
|
|
30
|
+
* For a wildcard `injectHost` (`*.X`), an exact allowedDomain can never
|
|
31
|
+
* cover it (it admits only one host), so coverage requires an allowed
|
|
32
|
+
* wildcard `*.Y` whose base is `X` or an ancestor of `X` — e.g.
|
|
33
|
+
* `*.api.github.com` is covered by `*.github.com`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isInjectHostCoveredByAllowedDomains(injectHost: string, allowedDomains: readonly string[]): boolean;
|
|
36
|
+
//# sourceMappingURL=domain-pattern.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-pattern.d.ts","sourceRoot":"","sources":["../../src/sandbox/domain-pattern.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAST;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mCAAmC,CACjD,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,SAAS,MAAM,EAAE,GAChC,OAAO,CAUT"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain-pattern matching shared between runtime host filtering
|
|
3
|
+
* (sandbox-manager) and config-time validation (sandbox-config).
|
|
4
|
+
* Lives in its own module so the schema can import it without pulling
|
|
5
|
+
* in sandbox-manager (which imports the schema — circular).
|
|
6
|
+
*/
|
|
7
|
+
import { isIP } from 'node:net';
|
|
8
|
+
import { stripBrackets } from './parent-proxy.js';
|
|
9
|
+
/**
|
|
10
|
+
* Match a hostname against a domain pattern.
|
|
11
|
+
*
|
|
12
|
+
* Patterns:
|
|
13
|
+
* - `*` matches everything (deny-all; the schema only accepts this in
|
|
14
|
+
* deniedDomains).
|
|
15
|
+
* - `*.example.com` matches any strict subdomain of example.com.
|
|
16
|
+
* - anything else matches exactly (case-insensitive).
|
|
17
|
+
*
|
|
18
|
+
* Wildcard suffix matching is refused for IP literals so an IPv6 zone-ID
|
|
19
|
+
* payload like `::ffff:1.2.3.4%x.allowed.com` cannot pass `.endsWith()`
|
|
20
|
+
* while the OS connects to the bare IP. isValidHost already rejects `%`,
|
|
21
|
+
* but we refuse here too for defence in depth.
|
|
22
|
+
*/
|
|
23
|
+
export function matchesDomainPattern(hostname, pattern) {
|
|
24
|
+
const h = hostname.toLowerCase();
|
|
25
|
+
if (pattern === '*')
|
|
26
|
+
return true;
|
|
27
|
+
if (pattern.startsWith('*.')) {
|
|
28
|
+
if (isIP(stripBrackets(h)))
|
|
29
|
+
return false;
|
|
30
|
+
const baseDomain = pattern.substring(2).toLowerCase();
|
|
31
|
+
return h.endsWith('.' + baseDomain);
|
|
32
|
+
}
|
|
33
|
+
return h === pattern.toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Decide whether a per-credential `injectHosts` entry is reachable via
|
|
37
|
+
* `network.allowedDomains` — i.e. every concrete host that could match
|
|
38
|
+
* `injectHost` is allowed by at least one entry in `allowedDomains`.
|
|
39
|
+
*
|
|
40
|
+
* For an exact `injectHost` (`api.github.com`) this is just
|
|
41
|
+
* `matchesDomainPattern` against each allowed pattern.
|
|
42
|
+
*
|
|
43
|
+
* For a wildcard `injectHost` (`*.X`), an exact allowedDomain can never
|
|
44
|
+
* cover it (it admits only one host), so coverage requires an allowed
|
|
45
|
+
* wildcard `*.Y` whose base is `X` or an ancestor of `X` — e.g.
|
|
46
|
+
* `*.api.github.com` is covered by `*.github.com`.
|
|
47
|
+
*/
|
|
48
|
+
export function isInjectHostCoveredByAllowedDomains(injectHost, allowedDomains) {
|
|
49
|
+
if (!injectHost.startsWith('*.')) {
|
|
50
|
+
return allowedDomains.some(p => matchesDomainPattern(injectHost, p));
|
|
51
|
+
}
|
|
52
|
+
const injectBase = injectHost.slice(2).toLowerCase();
|
|
53
|
+
return allowedDomains.some(p => {
|
|
54
|
+
if (!p.startsWith('*.'))
|
|
55
|
+
return false;
|
|
56
|
+
const allowedBase = p.slice(2).toLowerCase();
|
|
57
|
+
return injectBase === allowedBase || injectBase.endsWith('.' + allowedBase);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=domain-pattern.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-pattern.js","sourceRoot":"","sources":["../../src/sandbox/domain-pattern.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAC/B,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,oBAAoB,CAClC,QAAgB,EAChB,OAAe;IAEf,MAAM,CAAC,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAA;IAChC,IAAI,OAAO,KAAK,GAAG;QAAE,OAAO,IAAI,CAAA;IAChC,IAAI,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAA;QACxC,MAAM,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;QACrD,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAG,GAAG,UAAU,CAAC,CAAA;IACrC,CAAC;IACD,OAAO,CAAC,KAAK,OAAO,CAAC,WAAW,EAAE,CAAA;AACpC,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,mCAAmC,CACjD,UAAkB,EAClB,cAAiC;IAEjC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACjC,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,oBAAoB,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAA;IACtE,CAAC;IACD,MAAM,UAAU,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;IACpD,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;QAC7B,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAA;QACrC,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;QAC5C,OAAO,UAAU,KAAK,WAAW,IAAI,UAAU,CAAC,QAAQ,CAAC,GAAG,GAAG,WAAW,CAAC,CAAA;IAC7E,CAAC,CAAC,CAAA;AACJ,CAAC"}
|