Skip to content

Commit b3dfcb5

Browse files
cpuschmagustavoluvizotto
authored andcommitted
feat: Refactor ParseDN function to fix resource usage and invalid parsings (fixes go-ldap#487) (go-ldap#497)
1 parent 64975db commit b3dfcb5

File tree

6 files changed

+356
-214
lines changed

6 files changed

+356
-214
lines changed

dn.go

+166-94
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@ package ldap
22

33
import (
44
"bytes"
5-
enchex "encoding/hex"
5+
"encoding/asn1"
6+
"encoding/hex"
67
"errors"
78
"fmt"
89
"sort"
910
"strings"
10-
11-
ber "github.com/go-asn1-ber/asn1-ber"
1211
)
1312

1413
// AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514
@@ -19,8 +18,43 @@ type AttributeTypeAndValue struct {
1918
Value string
2019
}
2120

21+
func (a *AttributeTypeAndValue) setType(str string) error {
22+
result, err := decodeString(str)
23+
if err != nil {
24+
return err
25+
}
26+
a.Type = result
27+
28+
return nil
29+
}
30+
31+
func (a *AttributeTypeAndValue) setValue(s string) error {
32+
// https://www.ietf.org/rfc/rfc4514.html#section-2.4
33+
// If the AttributeType is of the dotted-decimal form, the
34+
// AttributeValue is represented by an number sign ('#' U+0023)
35+
// character followed by the hexadecimal encoding of each of the octets
36+
// of the BER encoding of the X.500 AttributeValue.
37+
if len(s) > 0 && s[0] == '#' {
38+
decodedString, err := decodeEncodedString(s[1:])
39+
if err != nil {
40+
return err
41+
}
42+
43+
a.Value = decodedString
44+
return nil
45+
} else {
46+
decodedString, err := decodeString(s)
47+
if err != nil {
48+
return err
49+
}
50+
51+
a.Value = decodedString
52+
return nil
53+
}
54+
}
55+
2256
// String returns a normalized string representation of this attribute type and
23-
// value pair which is the a lowercased join of the Type and Value with a "=".
57+
// value pair which is the lowercase join of the Type and Value with a "=".
2458
func (a *AttributeTypeAndValue) String() string {
2559
return strings.ToLower(a.Type) + "=" + a.encodeValue()
2660
}
@@ -39,7 +73,7 @@ func (a *AttributeTypeAndValue) encodeValue() string {
3973

4074
escapeHex := func(c byte) {
4175
encodedBuf.WriteByte('\\')
42-
encodedBuf.WriteString(enchex.EncodeToString([]byte{c}))
76+
encodedBuf.WriteString(hex.EncodeToString([]byte{c}))
4377
}
4478

4579
for i := 0; i < len(value); i++ {
@@ -108,114 +142,152 @@ func (d *DN) String() string {
108142
return strings.Join(rdns, ",")
109143
}
110144

145+
// Remove leading and trailing spaces from the attribute type and value
146+
// and unescape any escaped characters in these fields
147+
//
148+
// decodeString is based on https://github.com/inteon/cert-manager/blob/ed280d28cd02b262c5db46054d88e70ab518299c/pkg/util/pki/internal/dn.go#L170
149+
func decodeString(str string) (string, error) {
150+
s := []rune(strings.TrimSpace(str))
151+
// Re-add the trailing space if the last character was an escaped space character
152+
if len(s) > 0 && s[len(s)-1] == '\\' && str[len(str)-2] == ' ' {
153+
s = append(s, ' ')
154+
}
155+
156+
builder := strings.Builder{}
157+
for i := 0; i < len(s); i++ {
158+
char := s[i]
159+
160+
// If the character is not an escape character, just add it to the
161+
// builder and continue
162+
if char != '\\' {
163+
builder.WriteRune(char)
164+
continue
165+
}
166+
167+
// If the escape character is the last character, it's a corrupted
168+
// escaped character
169+
if i+1 >= len(s) {
170+
return "", fmt.Errorf("got corrupted escaped character: '%s'", string(s))
171+
}
172+
173+
// If the escaped character is a special character, just add it to
174+
// the builder and continue
175+
switch s[i+1] {
176+
case ' ', '"', '#', '+', ',', ';', '<', '=', '>', '\\':
177+
builder.WriteRune(s[i+1])
178+
i++
179+
continue
180+
}
181+
182+
// If the escaped character is not a special character, it should
183+
// be a hex-encoded character of the form \XX if it's not at least
184+
// two characters long, it's a corrupted escaped character
185+
if i+2 >= len(s) {
186+
return "", errors.New("failed to decode escaped character: encoding/hex: invalid byte: " + string(s[i+1]))
187+
}
188+
189+
// Get the runes for the two characters after the escape character
190+
// and convert them to a byte slice
191+
xx := []byte(string(s[i+1 : i+3]))
192+
193+
// If the two runes are not hex characters and result in more than
194+
// two bytes when converted to a byte slice, it's a corrupted
195+
// escaped character
196+
if len(xx) != 2 {
197+
return "", errors.New("failed to decode escaped character: invalid byte: " + string(xx))
198+
}
199+
200+
// Decode the hex-encoded character and add it to the builder
201+
dst := []byte{0}
202+
if n, err := hex.Decode(dst, xx); err != nil {
203+
return "", errors.New("failed to decode escaped character: " + err.Error())
204+
} else if n != 1 {
205+
return "", fmt.Errorf("failed to decode escaped character: encoding/hex: expected 1 byte when un-escaping, got %d", n)
206+
}
207+
208+
builder.WriteByte(dst[0])
209+
i += 2
210+
}
211+
212+
return builder.String(), nil
213+
}
214+
215+
func decodeEncodedString(str string) (string, error) {
216+
decoded, err := hex.DecodeString(str)
217+
if err != nil {
218+
return "", fmt.Errorf("failed to decode BER encoding: %s", err)
219+
}
220+
221+
var rawValue asn1.RawValue
222+
result, err := asn1.Unmarshal(decoded, &rawValue)
223+
if err != nil {
224+
return "", fmt.Errorf("failed to unmarshal hex-encoded string: %s", err)
225+
}
226+
if len(result) != 0 {
227+
return "", errors.New("trailing data after unmarshalling hex-encoded string")
228+
}
229+
230+
return string(rawValue.Bytes), nil
231+
}
232+
111233
// ParseDN returns a distinguishedName or an error.
112234
// The function respects https://tools.ietf.org/html/rfc4514
113235
func ParseDN(str string) (*DN, error) {
114-
dn := new(DN)
115-
dn.RDNs = make([]*RelativeDN, 0)
116-
rdn := new(RelativeDN)
117-
rdn.Attributes = make([]*AttributeTypeAndValue, 0)
118-
buffer := bytes.Buffer{}
119-
attribute := new(AttributeTypeAndValue)
120-
escaping := false
121-
122-
unescapedTrailingSpaces := 0
123-
stringFromBuffer := func() string {
124-
s := buffer.String()
125-
s = s[0 : len(s)-unescapedTrailingSpaces]
126-
buffer.Reset()
127-
unescapedTrailingSpaces = 0
128-
return s
236+
var dn = &DN{RDNs: make([]*RelativeDN, 0)}
237+
if str = strings.TrimSpace(str); len(str) == 0 {
238+
return dn, nil
129239
}
130240

241+
var (
242+
rdn = &RelativeDN{}
243+
attr = &AttributeTypeAndValue{}
244+
escaping bool
245+
startPos int
246+
appendAttributesToRDN = func(end bool) {
247+
rdn.Attributes = append(rdn.Attributes, attr)
248+
attr = &AttributeTypeAndValue{}
249+
if end {
250+
dn.RDNs = append(dn.RDNs, rdn)
251+
rdn = &RelativeDN{}
252+
}
253+
}
254+
)
255+
131256
for i := 0; i < len(str); i++ {
132257
char := str[i]
133258
switch {
134259
case escaping:
135-
unescapedTrailingSpaces = 0
136260
escaping = false
137-
switch char {
138-
case ' ', '"', '#', '+', ',', ';', '<', '=', '>', '\\':
139-
buffer.WriteByte(char)
140-
continue
141-
}
142-
// Not a special character, assume hex encoded octet
143-
if len(str) == i+1 {
144-
return nil, errors.New("got corrupted escaped character")
145-
}
146-
147-
dst := []byte{0}
148-
n, err := enchex.Decode([]byte(dst), []byte(str[i:i+2]))
149-
if err != nil {
150-
return nil, fmt.Errorf("failed to decode escaped character: %s", err)
151-
} else if n != 1 {
152-
return nil, fmt.Errorf("expected 1 byte when un-escaping, got %d", n)
153-
}
154-
buffer.WriteByte(dst[0])
155-
i++
156261
case char == '\\':
157-
unescapedTrailingSpaces = 0
158262
escaping = true
159-
case char == '=' && attribute.Type == "":
160-
attribute.Type = stringFromBuffer()
161-
// Special case: If the first character in the value is # the
162-
// following data is BER encoded so we can just fast forward
163-
// and decode.
164-
if len(str) > i+1 && str[i+1] == '#' {
165-
i += 2
166-
index := strings.IndexAny(str[i:], ",+")
167-
var data string
168-
if index > 0 {
169-
data = str[i : i+index]
170-
} else {
171-
data = str[i:]
172-
}
173-
rawBER, err := enchex.DecodeString(data)
174-
if err != nil {
175-
return nil, fmt.Errorf("failed to decode BER encoding: %s", err)
176-
}
177-
packet, err := ber.DecodePacketErr(rawBER)
178-
if err != nil {
179-
return nil, fmt.Errorf("failed to decode BER packet: %s", err)
180-
}
181-
buffer.WriteString(packet.Data.String())
182-
i += len(data) - 1
263+
case char == '=' && len(attr.Type) == 0:
264+
if err := attr.setType(str[startPos:i]); err != nil {
265+
return nil, err
183266
}
267+
startPos = i + 1
184268
case char == ',' || char == '+' || char == ';':
185-
// We're done with this RDN or value, push it
186-
if len(attribute.Type) == 0 {
187-
return nil, errors.New("incomplete type, value pair")
269+
if len(attr.Type) == 0 {
270+
return dn, errors.New("incomplete type, value pair")
188271
}
189-
attribute.Value = stringFromBuffer()
190-
rdn.Attributes = append(rdn.Attributes, attribute)
191-
attribute = new(AttributeTypeAndValue)
192-
if char == ',' || char == ';' {
193-
dn.RDNs = append(dn.RDNs, rdn)
194-
rdn = new(RelativeDN)
195-
rdn.Attributes = make([]*AttributeTypeAndValue, 0)
272+
if err := attr.setValue(str[startPos:i]); err != nil {
273+
return nil, err
196274
}
197-
case char == ' ' && buffer.Len() == 0:
198-
// ignore unescaped leading spaces
199-
continue
200-
default:
201-
if char == ' ' {
202-
// Track unescaped spaces in case they are trailing and we need to remove them
203-
unescapedTrailingSpaces++
204-
} else {
205-
// Reset if we see a non-space char
206-
unescapedTrailingSpaces = 0
207-
}
208-
buffer.WriteByte(char)
275+
276+
startPos = i + 1
277+
last := char == ',' || char == ';'
278+
appendAttributesToRDN(last)
209279
}
210280
}
211-
if buffer.Len() > 0 {
212-
if len(attribute.Type) == 0 {
213-
return nil, errors.New("DN ended with incomplete type, value pair")
214-
}
215-
attribute.Value = stringFromBuffer()
216-
rdn.Attributes = append(rdn.Attributes, attribute)
217-
dn.RDNs = append(dn.RDNs, rdn)
281+
282+
if len(attr.Type) == 0 {
283+
return dn, errors.New("DN ended with incomplete type, value pair")
218284
}
285+
286+
if err := attr.setValue(str[startPos:]); err != nil {
287+
return dn, err
288+
}
289+
appendAttributesToRDN(true)
290+
219291
return dn, nil
220292
}
221293

dn_test.go

+9-9
Original file line numberDiff line numberDiff line change
@@ -82,21 +82,21 @@ func TestSuccessfulDNParsing(t *testing.T) {
8282
for test, answer := range testcases {
8383
dn, err := ParseDN(test)
8484
if err != nil {
85-
t.Errorf(err.Error())
85+
t.Errorf("ParseDN failed for DN test '%s': %s", test, err)
8686
continue
8787
}
8888
if !reflect.DeepEqual(dn, &answer) {
89-
t.Errorf("Parsed DN %s is not equal to the expected structure", test)
89+
t.Errorf("Parsed DN '%s' is not equal to the expected structure", test)
9090
t.Logf("Expected:")
9191
for _, rdn := range answer.RDNs {
92-
for _, attribs := range rdn.Attributes {
93-
t.Logf("#%v\n", attribs)
92+
for _, attribute := range rdn.Attributes {
93+
t.Logf(" #%v\n", attribute)
9494
}
9595
}
9696
t.Logf("Actual:")
9797
for _, rdn := range dn.RDNs {
98-
for _, attribs := range rdn.Attributes {
99-
t.Logf("#%v\n", attribs)
98+
for _, attribute := range rdn.Attributes {
99+
t.Logf(" #%v\n", attribute)
100100
}
101101
}
102102
}
@@ -107,7 +107,7 @@ func TestErrorDNParsing(t *testing.T) {
107107
testcases := map[string]string{
108108
"*": "DN ended with incomplete type, value pair",
109109
"cn=Jim\\0Test": "failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'",
110-
"cn=Jim\\0": "got corrupted escaped character",
110+
"cn=Jim\\0": "failed to decode escaped character: encoding/hex: invalid byte: 0",
111111
"DC=example,=net": "DN ended with incomplete type, value pair",
112112
"1=#0402486": "failed to decode BER encoding: encoding/hex: odd length hex string",
113113
"test,DC=example,DC=com": "incomplete type, value pair",
@@ -117,9 +117,9 @@ func TestErrorDNParsing(t *testing.T) {
117117
for test, answer := range testcases {
118118
_, err := ParseDN(test)
119119
if err == nil {
120-
t.Errorf("Expected %s to fail parsing but succeeded\n", test)
120+
t.Errorf("Expected '%s' to fail parsing but succeeded\n", test)
121121
} else if err.Error() != answer {
122-
t.Errorf("Unexpected error on %s:\n%s\nvs.\n%s\n", test, answer, err.Error())
122+
t.Errorf("Unexpected error on: '%s':\nExpected: %s\nGot: %s\n", test, answer, err.Error())
123123
}
124124
}
125125
}

ldap.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,6 @@ func DebugBinaryFile(fileName string) error {
314314
return nil
315315
}
316316

317-
var hex = "0123456789abcdef"
318-
319317
func mustEscape(c byte) bool {
320318
return c > 0x7f || c == '(' || c == ')' || c == '\\' || c == '*' || c == 0
321319
}
@@ -324,6 +322,7 @@ func mustEscape(c byte) bool {
324322
// characters in the set `()*\` and those out of the range 0 < c < 0x80,
325323
// as defined in RFC4515.
326324
func EscapeFilter(filter string) string {
325+
const hexValues = "0123456789abcdef"
327326
escape := 0
328327
for i := 0; i < len(filter); i++ {
329328
if mustEscape(filter[i]) {
@@ -338,8 +337,8 @@ func EscapeFilter(filter string) string {
338337
c := filter[i]
339338
if mustEscape(c) {
340339
buf[j+0] = '\\'
341-
buf[j+1] = hex[c>>4]
342-
buf[j+2] = hex[c&0xf]
340+
buf[j+1] = hexValues[c>>4]
341+
buf[j+2] = hexValues[c&0xf]
343342
j += 3
344343
} else {
345344
buf[j] = c

0 commit comments

Comments
 (0)