437 lines
15 KiB
TypeScript
437 lines
15 KiB
TypeScript
/**
|
|
* Restricted-token failure-path tests with stub binding tables (the
|
|
* failure-paths.spec.ts pattern): every checked Win32 call in the token
|
|
* pipeline — open, logon-SID scan, well-known SID creation, default-DACL
|
|
* merge, restricted-token creation — has a failing counterpart, and each
|
|
* failure closes or frees what it created before throwing. Pure stubs — no
|
|
* real Win32 calls, so these run on every platform; the real-FFI round-trip
|
|
* lives in acl.spec.ts (win32 only).
|
|
*/
|
|
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import koffi from 'koffi'
|
|
|
|
import { allocBytes, isNullPtr } from '../src/ffi.ts'
|
|
import type { NativePtr, Win32Bindings } from '../src/ffi.ts'
|
|
import { Win32Error } from '../src/errors.ts'
|
|
import {
|
|
createRestrictedToken, findLogonSid, makeWellKnownSid, openCurrentProcessToken, setTokenDefaultDaclGrant,
|
|
} from '../src/token.ts'
|
|
import * as abi from '../src/win32-abi.ts'
|
|
|
|
const PVOID = koffi.pointer('void')
|
|
|
|
describe('openCurrentProcessToken failure paths', () => {
|
|
it('reports when OpenProcess yields no handle', () => {
|
|
const api = {
|
|
openProcess: vi.fn(() => 0n),
|
|
getLastError: vi.fn(() => 5),
|
|
formatMessageW: vi.fn(() => 0),
|
|
} as unknown as Win32Bindings
|
|
let caught: unknown
|
|
try {
|
|
openCurrentProcessToken(api)
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('OpenProcess')
|
|
})
|
|
|
|
it('closes the process handle and reports when OpenProcessToken fails', () => {
|
|
const closeHandle = vi.fn(() => 1)
|
|
const api = {
|
|
openProcess: vi.fn(() => 7n),
|
|
openProcessToken: vi.fn(() => 0),
|
|
closeHandle,
|
|
getLastError: vi.fn(() => 5),
|
|
formatMessageW: vi.fn(() => 0),
|
|
} as unknown as Win32Bindings
|
|
let caught: unknown
|
|
try {
|
|
openCurrentProcessToken(api)
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('OpenProcessToken')
|
|
expect(closeHandle).toHaveBeenCalledWith(7n)
|
|
})
|
|
|
|
it('reports a failed CloseHandle of the process handle', () => {
|
|
const api = {
|
|
openProcess: vi.fn(() => 7n),
|
|
openProcessToken: vi.fn((_process: unknown, _access: unknown, slot: NativePtr) => {
|
|
koffi.encode(slot, PVOID, 9n)
|
|
return 1
|
|
}),
|
|
closeHandle: vi.fn(() => 0),
|
|
getLastError: vi.fn(() => 5),
|
|
formatMessageW: vi.fn(() => 0),
|
|
} as unknown as Win32Bindings
|
|
let caught: unknown
|
|
try {
|
|
openCurrentProcessToken(api)
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('CloseHandle')
|
|
})
|
|
|
|
it('rejects a NULL token handle after a successful OpenProcessToken', () => {
|
|
const api = {
|
|
openProcess: vi.fn(() => 7n),
|
|
openProcessToken: vi.fn(() => 1), // succeeds without writing the out slot
|
|
closeHandle: vi.fn(() => 1),
|
|
getLastError: vi.fn(() => 5),
|
|
formatMessageW: vi.fn(() => 0),
|
|
} as unknown as Win32Bindings
|
|
let caught: unknown
|
|
try {
|
|
openCurrentProcessToken(api)
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('OpenProcessToken')
|
|
})
|
|
})
|
|
|
|
/**
|
|
* The stub the logon-SID scan needs: the size probe writes `needed`, the
|
|
* second call fills a TOKEN_GROUPS buffer (GroupCount@0, SID pointer@8,
|
|
* attributes@16) with the state's one group. The CopySid mock comes back
|
|
* beside the table for the one test that asserts on its arguments.
|
|
*/
|
|
function logonApi(state: {
|
|
needed: number
|
|
groupCount: number
|
|
sidPtr: bigint
|
|
logon: boolean
|
|
secondOk?: boolean
|
|
sidLength?: number
|
|
copyOk?: boolean
|
|
}): { api: Win32Bindings; copySid: ReturnType<typeof vi.fn> } {
|
|
const copySid = vi.fn(() => (state.copyOk === false ? 0 : 1))
|
|
const api = {
|
|
getTokenInformation: vi.fn((_token: unknown, cls: number, info: Buffer | null, _length: number, needed: NativePtr) => {
|
|
if (cls !== abi.TokenGroups) throw new Error(`unexpected token information class ${cls}`)
|
|
if (info === null) {
|
|
koffi.encode(needed, 'uint32', state.needed)
|
|
return 0 // the size probe is expected to "fail"
|
|
}
|
|
if (state.secondOk === false) return 0
|
|
info.writeUInt32LE(state.groupCount, 0)
|
|
if (state.groupCount > 0) {
|
|
info.writeBigUInt64LE(state.sidPtr, abi.TOKEN_GROUPS_OFFSET)
|
|
info.writeUInt32LE(state.logon ? abi.SE_GROUP_LOGON_ID : 0, abi.TOKEN_GROUPS_OFFSET + 8)
|
|
}
|
|
return 1
|
|
}),
|
|
getLengthSid: vi.fn(() => state.sidLength ?? 12),
|
|
copySid,
|
|
getLastError: vi.fn(() => 5),
|
|
formatMessageW: vi.fn(() => 0),
|
|
} as unknown as Win32Bindings
|
|
return { api, copySid }
|
|
}
|
|
|
|
describe('findLogonSid failure paths', () => {
|
|
const token = 9n as NativePtr
|
|
|
|
it('reports a size probe that wrote nothing', () => {
|
|
const { api } = logonApi({ needed: 0, groupCount: 0, sidPtr: 0n, logon: false })
|
|
let caught: unknown
|
|
try {
|
|
findLogonSid(api, token)
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('GetTokenInformation')
|
|
})
|
|
|
|
it('rejects an implausibly small TokenGroups size', () => {
|
|
const { api } = logonApi({ needed: 4, groupCount: 0, sidPtr: 0n, logon: false })
|
|
let caught: unknown
|
|
try {
|
|
findLogonSid(api, token)
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('GetTokenInformation')
|
|
})
|
|
|
|
it('reports a failed TokenGroups read', () => {
|
|
const { api } = logonApi({ needed: 24, groupCount: 1, sidPtr: 77n, logon: true, secondOk: false })
|
|
let caught: unknown
|
|
try {
|
|
findLogonSid(api, token)
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('GetTokenInformation')
|
|
})
|
|
|
|
it('skips a NULL group SID pointer and throws when no logon SID remains', () => {
|
|
const { api } = logonApi({ needed: 24, groupCount: 1, sidPtr: 0n, logon: true })
|
|
expect(() => findLogonSid(api, token)).toThrow(/no logon SID found/u)
|
|
})
|
|
|
|
it('skips a non-logon group and throws when no logon SID remains', () => {
|
|
const { api } = logonApi({ needed: 24, groupCount: 1, sidPtr: 77n, logon: false })
|
|
expect(() => findLogonSid(api, token)).toThrow(/no logon SID found/u)
|
|
})
|
|
|
|
it('reports a zero logon-SID length', () => {
|
|
const { api } = logonApi({ needed: 24, groupCount: 1, sidPtr: 77n, logon: true, sidLength: 0 })
|
|
let caught: unknown
|
|
try {
|
|
findLogonSid(api, token)
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('GetLengthSid')
|
|
})
|
|
|
|
it('reports a failed CopySid of the logon SID', () => {
|
|
const { api } = logonApi({ needed: 24, groupCount: 1, sidPtr: 77n, logon: true, copyOk: false })
|
|
let caught: unknown
|
|
try {
|
|
findLogonSid(api, token)
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('CopySid')
|
|
})
|
|
|
|
it('copies the logon SID and returns the new allocation', () => {
|
|
const { api, copySid } = logonApi({ needed: 24, groupCount: 1, sidPtr: 77n, logon: true })
|
|
const copy = findLogonSid(api, token)
|
|
expect(isNullPtr(copy)).toBe(false)
|
|
expect(copySid).toHaveBeenCalledWith(12, copy, 77n)
|
|
})
|
|
})
|
|
|
|
describe('makeWellKnownSid failure paths', () => {
|
|
it('reports when CreateWellKnownSid fails', () => {
|
|
const api = {
|
|
createWellKnownSid: vi.fn(() => 0),
|
|
getLastError: vi.fn(() => 5),
|
|
formatMessageW: vi.fn(() => 0),
|
|
} as unknown as Win32Bindings
|
|
let caught: unknown
|
|
try {
|
|
makeWellKnownSid(api, abi.WinWorldSid)
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('CreateWellKnownSid')
|
|
})
|
|
|
|
it('reports when the created well-known SID is invalid', () => {
|
|
const api = {
|
|
createWellKnownSid: vi.fn(() => 1),
|
|
isValidSid: vi.fn(() => 0),
|
|
getLastError: vi.fn(() => 5),
|
|
formatMessageW: vi.fn(() => 0),
|
|
} as unknown as Win32Bindings
|
|
let caught: unknown
|
|
try {
|
|
makeWellKnownSid(api, abi.WinWorldSid)
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('IsValidSid')
|
|
})
|
|
})
|
|
|
|
/**
|
|
* The stub the default-DACL merge needs: the size probe writes `needed`, the
|
|
* second call fills the DACL pointer slot, and the merge/apply calls follow
|
|
* the state's results.
|
|
*/
|
|
function daclApi(state: {
|
|
needed: number
|
|
currentDacl: bigint
|
|
secondOk?: boolean
|
|
mergeResult?: number
|
|
newDacl: bigint
|
|
setTokenInfo?: number
|
|
}): Win32Bindings {
|
|
const api = {
|
|
getTokenInformation: vi.fn((_token: unknown, cls: number, info: Buffer | null, _length: number, needed: NativePtr) => {
|
|
if (cls !== abi.TokenDefaultDacl) throw new Error(`unexpected token information class ${cls}`)
|
|
if (info === null) {
|
|
koffi.encode(needed, 'uint32', state.needed)
|
|
return 0 // the size probe is expected to "fail"
|
|
}
|
|
if (state.secondOk === false) return 0
|
|
info.writeBigUInt64LE(state.currentDacl, 0)
|
|
return 1
|
|
}),
|
|
setEntriesInAclW: vi.fn((_count: unknown, _entries: unknown, _old: unknown, newAcl: NativePtr) => {
|
|
if (state.mergeResult !== undefined && state.mergeResult !== 0) return state.mergeResult
|
|
koffi.encode(newAcl, PVOID, state.newDacl)
|
|
return 0
|
|
}),
|
|
setTokenInformation: vi.fn(() => state.setTokenInfo ?? 1),
|
|
localFree: vi.fn(() => 0n),
|
|
getLastError: vi.fn(() => 5),
|
|
formatMessageW: vi.fn(() => 0),
|
|
} as unknown as Win32Bindings
|
|
return api
|
|
}
|
|
|
|
describe('setTokenDefaultDaclGrant failure paths', () => {
|
|
const token = 9n as NativePtr
|
|
const sid = 77n as NativePtr
|
|
|
|
it('reports a size probe that wrote nothing', () => {
|
|
const api = daclApi({ needed: 0, currentDacl: 0n, newDacl: 0n })
|
|
let caught: unknown
|
|
try {
|
|
setTokenDefaultDaclGrant(api, token, sid)
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('GetTokenInformation')
|
|
})
|
|
|
|
it('reports a failed default-DACL read', () => {
|
|
const api = daclApi({ needed: 8, currentDacl: 88n, secondOk: false, newDacl: 0n })
|
|
let caught: unknown
|
|
try {
|
|
setTokenDefaultDaclGrant(api, token, sid)
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('GetTokenInformation')
|
|
})
|
|
|
|
it('rejects a token that carries no default DACL', () => {
|
|
const api = daclApi({ needed: 8, currentDacl: 0n, newDacl: 0n })
|
|
expect(() => { setTokenDefaultDaclGrant(api, token, sid) }).toThrow(/no default DACL/u)
|
|
})
|
|
|
|
it('reports a failed SetEntriesInAclW merge', () => {
|
|
const api = daclApi({ needed: 8, currentDacl: 88n, mergeResult: 5, newDacl: 0n })
|
|
let caught: unknown
|
|
try {
|
|
setTokenDefaultDaclGrant(api, token, sid)
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('SetEntriesInAclW')
|
|
})
|
|
|
|
it('rejects a NULL merged default DACL', () => {
|
|
const api = daclApi({ needed: 8, currentDacl: 88n, newDacl: 0n })
|
|
let caught: unknown
|
|
try {
|
|
setTokenDefaultDaclGrant(api, token, sid)
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('SetEntriesInAclW')
|
|
})
|
|
|
|
it('frees the merged DACL and reports when SetTokenInformation fails', () => {
|
|
const localFree = vi.fn(() => 0n)
|
|
const api = daclApi({ needed: 8, currentDacl: 88n, newDacl: 99n, setTokenInfo: 0 })
|
|
;(api.localFree as unknown as ReturnType<typeof vi.fn>).mockImplementation(localFree)
|
|
let caught: unknown
|
|
try {
|
|
setTokenDefaultDaclGrant(api, token, sid)
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('SetTokenInformation')
|
|
expect(localFree).toHaveBeenCalledWith(99n)
|
|
})
|
|
|
|
it('frees the merged DACL after a successful apply', () => {
|
|
const localFree = vi.fn(() => 0n)
|
|
const api = daclApi({ needed: 8, currentDacl: 88n, newDacl: 99n })
|
|
;(api.localFree as unknown as ReturnType<typeof vi.fn>).mockImplementation(localFree)
|
|
setTokenDefaultDaclGrant(api, token, sid)
|
|
expect(localFree).toHaveBeenCalledWith(99n)
|
|
})
|
|
})
|
|
|
|
describe('createRestrictedToken failure paths', () => {
|
|
it('builds the read-only restricting list without a write SID', () => {
|
|
const create = vi.fn((
|
|
_existing: unknown, _flags: unknown, _dc: unknown, _ds: unknown, _pc: unknown, _pd: unknown,
|
|
count: number, _sids: unknown, slot: NativePtr,
|
|
) => {
|
|
koffi.encode(slot, PVOID, 9n)
|
|
expect(count).toBe(2)
|
|
return 1
|
|
})
|
|
const api = { createRestrictedToken: create } as unknown as Win32Bindings
|
|
const logon = allocBytes(12)
|
|
expect(createRestrictedToken(api, 1n as NativePtr, logon, [], { world: 2n as NativePtr }, 'read-only')).toBe(9n)
|
|
})
|
|
|
|
it('builds the workspace-write restricting list with the write SID', () => {
|
|
const create = vi.fn((
|
|
_existing: unknown, _flags: unknown, _dc: unknown, _ds: unknown, _pc: unknown, _pd: unknown,
|
|
count: number, _sids: unknown, slot: NativePtr,
|
|
) => {
|
|
koffi.encode(slot, PVOID, 9n)
|
|
expect(count).toBe(3)
|
|
return 1
|
|
})
|
|
const api = { createRestrictedToken: create } as unknown as Win32Bindings
|
|
const logon = allocBytes(12)
|
|
expect(createRestrictedToken(api, 1n as NativePtr, logon, [3n as NativePtr], { world: 2n as NativePtr }, 'workspace-write')).toBe(9n)
|
|
})
|
|
|
|
it('reports when CreateRestrictedToken fails', () => {
|
|
const api = {
|
|
createRestrictedToken: vi.fn(() => 0),
|
|
getLastError: vi.fn(() => 5),
|
|
formatMessageW: vi.fn(() => 0),
|
|
} as unknown as Win32Bindings
|
|
const logon = allocBytes(12)
|
|
let caught: unknown
|
|
try {
|
|
createRestrictedToken(api, 1n as NativePtr, logon, [], { world: 2n as NativePtr }, 'read-only')
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('CreateRestrictedToken')
|
|
})
|
|
|
|
it('rejects a NULL token handle after a successful CreateRestrictedToken', () => {
|
|
const api = {
|
|
createRestrictedToken: vi.fn(() => 1), // succeeds without writing the out slot
|
|
getLastError: vi.fn(() => 5),
|
|
formatMessageW: vi.fn(() => 0),
|
|
} as unknown as Win32Bindings
|
|
const logon = allocBytes(12)
|
|
let caught: unknown
|
|
try {
|
|
createRestrictedToken(api, 1n as NativePtr, logon, [], { world: 2n as NativePtr }, 'read-only')
|
|
} catch (error) {
|
|
caught = error
|
|
}
|
|
expect(caught).toBeInstanceOf(Win32Error)
|
|
expect((caught as Win32Error).api).toBe('CreateRestrictedToken')
|
|
})
|
|
})
|