fix(sandbox): drop Authenticated Users from both restricting lists — close the C:\\-root escape, CIM unavailable in every confined mode
workspace-write now runs [logon SID, Everyone, orphan]: the two lists differ only by the orphan, and the keep-alive group (logon SID + Everyone) is the single shared invariant. The WMI namespace security check fails in BOTH modes (0x80041003), so CIM/Get-ComputerInfo are unavailable everywhere — the price of closing the C:\\-root tree-creation escape (AU:(AD) + AU:(OI)(CI)(IO)(M)) in workspace-write too. The unused WinLocalSid/WinInteractiveSid/WinAuthenticatedUserSid constants and their ABI-probe prints are removed; the enforcement 'full' claim now stands on a closed NTFS surface. New regression: a C:\\Users\\Public subdirectory write is denied under BOTH modes (the ambient-writable blind spot the review flagged — INTERACTIVE is absent from both lists). FAT-class (non-ACL) targets outside the granted roots remain writable (no security descriptors to intersect) — documented as a legacy residue, warn-only, not engineered around. Docs/design note/PR body updated in both languages (list I/J terminology gone everywhere).
This commit is contained in:
@@ -195,14 +195,9 @@ export class AclSandbox {
|
||||
this.sidAllocations.push(logonSid)
|
||||
const worldSid = makeWellKnownSid(api, abi.WinWorldSid)
|
||||
this.sidAllocations.push(worldSid)
|
||||
const authUserSid = makeWellKnownSid(api, abi.WinAuthenticatedUserSid)
|
||||
this.sidAllocations.push(authUserSid)
|
||||
const restricted = createRestrictedToken(
|
||||
api, currentToken, logonSid, writeSidPtr,
|
||||
{
|
||||
world: worldSid,
|
||||
authUser: authUserSid,
|
||||
},
|
||||
{ world: worldSid },
|
||||
this.mode,
|
||||
)
|
||||
this.token = restricted
|
||||
|
||||
@@ -15,9 +15,12 @@
|
||||
* - workspace-write: the workspace and temp directories carry the orphan-SID
|
||||
* Write grant; every other write is denied by the token intersection.
|
||||
* - read-only: STRICT zero grants — no directory is writable, not even the
|
||||
* NUL device (`> $null` fails with access denied); the token's restricting
|
||||
* list also drops Authenticated Users (CIM unavailable — documented in
|
||||
* README).
|
||||
* NUL device (`> $null` fails with access denied); the restricting list
|
||||
* carries no orphan SID, so a standing grant ACE from an earlier
|
||||
* workspace-write period stays inert. BOTH modes drop Authenticated Users
|
||||
* (CIM unavailable — documented in README) and INTERACTIVE/LOCAL (the
|
||||
* Public tree writes are denied); the two lists share the keep-alive group
|
||||
* (logon SID, EVERYONE) and differ only by the orphan.
|
||||
*
|
||||
* `--write-sid`: the seam's per-session grant contract — the CALLER has
|
||||
* already materialized the orphan-SID ACEs (once per session, server
|
||||
|
||||
@@ -101,44 +101,41 @@ function buildRestrictingSids(sids: readonly NativePtr[]): Buffer {
|
||||
return buffer
|
||||
}
|
||||
|
||||
/** The well-known SIDs packed into every restricted token's restricting list. */
|
||||
/** The well-known SID packed into every restricted token's restricting list. */
|
||||
export interface RestrictingSidSet {
|
||||
world: NativePtr
|
||||
authUser: NativePtr
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the write-restricted token with the mode-selected restricting list
|
||||
* (dual lists verified on Win11 26200, see the POC-worktree restrict-variant
|
||||
* harness):
|
||||
* - list I (read-only): [logon SID, EVERYONE]
|
||||
* - list J (workspace-write): [logon SID, EVERYONE, Authenticated Users, orphan]
|
||||
* (verified on Win11 26200, see the POC-worktree restrict-variant harness):
|
||||
* - read-only: [logon SID, EVERYONE]
|
||||
* - workspace-write: [logon SID, EVERYONE, orphan]
|
||||
*
|
||||
* The logon SID and EVERYONE are shared: they keep the early startup chain
|
||||
* (0xC0000142 without them) and CNG (`\Device\CNG` write trustee — pwsh
|
||||
* crashes 0xE0434352 without EVERYONE) alive. Authenticated Users exists in
|
||||
* list J ONLY because the CIM path's WMI namespace security check requires it
|
||||
* (0x80041003 otherwise) — read-only drops it for a zero ambient-write
|
||||
* surface (it closes the host's C:\-root tree-creation escape, where
|
||||
* `AU:(AD)` + `AU:(OI)(CI)(IO)(M)` ACEs stand) at the cost of CIM
|
||||
* unavailability; documented in README. List I also carries NO orphan: a
|
||||
* standing grant ACE from an earlier workspace-write period (a
|
||||
* `/permission` mode downgrade, or a crash-resumed session) must stay INERT
|
||||
* under read-only — the WRITE_RESTRICTED pass-2 check grants only what the
|
||||
* restricting list carries, so omitting the orphan keeps read-only strictly
|
||||
* zero-grant even with stale ACEs standing, while the unrevoked ACE keeps
|
||||
* the re-upgrade free (the seam's grant map hits it — no re-propagation).
|
||||
* INTERACTIVE/LOCAL are absent from BOTH lists — the host's Public tree
|
||||
* grants write to INTERACTIVE, so removing it closes that escape. S-1-2-1
|
||||
* (console logon) is intentionally absent: see win32-abi.ts for the
|
||||
* verified failure modes. FAILS CLOSED: any failure throws — never spawn
|
||||
* unrestricted.
|
||||
* The logon SID + EVERYONE keep-alive group is shared by both modes: early
|
||||
* DLL init dies with 0xC0000142 and CNG (`\Device\CNG` write trustee —
|
||||
* pwsh crashes 0xE0434352) fails without them. The orphan SID joins ONLY
|
||||
* workspace-write — read-only carries no orphan, so a standing grant ACE
|
||||
* from an earlier workspace-write period (a `/permission` mode downgrade, or
|
||||
* a crash-resumed session) stays INERT under read-only: the WRITE_RESTRICTED
|
||||
* pass-2 check grants only what the restricting list carries, keeping
|
||||
* read-only strictly zero-grant even with stale ACEs standing, while the
|
||||
* unrevoked ACE keeps the re-upgrade free (the seam's grant map hits it — no
|
||||
* re-propagation). Authenticated Users is absent from BOTH lists: the WMI
|
||||
* namespace security check fails (0x80041003), so CIM is unavailable in
|
||||
* every confined mode, and the C:\-root tree-creation escape (standing
|
||||
* `AU:(AD)` + `AU:(OI)(CI)(IO)(M)` ACEs) is closed in both — documented in
|
||||
* README. INTERACTIVE/LOCAL are absent from BOTH lists too — the host's
|
||||
* Public tree grants write to INTERACTIVE, so removing it closes that
|
||||
* escape. S-1-2-1 (console logon) is intentionally absent: see win32-abi.ts
|
||||
* for the verified failure modes. FAILS CLOSED: any failure throws — never
|
||||
* spawn unrestricted.
|
||||
* @param api - the binding table.
|
||||
* @param currentToken - the process token to restrict.
|
||||
* @param logonSid - the copied logon session SID.
|
||||
* @param writeSid - the orphan SID forming the write allowlist (list J only).
|
||||
* @param writeSid - the orphan SID forming the write allowlist (workspace-write only).
|
||||
* @param known - the well-known SIDs entering the restricting list.
|
||||
* @param mode - selects the restricting list (I for read-only, J for workspace-write).
|
||||
* @param mode - selects the restricting list (workspace-write adds the orphan).
|
||||
* @returns the restricted token handle.
|
||||
*/
|
||||
export function createRestrictedToken(
|
||||
@@ -151,7 +148,7 @@ export function createRestrictedToken(
|
||||
): NativePtr {
|
||||
const restrictingSids = buildRestrictingSids(mode === 'read-only'
|
||||
? [logonSid, known.world]
|
||||
: [logonSid, known.world, known.authUser, writeSid])
|
||||
: [logonSid, known.world, writeSid])
|
||||
const tokenSlot = allocPtrSlot()
|
||||
const created = api.createRestrictedToken(
|
||||
currentToken,
|
||||
|
||||
@@ -79,20 +79,8 @@ export const LUA_TOKEN = 0x4
|
||||
export const WRITE_RESTRICTED = 0x8
|
||||
|
||||
// WELL_KNOWN_SID_TYPE (winnt.h lines ~3369-3407)
|
||||
/** WinWorldSid: S-1-1-0 (Everyone). */
|
||||
/** WinWorldSid: S-1-1-0 (Everyone) — the only well-known SID the restricted tokens use (keep-alive group; see token.ts). */
|
||||
export const WinWorldSid = 1
|
||||
/**
|
||||
* WinLocalSid: S-1-2-0 (LOCAL) — safe, created successfully on every init
|
||||
* (it sits in every restricted token's restricting list). The
|
||||
* CreateWellKnownSid ERROR_INVALID_PARAMETER failure documented in this
|
||||
* module's header comment belongs to WinLocalLogonSid (S-1-2-1), NOT to
|
||||
* this type.
|
||||
*/
|
||||
export const WinLocalSid = 2
|
||||
/** WinInteractiveSid: S-1-5-4 (INTERACTIVE). */
|
||||
export const WinInteractiveSid = 11
|
||||
/** WinAuthenticatedUserSid: S-1-5-11 (Authenticated Users). */
|
||||
export const WinAuthenticatedUserSid = 17
|
||||
|
||||
// TOKEN_INFORMATION_CLASS (winnt.h line ~3963: TokenUser=1, TokenGroups=2)
|
||||
/** TokenGroups: GetTokenInformation class returning the token's group SIDs. */
|
||||
|
||||
Reference in New Issue
Block a user