@sysid/sandbox-runtime-improved 0.0.52-sysid.1 → 0.0.55-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.
Files changed (79) hide show
  1. package/README.md +5 -2
  2. package/dist/cli.js +91 -7
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.d.ts +4 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +3 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/sandbox/http-proxy.d.ts +7 -0
  9. package/dist/sandbox/http-proxy.d.ts.map +1 -1
  10. package/dist/sandbox/http-proxy.js +20 -0
  11. package/dist/sandbox/http-proxy.js.map +1 -1
  12. package/dist/sandbox/linux-sandbox-utils.d.ts +2 -0
  13. package/dist/sandbox/linux-sandbox-utils.d.ts.map +1 -1
  14. package/dist/sandbox/linux-sandbox-utils.js +107 -46
  15. package/dist/sandbox/linux-sandbox-utils.js.map +1 -1
  16. package/dist/sandbox/macos-sandbox-utils.d.ts +3 -0
  17. package/dist/sandbox/macos-sandbox-utils.d.ts.map +1 -1
  18. package/dist/sandbox/macos-sandbox-utils.js +32 -3
  19. package/dist/sandbox/macos-sandbox-utils.js.map +1 -1
  20. package/dist/sandbox/mitm-leaf.d.ts.map +1 -1
  21. package/dist/sandbox/mitm-leaf.js +18 -5
  22. package/dist/sandbox/mitm-leaf.js.map +1 -1
  23. package/dist/sandbox/sandbox-config.d.ts +63 -2
  24. package/dist/sandbox/sandbox-config.d.ts.map +1 -1
  25. package/dist/sandbox/sandbox-config.js +52 -2
  26. package/dist/sandbox/sandbox-config.js.map +1 -1
  27. package/dist/sandbox/sandbox-manager.d.ts +5 -0
  28. package/dist/sandbox/sandbox-manager.d.ts.map +1 -1
  29. package/dist/sandbox/sandbox-manager.js +208 -44
  30. package/dist/sandbox/sandbox-manager.js.map +1 -1
  31. package/dist/sandbox/sandbox-schemas.d.ts +9 -3
  32. package/dist/sandbox/sandbox-schemas.d.ts.map +1 -1
  33. package/dist/sandbox/sandbox-utils.d.ts +1 -1
  34. package/dist/sandbox/sandbox-utils.d.ts.map +1 -1
  35. package/dist/sandbox/sandbox-utils.js +46 -18
  36. package/dist/sandbox/sandbox-utils.js.map +1 -1
  37. package/dist/sandbox/socks-proxy.d.ts +6 -0
  38. package/dist/sandbox/socks-proxy.d.ts.map +1 -1
  39. package/dist/sandbox/socks-proxy.js +11 -0
  40. package/dist/sandbox/socks-proxy.js.map +1 -1
  41. package/dist/sandbox/tls-terminate-proxy.js +18 -4
  42. package/dist/sandbox/tls-terminate-proxy.js.map +1 -1
  43. package/dist/sandbox/windows-sandbox-utils.d.ts +224 -0
  44. package/dist/sandbox/windows-sandbox-utils.d.ts.map +1 -0
  45. package/dist/sandbox/windows-sandbox-utils.js +433 -0
  46. package/dist/sandbox/windows-sandbox-utils.js.map +1 -0
  47. package/dist/vendor/srt-win/Cargo.lock +361 -0
  48. package/dist/vendor/srt-win/Cargo.toml +46 -0
  49. package/dist/vendor/srt-win/ci/cleanup.ps1 +49 -0
  50. package/dist/vendor/srt-win/ci/smoke-exec.ps1 +446 -0
  51. package/dist/vendor/srt-win/ci/smoke.ps1 +307 -0
  52. package/dist/vendor/srt-win/src/job.rs +102 -0
  53. package/dist/vendor/srt-win/src/launch.rs +732 -0
  54. package/dist/vendor/srt-win/src/lib.rs +20 -0
  55. package/dist/vendor/srt-win/src/main.rs +669 -0
  56. package/dist/vendor/srt-win/src/self_protect.rs +169 -0
  57. package/dist/vendor/srt-win/src/sid.rs +296 -0
  58. package/dist/vendor/srt-win/src/token.rs +341 -0
  59. package/dist/vendor/srt-win/src/util.rs +42 -0
  60. package/dist/vendor/srt-win/src/wfp.rs +992 -0
  61. package/dist/vendor/srt-win/src/winsta.rs +209 -0
  62. package/dist/vendor/srt-win/tests/sd_access_check_matrix.rs +259 -0
  63. package/package.json +2 -2
  64. package/vendor/srt-win/Cargo.lock +361 -0
  65. package/vendor/srt-win/Cargo.toml +46 -0
  66. package/vendor/srt-win/ci/cleanup.ps1 +49 -0
  67. package/vendor/srt-win/ci/smoke-exec.ps1 +446 -0
  68. package/vendor/srt-win/ci/smoke.ps1 +307 -0
  69. package/vendor/srt-win/src/job.rs +102 -0
  70. package/vendor/srt-win/src/launch.rs +732 -0
  71. package/vendor/srt-win/src/lib.rs +20 -0
  72. package/vendor/srt-win/src/main.rs +669 -0
  73. package/vendor/srt-win/src/self_protect.rs +169 -0
  74. package/vendor/srt-win/src/sid.rs +296 -0
  75. package/vendor/srt-win/src/token.rs +341 -0
  76. package/vendor/srt-win/src/util.rs +42 -0
  77. package/vendor/srt-win/src/wfp.rs +992 -0
  78. package/vendor/srt-win/src/winsta.rs +209 -0
  79. package/vendor/srt-win/tests/sd_access_check_matrix.rs +259 -0
@@ -0,0 +1,341 @@
1
+ //! Restricted-token construction for the deny-only-group sandbox.
2
+ //!
3
+ //! Token shape:
4
+ //! - `SidsToDisable = [<group_sid>, BUILTIN\Administrators]` — flips
5
+ //! them to `SE_GROUP_USE_FOR_DENY_ONLY` without touching the
6
+ //! restricting list.
7
+ //! - `LUA_TOKEN` flag — token reads as a normal limited-user token
8
+ //! to NT components.
9
+ //! - All privileges deleted except `SeChangeNotifyPrivilege`.
10
+ //! - Integrity Level = Medium (same as a normal user process).
11
+ //! - No `RestrictingSids` array — that breaks Schannel/LSA RPC.
12
+ //!
13
+ //! WFP's `ALE_USER_ID` AccessCheck honours
14
+ //! `SE_GROUP_USE_FOR_DENY_ONLY`, so an SDDL ACE
15
+ //! `(A;;CC;;;<group_sid>)` matches only when the group is *enabled*
16
+ //! — i.e. on the broker, never on the sandbox child.
17
+
18
+ use anyhow::{anyhow, Context, Result};
19
+ use std::ffi::c_void;
20
+ use std::mem::size_of;
21
+ use windows::Win32::Foundation::{HANDLE, LUID};
22
+ use windows::Win32::Security::{
23
+ AddAccessAllowedAce, AllocateAndInitializeSid, CreateRestrictedToken,
24
+ DuplicateTokenEx, FreeSid, GetLengthSid, GetTokenInformation,
25
+ InitializeAcl, LookupPrivilegeValueW, SecurityImpersonation,
26
+ SetTokenInformation, TokenDefaultDacl, TokenGroups, TokenIntegrityLevel,
27
+ TokenPrimary, TokenPrivileges, ACL, ACL_REVISION, LUA_TOKEN,
28
+ LUID_AND_ATTRIBUTES, PSID, SID_AND_ATTRIBUTES, SID_IDENTIFIER_AUTHORITY,
29
+ TOKEN_ALL_ACCESS, TOKEN_DEFAULT_DACL, TOKEN_GROUPS,
30
+ TOKEN_INFORMATION_CLASS, TOKEN_MANDATORY_LABEL, TOKEN_PRIVILEGES,
31
+ };
32
+ use windows::Win32::System::SystemServices::SE_GROUP_LOGON_ID;
33
+ use windows::Win32::System::Threading::{
34
+ GetCurrentProcess, OpenProcessToken,
35
+ };
36
+
37
+ use crate::sid::LocalPsid;
38
+ use crate::util::{pcwstr, wstr};
39
+
40
+ /// Medium IL (`SECURITY_MANDATORY_MEDIUM_RID`). The sandbox child
41
+ /// runs at Medium — same as normal user processes — so Schannel /
42
+ /// LSA / registry edge cases that fire at Low IL don't apply.
43
+ pub const IL_MEDIUM: u32 = 0x2000;
44
+
45
+ /// `BUILTIN\Administrators`. Always added to `SidsToDisable` so an
46
+ /// elevated broker still produces a non-admin child.
47
+ const SID_BUILTIN_ADMINS: &str = "S-1-5-32-544";
48
+
49
+ /// `SE_GROUP_INTEGRITY` attribute (winnt.h). Required on the
50
+ /// `TOKEN_MANDATORY_LABEL.Label.Attributes` field.
51
+ const SE_GROUP_INTEGRITY: u32 = 0x0000_0020;
52
+
53
+ /// Open this process's primary token with full access.
54
+ pub fn open_self_token() -> Result<HANDLE> {
55
+ unsafe {
56
+ let mut h = HANDLE::default();
57
+ OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &mut h)
58
+ .context("OpenProcessToken")?;
59
+ Ok(h)
60
+ }
61
+ }
62
+
63
+ /// Build the deny-only-group restricted token from `base` (the
64
+ /// broker's own token). `group_sid` is the discriminator group that
65
+ /// gets flipped deny-only. Returns a non-primary token; the caller
66
+ /// duplicates to primary via [`to_primary`].
67
+ pub fn make_sandbox_token(base: HANDLE, group_sid: &str) -> Result<HANDLE> {
68
+ // SIDs to disable. `LocalPsid` is RAII over
69
+ // `ConvertStringSidToSidW` and frees with `LocalFree` on drop —
70
+ // the donor used raw `PSID` + `FreeSid` here, which is the wrong
71
+ // free fn for that allocator.
72
+ //
73
+ // CI uses `group_sid == BUILTIN\Administrators` (the only SID
74
+ // that's both on the runner's token without a logout AND
75
+ // discriminator-shaped). Dedup so `CreateRestrictedToken`
76
+ // doesn't see the same SID twice in `SidsToDisable`.
77
+ let group = LocalPsid::from_string(group_sid)
78
+ .with_context(|| format!("parse group SID '{group_sid}'"))?;
79
+ let admins;
80
+ let mut psids: Vec<PSID> = vec![group.as_psid()];
81
+ if !group_sid.eq_ignore_ascii_case(SID_BUILTIN_ADMINS) {
82
+ admins = LocalPsid::from_string(SID_BUILTIN_ADMINS)?;
83
+ psids.push(admins.as_psid());
84
+ }
85
+ let disable: Vec<SID_AND_ATTRIBUTES> = psids
86
+ .into_iter()
87
+ .map(|s| SID_AND_ATTRIBUTES { Sid: s, Attributes: 0 })
88
+ .collect();
89
+
90
+ // Privileges to delete: everything in `base` except
91
+ // SeChangeNotifyPrivilege.
92
+ let to_delete = privileges_except(base, &["SeChangeNotifyPrivilege"])?;
93
+
94
+ let mut out = HANDLE::default();
95
+ unsafe {
96
+ CreateRestrictedToken(
97
+ base,
98
+ LUA_TOKEN,
99
+ Some(&disable),
100
+ if to_delete.is_empty() { None } else { Some(&to_delete) },
101
+ None, // RestrictingSids — intentionally empty
102
+ &mut out,
103
+ )
104
+ .with_context(|| {
105
+ format!("CreateRestrictedToken(disable=[Admins,{group_sid}])")
106
+ })?;
107
+ }
108
+ // `admins` / `group` LocalPsid drop here → LocalFree.
109
+
110
+ // RAII-own `out` so a `?` from set_il/set_default_dacl below
111
+ // closes it. Local — token.rs is the only module that holds
112
+ // raw HANDLEs across fallible calls outside launch.rs.
113
+ struct Guard(HANDLE);
114
+ impl Drop for Guard {
115
+ fn drop(&mut self) {
116
+ unsafe {
117
+ let _ = windows::Win32::Foundation::CloseHandle(self.0);
118
+ }
119
+ }
120
+ }
121
+ let guard = Guard(out);
122
+ set_il(guard.0, IL_MEDIUM)?;
123
+ set_default_dacl(guard.0, base)?;
124
+ // Disarm and return the raw handle to the caller.
125
+ let h = guard.0;
126
+ std::mem::forget(guard);
127
+ Ok(h)
128
+ }
129
+
130
+ /// Duplicate to a primary token (`CreateProcessAsUserW` requires a
131
+ /// primary).
132
+ pub fn to_primary(token: HANDLE) -> Result<HANDLE> {
133
+ unsafe {
134
+ let mut out = HANDLE::default();
135
+ DuplicateTokenEx(
136
+ token,
137
+ TOKEN_ALL_ACCESS,
138
+ None,
139
+ SecurityImpersonation,
140
+ TokenPrimary,
141
+ &mut out,
142
+ )
143
+ .context("DuplicateTokenEx(primary)")?;
144
+ Ok(out)
145
+ }
146
+ }
147
+
148
+ /// Set the token's integrity level via a mandatory-label SID built
149
+ /// with `AllocateAndInitializeSid` (so [`FreeSid`] is the correct
150
+ /// release fn — the one place in this crate where it is).
151
+ fn set_il(tok: HANDLE, rid: u32) -> Result<()> {
152
+ unsafe {
153
+ let ml_auth = SID_IDENTIFIER_AUTHORITY {
154
+ Value: [0, 0, 0, 0, 0, 16],
155
+ };
156
+ let mut sid = PSID::default();
157
+ AllocateAndInitializeSid(
158
+ &ml_auth, 1, rid, 0, 0, 0, 0, 0, 0, 0, &mut sid,
159
+ )
160
+ .context("AllocateAndInitializeSid(mandatory label)")?;
161
+ let tml = TOKEN_MANDATORY_LABEL {
162
+ Label: SID_AND_ATTRIBUTES {
163
+ Sid: sid,
164
+ Attributes: SE_GROUP_INTEGRITY,
165
+ },
166
+ };
167
+ let r = SetTokenInformation(
168
+ tok,
169
+ TokenIntegrityLevel,
170
+ &tml as *const _ as *const c_void,
171
+ size_of::<TOKEN_MANDATORY_LABEL>() as u32 + GetLengthSid(sid),
172
+ );
173
+ // Free regardless of outcome.
174
+ FreeSid(sid);
175
+ r.context("SetTokenInformation(IntegrityLevel)")?;
176
+ Ok(())
177
+ }
178
+ }
179
+
180
+ /// Rewrite the token's default DACL so objects the child *creates*
181
+ /// (process, thread, mutex, …) are accessible to SYSTEM and to the
182
+ /// logon session. The donor leaked the SYSTEM SID here; this version
183
+ /// uses [`LocalPsid`] so it's freed on return.
184
+ ///
185
+ /// We deliberately keep the **logon SID** grant rather than switching
186
+ /// it to the discriminator group — switching would prevent siblings
187
+ /// inside the sandbox (e.g. a parallel build's worker processes) from
188
+ /// opening each other's pipes/mutexes, since they all hold the group
189
+ /// deny-only.
190
+ fn set_default_dacl(tok: HANDLE, base: HANDLE) -> Result<()> {
191
+ let system = LocalPsid::from_string("S-1-5-18")?;
192
+
193
+ // Find the logon-session SID in the base token's groups.
194
+ let groups_buf = get_token_info(base, TokenGroups)?;
195
+ let logon = unsafe {
196
+ let tg = &*(groups_buf.as_ptr() as *const TOKEN_GROUPS);
197
+ let arr = std::slice::from_raw_parts(
198
+ tg.Groups.as_ptr(),
199
+ tg.GroupCount as usize,
200
+ );
201
+ arr.iter()
202
+ .find(|g| g.Attributes & (SE_GROUP_LOGON_ID as u32) != 0)
203
+ .map(|g| g.Sid)
204
+ };
205
+
206
+ let sids: Vec<PSID> = match logon {
207
+ Some(l) => vec![system.as_psid(), l],
208
+ None => vec![system.as_psid()],
209
+ };
210
+
211
+ // Sized ACL: header + per-ACE (8-byte fixed prefix + SID body).
212
+ const ACE_FIXED: usize = 8;
213
+ let mut total = size_of::<ACL>();
214
+ for s in &sids {
215
+ total += ACE_FIXED + unsafe { GetLengthSid(*s) } as usize;
216
+ }
217
+ total = (total + 3) & !3;
218
+ let mut buf = vec![0u8; total];
219
+ let acl = buf.as_mut_ptr() as *mut ACL;
220
+ unsafe {
221
+ InitializeAcl(acl, total as u32, ACL_REVISION)
222
+ .context("InitializeAcl(default DACL)")?;
223
+ // GENERIC_ALL.
224
+ const GENERIC_ALL: u32 = 0x1000_0000;
225
+ for s in &sids {
226
+ AddAccessAllowedAce(acl, ACL_REVISION, GENERIC_ALL, *s)
227
+ .context("AddAccessAllowedAce(default DACL)")?;
228
+ }
229
+ let tdd = TOKEN_DEFAULT_DACL { DefaultDacl: acl };
230
+ SetTokenInformation(
231
+ tok,
232
+ TokenDefaultDacl,
233
+ &tdd as *const _ as *const c_void,
234
+ size_of::<TOKEN_DEFAULT_DACL>() as u32,
235
+ )
236
+ .context("SetTokenInformation(DefaultDacl)")?;
237
+ }
238
+ // `groups_buf` (which backs `logon`) and `system` both drop here.
239
+ Ok(())
240
+ }
241
+
242
+ fn get_token_info(tok: HANDLE, cls: TOKEN_INFORMATION_CLASS) -> Result<Vec<u8>> {
243
+ unsafe {
244
+ let mut len = 0u32;
245
+ let _ = GetTokenInformation(tok, cls, None, 0, &mut len);
246
+ if len == 0 {
247
+ return Err(anyhow!(
248
+ "GetTokenInformation({cls:?}) sizing returned 0"
249
+ ));
250
+ }
251
+ let mut buf = vec![0u8; len as usize];
252
+ GetTokenInformation(
253
+ tok,
254
+ cls,
255
+ Some(buf.as_mut_ptr() as *mut c_void),
256
+ len,
257
+ &mut len,
258
+ )
259
+ .with_context(|| format!("GetTokenInformation({cls:?})"))?;
260
+ Ok(buf)
261
+ }
262
+ }
263
+
264
+ /// Every privilege LUID in `base` except those named in `keep`.
265
+ fn privileges_except(
266
+ base: HANDLE,
267
+ keep: &[&str],
268
+ ) -> Result<Vec<LUID_AND_ATTRIBUTES>> {
269
+ let keep_luids: Vec<LUID> = keep
270
+ .iter()
271
+ .filter_map(|n| {
272
+ let mut l = LUID::default();
273
+ let w = wstr(n);
274
+ unsafe { LookupPrivilegeValueW(None, pcwstr(&w), &mut l).ok()? };
275
+ Some(l)
276
+ })
277
+ .collect();
278
+ let buf = get_token_info(base, TokenPrivileges)?;
279
+ unsafe {
280
+ let tp = &*(buf.as_ptr() as *const TOKEN_PRIVILEGES);
281
+ let arr = std::slice::from_raw_parts(
282
+ tp.Privileges.as_ptr(),
283
+ tp.PrivilegeCount as usize,
284
+ );
285
+ Ok(arr
286
+ .iter()
287
+ .filter(|p| {
288
+ !keep_luids.iter().any(|k| {
289
+ k.LowPart == p.Luid.LowPart && k.HighPart == p.Luid.HighPart
290
+ })
291
+ })
292
+ .map(|p| LUID_AND_ATTRIBUTES {
293
+ Luid: p.Luid,
294
+ Attributes: Default::default(),
295
+ })
296
+ .collect())
297
+ }
298
+ }
299
+
300
+ #[cfg(test)]
301
+ mod tests {
302
+ use super::*;
303
+ use windows::Win32::Foundation::CloseHandle;
304
+
305
+ #[test]
306
+ fn restricted_token_builds() {
307
+ // Group SID is BUILTIN\Users — always present, so the test
308
+ // doesn't depend on the discriminator group being installed.
309
+ let base = open_self_token().expect("open_self_token");
310
+ let r = make_sandbox_token(base, "S-1-5-32-545");
311
+ unsafe {
312
+ let _ = CloseHandle(base);
313
+ }
314
+ let tok = r.expect("make_sandbox_token");
315
+ let prim = to_primary(tok).expect("to_primary");
316
+ unsafe {
317
+ let _ = CloseHandle(tok);
318
+ let _ = CloseHandle(prim);
319
+ }
320
+ }
321
+
322
+ #[test]
323
+ fn privileges_except_keeps_change_notify() {
324
+ let base = open_self_token().expect("open_self_token");
325
+ let to_delete =
326
+ privileges_except(base, &["SeChangeNotifyPrivilege"]).unwrap();
327
+ // Resolve SeChangeNotifyPrivilege's LUID and assert it's NOT
328
+ // in the deletion set.
329
+ let mut keep = LUID::default();
330
+ let w = wstr("SeChangeNotifyPrivilege");
331
+ unsafe {
332
+ LookupPrivilegeValueW(None, pcwstr(&w), &mut keep).unwrap();
333
+ let _ = CloseHandle(base);
334
+ }
335
+ assert!(
336
+ !to_delete.iter().any(|p| p.Luid.LowPart == keep.LowPart
337
+ && p.Luid.HighPart == keep.HighPart),
338
+ "SeChangeNotifyPrivilege must not be in the deletion set"
339
+ );
340
+ }
341
+ }
@@ -0,0 +1,42 @@
1
+ //! Small Win32 string helpers shared by `sid.rs` and `wfp.rs`.
2
+
3
+ use std::ffi::c_void;
4
+ use windows::core::{PCWSTR, PWSTR};
5
+ use windows::Win32::Foundation::{LocalFree, HLOCAL};
6
+
7
+ /// UTF-8 → NUL-terminated UTF-16 buffer. Keep the returned `Vec`
8
+ /// alive for as long as the resulting `PCWSTR` / `PWSTR` is in use.
9
+ pub fn wstr(s: &str) -> Vec<u16> {
10
+ s.encode_utf16().chain(std::iter::once(0)).collect()
11
+ }
12
+
13
+ /// Borrow a `Vec<u16>` from `wstr` as a `PCWSTR`.
14
+ pub fn pcwstr(buf: &[u16]) -> PCWSTR {
15
+ PCWSTR(buf.as_ptr())
16
+ }
17
+
18
+ /// Read a NUL-terminated `PWSTR` (typically returned by a Win32 API
19
+ /// that allocates) into an owned `String`. Caller still owns the
20
+ /// underlying allocation.
21
+ pub fn from_pwstr(p: PWSTR) -> String {
22
+ if p.is_null() {
23
+ return String::new();
24
+ }
25
+ let mut len = 0usize;
26
+ unsafe {
27
+ while *p.0.add(len) != 0 {
28
+ len += 1;
29
+ }
30
+ }
31
+ let slice = unsafe { std::slice::from_raw_parts(p.0, len) };
32
+ String::from_utf16_lossy(slice)
33
+ }
34
+
35
+ /// `LocalFree` a pointer returned by a Win32 API documented to require
36
+ /// it (e.g. `ConvertSidToStringSidW`,
37
+ /// `ConvertStringSecurityDescriptorToSecurityDescriptorW`).
38
+ pub fn local_free(p: *mut c_void) {
39
+ unsafe {
40
+ let _ = LocalFree(Some(HLOCAL(p)));
41
+ }
42
+ }