Skip to content

Windows write jail is still bypassable on profiles that set denyRead #869

Description

@Vasanthdev2004

#865 removed the World SID (S-1-1-0, Everyone) from the WRITE_RESTRICTED token, which is the shape every Windows user gets by default. It could not remove it from the other shape, and that one is still bypassable.

A profile with a non-empty denyRead builds its restricted token without WRITE_RESTRICTED, and Everyone stays on the restricted-SID list. A restricted SID is a key to every object whose DACL names it, and every principal carries Everyone, so the restricted-SID check passes for free on any path whose DACL grants Everyone write. The workspace write jail then falls back to the ordinary user's own permissions, which is the exact boundary this token exists to be stricter than.

No privilege, no symlink and no race is needed. An Everyone-writable directory is enough, and share roots opened with Everyone:F plus loose installer ACLs supply them.

Site: createWindowsRestrictedTokenFromBase in internal/sandbox/windows_token_windows.go.

Who is affected. Only Windows users who set denyRead themselves. Zero never populates it on Windows: credentialDenyReadPaths returns nil there (internal/sandbox/profile.go:166), so the default posture runs the token #865 fixed.

Why #865 could not cover this shape. Without WRITE_RESTRICTED the restricted-SID check applies to reads as well as writes, and default Windows DACLs grant BUILTIN\Users rather than anything on this list. A token without Everyone cannot open cmd.exe: the process dies at launch with STATUS_ACCESS_DENIED (0xC0000022) before it runs anything. That was tried first and it breaks the unelevated sandbox outright. So #865 fixed the shape where nothing depends on Everyone and left this one documented in the code.

What closing it needs. A read-side grant that is not a universal group. AppContainer or LPAC with an explicit capability SID is the obvious candidate. A narrower interim: keep Everyone off the restricted-SID list and grant read through the per-sandbox capability SID on the executable search paths, trading machine setup for enforcement.

Same class, worth auditing at the same time. Authenticated Users, Users, INTERACTIVE and BATCH each produce this bypass if they reach that list. #640 adds Users and Authenticated Users behind a broadenReadSIDs parameter that its own doc comment says callers must always pass false, plus a guard refusing to combine it with writeRestricted. That is the right posture, and it needs to stay that way.

Do not close it by widening the default. #662 asks for the credential deny-read defaults to apply on Windows. That makes denyRead non-empty for every Windows user and moves all of them onto this token shape, turning a config almost nobody hits into the default one. #662 needs this fixed first, not the other way round.

Related: #865 (fix for the default shape), #612 (why DenyRead uses the non-WRITE_RESTRICTED token), #640 (proposes rejecting denyRead on these tiers outright, which is a different trade for the same problem).

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions