CreateRestrictedToken returns ERROR_INVALID_PARAMETER (87) with a capability SID but succeeds with Everyone SID

Devin Lewis 0 Reputation points
2026-10-17T01:01:49.0866667+00:00

CreateRestrictedToken returns ERROR_INVALID_PARAMETER (87) with a derived capability SID, but succeeds with Everyone SID

Category: Windows development → Windows API - Win32

I'm investigating a Windows access-token compatibility issue involving CreateRestrictedToken and capability SIDs generated through DeriveCapabilitySidsFromName.

My goal is to create a restricted, non-AppContainer worker token that can read protected inputs but write only to explicitly authorized scratch locations.

Environment

Windows build: 10.0.26300, x64

Caller: Medium-integrity, non-elevated process

Source token: Primary token with Limited elevation type

TokenHasRestrictions: 1

TokenRestrictedSids: Empty

TokenIsAppContainer: 0

Source-token access rights: 0x008B

The token appears consistent with UAC filtering, but I have not established that its filtering history causes the failure.

API invocation

Both calls used the same original token handle and the same CreateRestrictedToken argument pattern:

Flags: 0x9 (DISABLE_MAX_PRIVILEGE | WRITE_RESTRICTED)

DisableSidCount: 0

SidsToDisable: NULL

DeletePrivilegeCount: 0

PrivilegesToDelete: NULL

RestrictedSidCount: 1

SidsToRestrict: Pointer to one SID_AND_ATTRIBUTES

SID_AND_ATTRIBUTES.Attributes: 0

NewTokenHandle: Valid output-handle pointer

The experiment used the same process, original token, native function declaration, argument counts, flags and error-capture method.

Call A — Derived capability SID

The restricting SID came from the CapabilitySids output of DeriveCapabilitySidsFromName, using an application-specific capability name.

SID namespace: S-1-15-3-1024-...

SID length: 48 bytes

IsValidSid: Passed

String conversion and round-trip validation: Passed

SID absent from the source token's ordinary TokenGroups

CreateRestrictedToken: FALSE

Immediate GetLastError: 87 (ERROR_INVALID_PARAMETER)

Output token handle: NULL

Call B — Everyone SID

The restricting SID was S-1-1-0.

SID length: 12 bytes

IsValidSid: Passed

String conversion and round-trip validation: Passed

Everyone present and enabled in the source token's ordinary groups

CreateRestrictedToken: TRUE

Valid output token handle returned and closed successfully

Call A preceded Call B. I have not performed a reverse-order experiment or inspected the successful output token's restricting-SID list.

Questions

Are AppAuthority capability SIDs returned in CapabilitySids supported as restricting SIDs in CreateRestrictedToken for ordinary non-AppContainer primary tokens? Are there documented restrictions involving SID namespace, group membership or token-filtering state?

When TokenHasRestrictions is true but TokenRestrictedSids is empty, how should the documented restricted-SID intersection behavior be interpreted? Does the source token's UAC-filtered state affect which restricting SIDs can be accepted?

Is there a documented reason why a structurally valid derived capability SID could produce ERROR_INVALID_PARAMETER while Everyone succeeds with otherwise equivalent arguments?

If capability SIDs are not appropriate for this use, what is the supported Windows design for a restricted, non-AppContainer worker that must retain WRITE_RESTRICTED behavior, read protected inputs and write only to narrowly authorized scratch directories? In particular, what SID-selection and DACL strategy would Microsoft recommend?

I understand that successful token creation alone does not demonstrate effective filesystem isolation. Any proposed alternative would still require explicit access-control and negative-write testing.

I'm looking for clarification of the supported API contract before changing the token or ACL design.

Thank you.

CreateRestrictedToken returns ERROR_INVALID_PARAMETER (87) with a derived capability SID, but succeeds with Everyone SID

Category: Windows development → Windows API - Win32

I'm investigating a Windows access-token compatibility issue involving CreateRestrictedToken and capability SIDs generated through DeriveCapabilitySidsFromName.

My goal is to create a restricted, non-AppContainer worker token that can read protected inputs but write only to explicitly authorized scratch locations.

Environment

Windows build: 10.0.26300, x64

Caller: Medium-integrity, non-elevated process

Source token: Primary token with Limited elevation type

TokenHasRestrictions: 1

TokenRestrictedSids: Empty

TokenIsAppContainer: 0

Source-token access rights: 0x008B

The token appears consistent with UAC filtering, but I have not established that its filtering history causes the failure.

API invocation

Both calls used the same original token handle and the same CreateRestrictedToken argument pattern:

Flags: 0x9 (DISABLE_MAX_PRIVILEGE | WRITE_RESTRICTED)

DisableSidCount: 0

SidsToDisable: NULL

DeletePrivilegeCount: 0

PrivilegesToDelete: NULL

RestrictedSidCount: 1

SidsToRestrict: Pointer to one SID_AND_ATTRIBUTES

SID_AND_ATTRIBUTES.Attributes: 0

NewTokenHandle: Valid output-handle pointer

The experiment used the same process, original token, native function declaration, argument counts, flags and error-capture method.

Call A — Derived capability SID

The restricting SID came from the CapabilitySids output of DeriveCapabilitySidsFromName, using an application-specific capability name.

SID namespace: S-1-15-3-1024-...

SID length: 48 bytes

IsValidSid: Passed

String conversion and round-trip validation: Passed

SID absent from the source token's ordinary TokenGroups

CreateRestrictedToken: FALSE

Immediate GetLastError: 87 (ERROR_INVALID_PARAMETER)

Output token handle: NULL

Call B — Everyone SID

The restricting SID was S-1-1-0.

SID length: 12 bytes

IsValidSid: Passed

String conversion and round-trip validation: Passed

Everyone present and enabled in the source token's ordinary groups

CreateRestrictedToken: TRUE

Valid output token handle returned and closed successfully

Call A preceded Call B. I have not performed a reverse-order experiment or inspected the successful output token's restricting-SID list.

Questions

Are AppAuthority capability SIDs returned in CapabilitySids supported as restricting SIDs in CreateRestrictedToken for ordinary non-AppContainer primary tokens? Are there documented restrictions involving SID namespace, group membership or token-filtering state?

When TokenHasRestrictions is true but TokenRestrictedSids is empty, how should the documented restricted-SID intersection behavior be interpreted? Does the source token's UAC-filtered state affect which restricting SIDs can be accepted?

Is there a documented reason why a structurally valid derived capability SID could produce ERROR_INVALID_PARAMETER while Everyone succeeds with otherwise equivalent arguments?

If capability SIDs are not appropriate for this use, what is the supported Windows design for a restricted, non-AppContainer worker that must retain WRITE_RESTRICTED behavior, read protected inputs and write only to narrowly authorized scratch directories? In particular, what SID-selection and DACL strategy would Microsoft recommend?

I understand that successful token creation alone does not demonstrate effective filesystem isolation. Any proposed alternative would still require explicit access-control and negative-write testing.

I'm looking for clarification of the supported API contract before changing the token or ACL design.

Thank you.

Windows development | Windows API - Win32
0 comments No comments

1 answer

Sort by: Most helpful
  1. AI answer

    2026-10-17T01:03:32.3533333+00:00

    The public Win32 contract does not establish that an AppAuthority capability SID is invalid as a restricting SID, but it also does not document capability SIDs as supported inputs or explain ERROR_INVALID_PARAMETER for this combination; therefore, do not treat the observed error as proof of a membership, UAC-filtering, or namespace rule.

    What the documented contract establishes

    CreateRestrictedToken documents these requirements for SidsToRestrict:

    • It is an array of valid SID_AND_ATTRIBUTES structures.
    • Attributes must be zero.
    • Restricting SIDs are enabled automatically for access checks.
    • It does not state that a restricting SID must already appear in TokenGroups.
    • It does not state any permitted or prohibited SID authorities.
    • The documented explicit ERROR_INVALID_PARAMETER case concerns passing too many privileges in PrivilegesToDelete, which does not apply to your call because DISABLE_MAX_PRIVILEGE causes that list to be ignored anyway.^1^

    Consequently, Everyone being present in TokenGroups while the capability SID is absent does not establish the reason for the different outcomes. The documented “ignore SIDs not in the existing token” rule applies to SidsToDisable, not SidsToRestrict.^1^

    DeriveCapabilitySidsFromName returns two distinct arrays:

    • NT Authority capability group SIDs
    • AppAuthority capability SIDs

    The documentation describes their construction and ownership requirements, but not their use with CreateRestrictedToken.^2^ Capability SIDs use the S-1-15-3 prefix and are intended to identify capabilities granting components or applications access to associated resources.^3^

    There is therefore no documented basis to conclude either:

    1. all structurally valid capability SIDs must be accepted by CreateRestrictedToken, or
    2. capability SIDs are categorically prohibited as restricting SIDs.

    The exact ERROR_INVALID_PARAMETER result on build 26300 is outside the published contract shown here.

    TokenHasRestrictions versus a restricting-SID list

    TokenHasRestrictions != 0 means that the token has ever been filtered. It does not mean that TokenRestrictedSids currently contains an entry.^4^

    The narrower test is IsTokenRestricted:

    • Nonzero only when the token contains a list of restricting SIDs.
    • Zero when it has no restricting SIDs, even if it was created by CreateRestrictedToken and was filtered in other ways.^5^

    Accordingly, an empty TokenRestrictedSids result is not evidence of an existing empty set participating in intersection. The documented intersection rule applies if the existing token is a restricted token, meaning it already has a restricting-SID list.^1^ Nothing in the documented contract says that UAC filtering history changes which SID authorities SidsToRestrict accepts.

    Supported non-AppContainer design

    For a WRITE_RESTRICTED worker, use an ordinary SID whose acceptance you have validated with CreateRestrictedToken, rather than relying on capability-SID behavior that is not documented for this API.

    1. Select a dedicated trustee SID for the worker restriction. Avoid Everyone for isolation: because Everyone represents all users, it does not identify the scratch boundary narrowly.^6^
    2. Pass that SID as the sole SidsToRestrict entry with Attributes = 0, together with DISABLE_MAX_PRIVILEGE | WRITE_RESTRICTED.
    3. On each scratch directory, configure the DACL so that:
      • the worker’s normal enabled identity receives the required write rights; and
      • the restricting SID independently receives those same write rights.
    4. Do not grant the restricting SID write access on protected inputs or other destinations.
    5. Query the returned token with TokenRestrictedSids and verify that the intended SID is actually present.
    6. Run positive tests against scratch paths and negative create, overwrite, rename, delete, and directory-creation tests elsewhere.

    This follows the restricted-token access model: Windows performs one check using the token’s enabled SIDs and another using its restricting SIDs, and grants access only when both checks permit it.^7^ With WRITE_RESTRICTED, the restricting SIDs are considered only while evaluating write access.^1^

    Do not infer success from token creation alone. For the successful Everyone experiment, inspect TokenRestrictedSids; it determines whether the resulting token actually contains the requested restricting SID. Also run the worker on a desktop other than the default desktop, as Microsoft warns that restricted applications on the default desktop can attack unrestricted applications through window messaging.^7^

    For a definitive support statement about the build-26300 rejection of S-1-15-3-1024-…, a minimal native reproducer should be submitted through Microsoft support. Include the exact derived SID, both arrays returned by DeriveCapabilitySidsFromName, source-token information, and the successful control call. The public API documentation does not specify that implementation boundary.


    References

    1. CreateRestrictedToken function (securitybaseapi.h)
    2. DeriveCapabilitySidsFromName function (securitybaseapi.h)
    3. Some SIDs don't resolve into friendly names
    4. TOKEN_INFORMATION_CLASS enumeration (winnt.h)
    5. IsTokenRestricted function (securitybaseapi.h)
    6. Security identifiers
    7. Restricted Tokens
    AI-generated content may be incorrect. Read our transparency notes for more information.

    Was this answer helpful?

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.