11package sddl
22
33import (
4+ "fmt"
45 "strings"
56)
67
78// CutSDDL parses an SDDL string into its component parts.
89//
10+ // The scan is parenthesis-aware: the O:, G:, D:, and S: component markers are
11+ // only recognised at the top level (parenthesis depth 0), so a ':' or a marker
12+ // letter appearing inside an ACE body (for example in a conditional or
13+ // resource-attribute ACE) does not split the string. Malformed input — leading
14+ // characters before the first marker, or unbalanced parentheses — is reported
15+ // as an error instead of being silently discarded.
16+ //
917// Parameters:
1018// - sddlString (string): The SDDL string to parse.
1119//
1220// Returns:
13- // - (string, string, []string, []string): The owner SID, group SID, DACL ACEs, and SACL ACEs.
14- func CutSDDL (sddlString string ) (string , string , []string , []string ) {
21+ // - (string, string, []string, []string, error): The owner SID, group SID,
22+ // DACL ACEs, SACL ACEs, and an error if the string is malformed.
23+ func CutSDDL (sddlString string ) (string , string , []string , []string , error ) {
1524 sddlString = strings .TrimSpace (sddlString )
1625
1726 if len (sddlString ) == 0 {
18- return "" , "" , nil , nil
27+ return "" , "" , nil , nil , nil
1928 }
2029
21- // Match components starting with O:, G:, D:, or S: using regex
30+ // Match components starting with O:, G:, D:, or S:.
2231 components := map [string ]string {
2332 "O:" : "" ,
2433 "G:" : "" ,
@@ -27,41 +36,79 @@ func CutSDDL(sddlString string) (string, string, []string, []string) {
2736 }
2837
2938 currentComponent := ""
39+ depth := 0
3040 k := 0
3141 for k < len (sddlString ) {
32- upperChar := strings .ToUpper (string (sddlString [k ]))
33- if k + 1 < len (sddlString ) && (upperChar == "O" || upperChar == "G" || upperChar == "D" || upperChar == "S" ) && sddlString [k + 1 ] == ':' {
34- // Normalise the key to uppercase so lowercase markers (o:, g:, d:, s:)
35- // land in the same bucket as their uppercase counterparts; SDDL is
36- // case-insensitive at the component-marker level.
37- currentComponent = upperChar + ":"
38- k += 2
39- continue
42+ c := sddlString [k ]
43+
44+ // A component marker can only start at the top level. Recognising markers
45+ // at depth > 0 would let an ACE body containing "D:" (etc.) be mistaken
46+ // for a new component.
47+ if depth == 0 && k + 1 < len (sddlString ) && sddlString [k + 1 ] == ':' {
48+ upperChar := strings .ToUpper (string (c ))
49+ if upperChar == "O" || upperChar == "G" || upperChar == "D" || upperChar == "S" {
50+ // Normalise the key to uppercase so lowercase markers (o:, g:, d:,
51+ // s:) land in the same bucket as their uppercase counterparts; SDDL
52+ // is case-insensitive at the component-marker level.
53+ currentComponent = upperChar + ":"
54+ k += 2
55+ continue
56+ }
57+ }
58+
59+ switch c {
60+ case '(' :
61+ depth ++
62+ case ')' :
63+ depth --
64+ if depth < 0 {
65+ return "" , "" , nil , nil , fmt .Errorf ("malformed SDDL: unbalanced ')' at position %d" , k )
66+ }
4067 }
41- if currentComponent != "" {
42- components [currentComponent ] += string (sddlString [k ])
68+
69+ if currentComponent == "" {
70+ // Any character before the first component marker is invalid; the
71+ // string has already been trimmed, so this is genuine garbage.
72+ return "" , "" , nil , nil , fmt .Errorf ("malformed SDDL: unexpected character %q at position %d before any component marker" , c , k )
4373 }
74+ components [currentComponent ] += string (c )
4475 k ++
4576 }
4677
47- daclAces := CutAces (components ["D:" ])
48- saclAces := CutAces (components ["S:" ])
78+ if depth != 0 {
79+ return "" , "" , nil , nil , fmt .Errorf ("malformed SDDL: unbalanced '(' (missing %d closing parenthesis)" , depth )
80+ }
81+
82+ daclAces , err := CutAces (components ["D:" ])
83+ if err != nil {
84+ return "" , "" , nil , nil , fmt .Errorf ("invalid DACL: %w" , err )
85+ }
86+ saclAces , err := CutAces (components ["S:" ])
87+ if err != nil {
88+ return "" , "" , nil , nil , fmt .Errorf ("invalid SACL: %w" , err )
89+ }
4990
50- return components ["O:" ], components ["G:" ], daclAces , saclAces
91+ return components ["O:" ], components ["G:" ], daclAces , saclAces , nil
5192}
5293
5394// CutAces extracts individual ACE strings from a DACL/SACL component.
5495// Handles the format: flags(ace1)(ace2)...(aceN)
55- func CutAces (aclStr string ) []string {
96+ //
97+ // Nested parentheses inside an ACE (for example in a conditional expression)
98+ // are preserved: only top-level parentheses delimit ACEs. Unbalanced
99+ // parentheses and stray characters between or after ACEs are reported as an
100+ // error rather than silently dropping or truncating ACEs.
101+ func CutAces (aclStr string ) ([]string , error ) {
56102 var aces []string
57103
58- // Find first ( to separate flags
104+ // Find first ( to separate flags. No '(' means the component has no ACEs
105+ // (e.g. an empty DACL with only flags), which is valid.
59106 start := strings .Index (aclStr , "(" )
60107 if start == - 1 {
61- return aces
108+ return aces , nil
62109 }
63110
64- // Extract ACEs between parentheses
111+ // Extract ACEs between top-level parentheses.
65112 depth := 0
66113 aceStart := start
67114 for i := start ; i < len (aclStr ); i ++ {
@@ -73,11 +120,22 @@ func CutAces(aclStr string) []string {
73120 depth ++
74121 case ')' :
75122 depth --
123+ if depth < 0 {
124+ return nil , fmt .Errorf ("unbalanced ')' at position %d" , i )
125+ }
76126 if depth == 0 {
77127 aces = append (aces , aclStr [aceStart :i ])
78128 }
129+ default :
130+ if depth == 0 {
131+ return nil , fmt .Errorf ("unexpected character %q at position %d (outside any ACE)" , aclStr [i ], i )
132+ }
79133 }
80134 }
81135
82- return aces
136+ if depth != 0 {
137+ return nil , fmt .Errorf ("unbalanced '(' (missing %d closing parenthesis)" , depth )
138+ }
139+
140+ return aces , nil
83141}
0 commit comments