Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions v3/ldap.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package ldap

import (
"encoding/hex"
"fmt"
"log"
"os"
"strings"
"unicode/utf8"

ber "github.com/go-asn1-ber/asn1-ber"
)
Expand Down Expand Up @@ -489,6 +491,18 @@ func EscapeDN(dn string) string {
builder := strings.Builder{}

for i, r := range dn {
// A byte that is not part of a valid UTF-8 sequence ranges as
// (utf8.RuneError, width 1). Writing the rune would silently replace
// the byte with U+FFFD, so two different values could escape to the
// same DN; escape the raw byte as an RFC 4514 hex pair instead.
if r == utf8.RuneError {
if _, size := utf8.DecodeRuneInString(dn[i:]); size == 1 {
builder.WriteByte('\\')
builder.WriteString(hex.EncodeToString([]byte{dn[i]}))
continue
}
}

// Escape leading and trailing spaces
if (i == 0 || i == len(dn)-1) && r == ' ' {
builder.WriteRune('\\')
Expand Down
3 changes: 3 additions & 0 deletions v3/ldap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,9 @@ func TestEscapeDN(t *testing.T) {
{name: "nullByte", dn: "\u0000te\x00st\x00user" + string(rune(0)), want: "\\00te\\00st\\00user\\00"},
{name: "variousCharacters", dn: "test\"+,;<>\\-_user", want: "test\\\"\\+\\,\\;\\<\\>\\\\-_user"},
{name: "multiByteRunes", dn: "test\u0391user ", want: "test\u0391user\\ "},
{name: "invalidUTF8Byte", dn: "test\xebuser", want: "test\\ebuser"},
{name: "truncatedUTF8Sequence", dn: "test\xc3", want: "test\\c3"},
{name: "replacementCharKeptDistinct", dn: "test\ufffduser", want: "test\ufffduser"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
11 changes: 8 additions & 3 deletions v3/postaladdress.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,19 @@ func (p *PostalAddress) Escape() string {
builder := &strings.Builder{}

for _, line := range p.lines {
for _, char := range line {
switch char {
// Iterate bytes, not runes: ranging a line that is not valid UTF-8
// would replace each invalid byte with U+FFFD, so two different lines
// could escape to the same value. The escaped characters are ASCII and
// ParsePostalAddress reads bytes, so every other octet passes through
// unchanged.
for i := 0; i < len(line); i++ {
switch line[i] {
case '\\':
builder.WriteString("\\5C")
case '$':
builder.WriteString("\\24")
default:
builder.WriteRune(char)
builder.WriteByte(line[i])
}
}

Expand Down
13 changes: 13 additions & 0 deletions v3/postaladdress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,19 @@ func TestPostalAddressEqual(t *testing.T) {
}
}

func TestPostalAddressEscapeInvalidUTF8(t *testing.T) {
// A line that is not valid UTF-8 must keep its original bytes when
// escaped; replacing them with U+FFFD would make two different lines
// escape to the same value.
addr := mustNewPostalAddress(t, []string{"caf\xe9 $1"})
escaped := addr.Escape()
assert.Equal(t, "caf\xe9 \\241$", escaped)

parsed, err := ParsePostalAddress(escaped)
assert.NoError(t, err)
assert.True(t, addr.Equal(parsed), "escaped form should parse back to the same lines")
}

func TestParsePostalAddress_Escape(t *testing.T) {
t.Run("incomplete escape", func(t *testing.T) {
_, err := ParsePostalAddress("AAAAAAAAAA\\")
Expand Down
Loading