Fix panic on malformed ACE binary by validating unmarshal lengths (#30)#68
Fix panic on malformed ACE binary by validating unmarshal lengths (#30)#68p0dalirius wants to merge 2 commits into
Conversation
AccessControlMask.Unmarshal, SecurityIdentifierAuthority.Unmarshal and AccessControlObjectTypeFlags.Unmarshal indexed their input slice without a length check. A truncated ACE binary (e.g. ACE header with Size=4 and no body, SID with SubAuthorityCount=0 and missing authority bytes, or object ACE with missing flag field) would reach one of these leaf unmarshals with a slice shorter than required and panic with 'slice bounds out of range', crashing the process instead of returning a parse error to the caller. Validate len(input) up front in each of the three leaf unmarshals and return a descriptive parse error. Callers (AccessControlEntry.Unmarshal, SID.Unmarshal, AccessControlObjectType.Unmarshal) already propagate errors, so the panic-free behaviour reaches every entry point including NtSecurityDescriptor.Unmarshal.
Round-two audit of every exported Unmarshal: a harness that called each parser directly with 0..40 byte inputs surfaced four more leaves that indexed their input with no length check and panicked on empty or too-short slices: - ace/aceflags.AccessControlEntryFlag.Unmarshal (marshalledData[0]) - ace/acetype.AccessControlEntryType.Unmarshal (marshalledData[0]) - acl/revision.AccessControlListRevision.Unmarshal (marshalledData[0]) - securitydescriptor/control.NtSecurityDescriptorControl.Unmarshal (binary.LittleEndian.Uint16 needs >= 2 bytes) These were unreachable from the normal NtSecurityDescriptor.Unmarshal parse chain because every intermediate caller pre-slices to the exact required length, but external callers invoking the leaves directly would still crash the process. Add the missing length guards plus regression tests for each, and add go-fuzz tests for NtSecurityDescriptor.Unmarshal and FromSDDLString. Running each fuzz target for 15 s produces >2M and >1M iterations respectively with zero panics.
|
Expanded the scope of this PR after a second audit pass: ran a harness that calls every exported
External callers invoking these directly could still crash the process, so I added length guards and regression tests for each. Also added Net result: every path through the parsing functions now ends in a handled error — no reachable panic path remains on the Commit: 0bae9e8. |
Linked Issue
Closes #30
Root Cause
Three leaf
Unmarshalfunctions read their input via slice indexing without first validatinglen:ace/mask/AccessControlMask.go::Unmarshalat L33 readsmarshalledData[:4].sid/authority/SecurityIdentifierAuthority.go::Unmarshalat L67-L69 readsmarshalledData[0:2],[2:4],[4:6].object/flags/AccessControlObjectTypeFlags.go::Unmarshalat L27 readsrawBytes[0:4].When a truncated ACE binary travels down the parse chain — for example an
ACCESS_ALLOWEDACE whoseHeader.Sizeis exactly 4 (header only, no mask or SID), or anACCESS_ALLOWED_OBJECTACE with missing flag bytes — one of these leaves is eventually called with a slice shorter than the required field, causingruntime error: slice bounds out of rangeand crashing the process. This matches the stack signature in the report (ace/AccessControlEntry.go→DiscretionaryAccessControlList.Unmarshal→NtSecurityDescriptor.Unmarshal).Fix Description
Validate
len(input)at the top of each of the three leaf unmarshals and return a descriptive parse error on short input. No change to the successful-path behaviour or to marshalled output. The existing callers (AccessControlEntry.Unmarshal,SID.Unmarshal,AccessControlObjectType.Unmarshal) already propagate errors upward, so the improvement reachesNtSecurityDescriptor.Unmarshal.How Verified
recover(). Five distinct panic signatures confirmed (mask with 0/3 bytes; authority with 0/4 bytes; full ACE withSize=4+ ACCESS_ALLOWED).no panicfor every case and a non-nil error is returned instead.go test ./...green.Test Coverage
Added:
ace/AccessControlEntry_test.go::TestAccessControlEntry_Unmarshal_MalformedNoPanic— nine truncated-ACE shapes; each assertsrecover()sees no panic and a non-nil error is returned.ace/mask/AccessControlMask_test.go::TestAccessControlMask_Unmarshal_TruncatedReturnsError— 0–3 byte inputs.sid/authority/SecurityIdentifierAuthority_test.go::Test_SecurityIdentifierAuthority_Unmarshal_TruncatedReturnsError— 0–5 byte inputs.object/flags/AccessControlObjectTypeFlags_test.go::TestAccessControlObjectTypeFlags_Unmarshal_TruncatedReturnsError— 0–3 byte inputs.Scope of Change
ace/mask/AccessControlMask.go,sid/authority/SecurityIdentifierAuthority.go,object/flags/AccessControlObjectTypeFlags.go, plus the four test files.Risk and Rollout
Narrow. The three added checks are the first thing each function does and exit early on malformed input. No successful parse is affected. Safe to merge without staged rollout.
Notes
This PR addresses the panic reported in #30. A broader audit turned up no other reachable panic on the
NtSecurityDescriptor.Unmarshalpath — the SID sub-authority loop, the DACL/SACL entry loop, the GUID read, and the ACE header already bounds-check their inputs, and every other caller either validates upfront or exits via an error from the three fixed leaves.