build(deps): bump github.com/go-ldap/ldap/v3 from 3.4.13 to 3.4.14

Bumps [github.com/go-ldap/ldap/v3](https://github.com/go-ldap/ldap) from 3.4.13 to 3.4.14.
- [Release notes](https://github.com/go-ldap/ldap/releases)
- [Commits](https://github.com/go-ldap/ldap/compare/v3.4.13...v3.4.14)

---
updated-dependencies:
- dependency-name: github.com/go-ldap/ldap/v3
  dependency-version: 3.4.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2026-08-03 09:11:12 +00:00
committed by Ralf Haferkamp
parent 7c2cb14a80
commit b9adb507f8
22 changed files with 402 additions and 113 deletions

4
go.mod
View File

@@ -23,7 +23,7 @@ require (
github.com/go-chi/chi/v5 v5.3.1
github.com/go-chi/render v1.0.3
github.com/go-jose/go-jose/v3 v3.0.5
github.com/go-ldap/ldap/v3 v3.4.13
github.com/go-ldap/ldap/v3 v3.4.14
github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3
github.com/go-micro/plugins/v4/client/grpc v1.2.1
github.com/go-micro/plugins/v4/logger/zerolog v1.2.0
@@ -203,7 +203,7 @@ require (
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/gdexlab/go-render v1.0.1 // indirect
github.com/go-acme/lego/v4 v4.4.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/go-git/go-git/v5 v5.19.1 // indirect

8
go.sum
View File

@@ -370,8 +370,8 @@ github.com/go-acme/lego/v4 v4.4.0 h1:uHhU5LpOYQOdp3aDU+XY2bajseu8fuExphTL1Ss6/Fc
github.com/go-acme/lego/v4 v4.4.0/go.mod h1:l3+tFUFZb590dWcqhWZegynUthtaHJbG2fevUpoOOE0=
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-asn1-ber/asn1-ber v1.4.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-asn1-ber/asn1-ber v1.5.8 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ=
github.com/go-asn1-ber/asn1-ber v1.5.8/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
@@ -402,8 +402,8 @@ github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBj
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-ldap/ldap/v3 v3.1.7/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs=
github.com/go-ldap/ldap/v3 v3.4.14/go.mod h1:S4eJUMUNjDkE0ZJtIZdybwyb03sGGLW6gxXT1Hs8VKA=
github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3 h1:sfz1YppV05y4sYaW7kXZtrocU/+vimnIWt4cxAYh7+o=
github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3/go.mod h1:ZXFhGda43Z2TVbfGZefXyMJzsDHhCh0go3bZUcwTx7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=

View File

@@ -19,6 +19,10 @@ import (
// no limit.
var MaxPacketLengthBytes int64 = math.MaxInt32
// MaxNestingDepth specifies the maximum allowed nesting depth when calling ReadPacket, DecodePacket, or
// DecodePacketErr. Set to 0 for no limit.
var MaxNestingDepth int = 1000
type Packet struct {
Identifier
Value interface{}
@@ -218,7 +222,7 @@ func printPacket(out io.Writer, p *Packet, indent int, printBytes bool) {
// ReadPacket reads a single Packet from the reader.
func ReadPacket(reader io.Reader) (*Packet, error) {
p, _, err := readPacket(reader)
p, _, err := readPacket(reader, 0)
if err != nil {
return nil, err
}
@@ -278,7 +282,7 @@ func int64Length(i int64) (numBytes int) {
// DecodePacket decodes the given bytes into a single Packet
// If a decode error is encountered, nil is returned.
func DecodePacket(data []byte) *Packet {
p, _, _ := readPacket(bytes.NewBuffer(data))
p, _, _ := readPacket(bytes.NewBuffer(data), 0)
return p
}
@@ -286,7 +290,7 @@ func DecodePacket(data []byte) *Packet {
// DecodePacketErr decodes the given bytes into a single Packet
// If a decode error is encountered, nil is returned.
func DecodePacketErr(data []byte) (*Packet, error) {
p, _, err := readPacket(bytes.NewBuffer(data))
p, _, err := readPacket(bytes.NewBuffer(data), 0)
if err != nil {
return nil, err
}
@@ -294,12 +298,20 @@ func DecodePacketErr(data []byte) (*Packet, error) {
}
// readPacket reads a single Packet from the reader, returning the number of bytes read.
func readPacket(reader io.Reader) (*Packet, int, error) {
func readPacket(reader io.Reader, depth int) (*Packet, int, error) {
if MaxNestingDepth > 0 && depth >= MaxNestingDepth {
return nil, 0, fmt.Errorf("nesting depth %d exceeds maximum %d", depth, MaxNestingDepth)
}
identifier, length, read, err := readHeader(reader)
if err != nil {
return nil, read, err
}
if length != LengthIndefinite && MaxPacketLengthBytes > 0 && int64(length) > MaxPacketLengthBytes {
return nil, read, fmt.Errorf("length %d greater than maximum %d", length, MaxPacketLengthBytes)
}
p := &Packet{
Identifier: identifier,
}
@@ -326,13 +338,19 @@ func readPacket(reader io.Reader) (*Packet, int, error) {
}
// Read the next packet
child, r, err := readPacket(reader)
child, r, err := readPacket(reader, depth+1)
if err != nil {
return nil, read, unexpectedEOF(err)
}
contentRead += r
read += r
// Enforce the aggregate size limit for constructed packets. Indefinite length declares
// no bound up front, so the content bytes are only known as they are read.
if MaxPacketLengthBytes > 0 && int64(contentRead) > MaxPacketLengthBytes {
return nil, read, fmt.Errorf("length %d greater than maximum %d", contentRead, MaxPacketLengthBytes)
}
// Test is this is the EOC marker for our packet
if isEOCPacket(child) {
if length == LengthIndefinite {
@@ -351,11 +369,6 @@ func readPacket(reader io.Reader) (*Packet, int, error) {
return nil, read, errors.New("indefinite length used with primitive type")
}
// Read definite-length content
if MaxPacketLengthBytes > 0 && int64(length) > MaxPacketLengthBytes {
return nil, read, fmt.Errorf("length %d greater than maximum %d", length, MaxPacketLengthBytes)
}
var content []byte
if length > 0 {
// Read the content and limit it to the parsed length.

View File

@@ -40,7 +40,7 @@ func readLength(reader io.Reader) (length int, read int, err error) {
}
// Accumulate into a 64-bit variable
var length64 int64
var length64 uint64
for i := 0; i < lengthBytes; i++ {
b, err = readByte(reader)
if err != nil {
@@ -53,13 +53,14 @@ func readLength(reader io.Reader) (length int, read int, err error) {
// x.600, 8.1.3.5
length64 <<= 8
length64 |= int64(b)
length64 |= uint64(b)
}
// Cast to a platform-specific integer
length = int(length64)
// Ensure we didn't overflow
if int64(length) != length64 {
// Ensure we didn't overflow or wrap negative. Length octets are unsigned
// (x.600, 8.1.3.5), so a negative result is unrepresentable, not indefinite.
if length < 0 || uint64(length) != length64 {
return 0, read, errors.New("long-form length overflow")
}

View File

@@ -83,6 +83,9 @@ func (l *Conn) Add(addRequest *AddRequest) error {
return err
}
if len(packet.Children) < 2 {
return fmt.Errorf("ldap: malformed response: expected at least 2 children, got %d", len(packet.Children))
}
if packet.Children[1].Tag == ApplicationAddResponse {
err := GetLDAPError(packet)
if err != nil {

View File

@@ -3,13 +3,13 @@ package ldap
import (
"bytes"
"crypto/md5"
"crypto/rand"
"encoding/binary"
"encoding/hex"
enchex "encoding/hex"
"errors"
"fmt"
"io/ioutil"
"math/rand"
"strings"
"unicode/utf16"
@@ -221,12 +221,15 @@ func (l *Conn) DigestMD5Bind(digestMD5BindRequest *DigestMD5BindRequest) (*Diges
}
if len(params) > 0 {
resp := computeResponse(
resp, err := computeResponse(
params,
"ldap/"+strings.ToLower(digestMD5BindRequest.Host),
digestMD5BindRequest.Username,
digestMD5BindRequest.Password,
)
if err != nil {
return nil, fmt.Errorf("compute digest-md5 response: %s", err)
}
packet = ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request")
packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID"))
@@ -291,12 +294,22 @@ func parseParams(str string) (map[string]string, error) {
m := make(map[string]string)
var key, value string
var state int
var escaped bool
for i := 0; i <= len(str); i++ {
switch state {
case 0: // reading key
if i == len(str) {
return nil, fmt.Errorf("syntax error on %d", i)
}
// The digest-challenge is an RFC 2068 #rule (RFC 2831 section 2.1.1),
// which permits optional linear whitespace around the comma directive
// separators. Directive names are tokens that never contain
// whitespace, so skip it here; otherwise a directive following
// "..., name" is keyed with a leading space and the lookups in
// computeResponse (realm, nonce, authzid) miss it.
if str[i] == ' ' || str[i] == '\t' {
continue
}
if str[i] != '=' {
key += string(str[i])
continue
@@ -307,6 +320,14 @@ func parseParams(str string) (map[string]string, error) {
m[key] = value
break
}
// Linear whitespace outside a quoted string is not part of the
// value: an unquoted value is a token and a quoted value's content
// is read in the quoted state below. Skipping it lets a challenge
// using the whitespace the #rule allows (e.g. `nonce="n" , qop=auth`)
// parse the same as the unspaced form.
if str[i] == ' ' || str[i] == '\t' {
continue
}
switch str[i] {
case ',':
m[key] = value
@@ -325,20 +346,34 @@ func parseParams(str string) (map[string]string, error) {
if i == len(str) {
return nil, fmt.Errorf("syntax error on %d", i)
}
if str[i] != '"' {
switch {
case escaped:
// RFC 2831 section 7.1 quoted-pair: a backslash escapes the
// following character, so the next byte is taken literally
// (this is how a server sends a literal " or \ in a realm or
// nonce).
value += string(str[i])
} else {
escaped = false
case str[i] == '\\':
escaped = true
case str[i] == '"':
state = 1
default:
value += string(str[i])
}
}
}
return m, nil
}
func computeResponse(params map[string]string, uri, username, password string) string {
func computeResponse(params map[string]string, uri, username, password string) (string, error) {
nc := "00000001"
qop := "auth"
cnonce := enchex.EncodeToString(randomBytes(16))
rb, err := randomBytes(16)
if err != nil {
return "", err
}
cnonce := enchex.EncodeToString(rb)
x := username + ":" + params["realm"] + ":" + password
y := md5Hash([]byte(x))
@@ -361,14 +396,24 @@ func computeResponse(params map[string]string, uri, username, password string) s
resp := enchex.EncodeToString(md5Hash([]byte(kd)))
return fmt.Sprintf(
`username="%s",realm="%s",nonce="%s",cnonce="%s",nc=00000001,qop=%s,digest-uri="%s",response=%s`,
username,
params["realm"],
params["nonce"],
quotedStringEscape(username),
quotedStringEscape(params["realm"]),
quotedStringEscape(params["nonce"]),
cnonce,
qop,
uri,
quotedStringEscape(uri),
resp,
)
), nil
}
// quotedStringEscape escapes the two characters that may not appear unescaped
// inside a DIGEST-MD5 quoted string per RFC 2831 section 7.1: the backslash
// and the double quote. The backslash is replaced first so the quotes escaped
// afterwards are not doubled.
func quotedStringEscape(s string) string {
s = strings.ReplaceAll(s, `\`, `\\`)
s = strings.ReplaceAll(s, `"`, `\"`)
return s
}
func md5Hash(b []byte) []byte {
@@ -377,12 +422,12 @@ func md5Hash(b []byte) []byte {
return hasher.Sum(nil)
}
func randomBytes(len int) []byte {
b := make([]byte, len)
for i := 0; i < len; i++ {
b[i] = byte(rand.Intn(256))
func randomBytes(length int) ([]byte, error) {
b := make([]byte, length)
if _, err := rand.Read(b); err != nil {
return nil, err
}
return b
return b, nil
}
var externalBindRequest = requestFunc(func(envelope *ber.Packet) error {

View File

@@ -46,6 +46,9 @@ func (l *Conn) Compare(dn, attribute, value string) (bool, error) {
return false, err
}
if len(packet.Children) < 2 {
return false, fmt.Errorf("ldap: malformed response: expected at least 2 children, got %d", len(packet.Children))
}
if packet.Children[1].Tag == ApplicationCompareResponse {
err := GetLDAPError(packet)

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"net"
"net/url"
"strings"
"sync"
"sync/atomic"
"time"
@@ -112,7 +113,11 @@ type Conn struct {
outstandingRequests uint
messageMutex sync.Mutex
err error
// errMutex guards err only. It is a leaf lock: processMessages and reader
// record errors while another goroutine may hold messageMutex, so err must
// not share messageMutex or those writers could deadlock.
errMutex sync.Mutex
err error
}
var _ Client = &Conn{}
@@ -160,10 +165,18 @@ type DialContext struct {
func (dc *DialContext) dial(u *url.URL) (net.Conn, error) {
if u.Scheme == "ldapi" {
if u.Path == "" || u.Path == "/" {
u.Path = "/var/run/slapd/ldapi"
// RFC 4516 (and draft-chu-ldap-ldapi) put the socket path in the
// host component, percent-encoded; the path is an optional DN.
// parseLDAPURL has already decoded the host. Accept the older
// ldapi:///path form too so existing callers keep working.
path := u.Host
if path == "" {
path = u.Path
}
return dc.dialer.Dial("unix", u.Path)
if path == "" || path == "/" {
path = "/var/run/slapd/ldapi"
}
return dc.dialer.Dial("unix", path)
}
host, port, err := net.SplitHostPort(u.Host)
@@ -222,12 +235,33 @@ func DialTLS(network, addr string, config *tls.Config) (*Conn, error) {
return conn, nil
}
// parseLDAPURL parses an LDAP URL. It defers to net/url for the common
// ldap/ldaps/cldap schemes, but handles ldapi specially: the spec puts the
// unix socket path in the host, percent-encoded with %2F, and net/url rejects
// that as invalid. Pull the host out manually and decode it.
func parseLDAPURL(addr string) (*url.URL, error) {
const ldapi = "ldapi://"
if !strings.HasPrefix(addr, ldapi) {
return url.Parse(addr)
}
rest := addr[len(ldapi):]
host, path := rest, ""
if i := strings.IndexByte(rest, '/'); i >= 0 {
host, path = rest[:i], rest[i:]
}
decodedHost, err := url.PathUnescape(host)
if err != nil {
return nil, fmt.Errorf("ldapi: invalid host %q: %w", host, err)
}
return &url.URL{Scheme: "ldapi", Host: decodedHost, Path: path}, nil
}
// DialURL connects to the given ldap URL.
// The following schemas are supported: ldap://, ldaps://, ldapi://,
// and cldap:// (RFC1798, deprecated but used by Active Directory).
// On success a new Conn for the connection is returned.
func DialURL(addr string, opts ...DialOpt) (*Conn, error) {
u, err := url.Parse(addr)
u, err := parseLDAPURL(addr)
if err != nil {
return nil, NewError(ErrorNetwork, err)
}
@@ -338,11 +372,21 @@ func (l *Conn) nextMessageID() int64 {
// GetLastError returns the last recorded error from goroutines like processMessages and reader.
// Only the last recorded error will be returned.
func (l *Conn) GetLastError() error {
l.messageMutex.Lock()
defer l.messageMutex.Unlock()
l.errMutex.Lock()
defer l.errMutex.Unlock()
return l.err
}
// setError records the connection's last error. The background goroutines that
// call it (processMessages, reader, the per-request timeout helper and the
// SearchAsync worker) run concurrently with callers of GetLastError, so the
// write must take the mutex the getter reads under.
func (l *Conn) setError(err error) {
l.errMutex.Lock()
defer l.errMutex.Unlock()
l.err = err
}
// StartTLS sends the command to start a TLS session and then creates a new TLS Client
func (l *Conn) StartTLS(config *tls.Config) error {
if l.isTLS {
@@ -491,7 +535,7 @@ func (l *Conn) sendProcessMessage(message *messagePacket) bool {
func (l *Conn) processMessages() {
defer func() {
if err := recover(); err != nil {
l.err = fmt.Errorf("ldap: recovered panic in processMessages: %v", err)
l.setError(fmt.Errorf("ldap: recovered panic in processMessages: %v", err))
}
for messageID, msgCtx := range l.messageContexts {
// If we are closing due to an error, inform anyone who
@@ -541,7 +585,7 @@ func (l *Conn) processMessages() {
timer := time.NewTimer(time.Duration(requestTimeout))
defer func() {
if err := recover(); err != nil {
l.err = fmt.Errorf("ldap: recovered panic in RequestTimeout: %v", err)
l.setError(fmt.Errorf("ldap: recovered panic in RequestTimeout: %v", err))
}
timer.Stop()
@@ -563,7 +607,7 @@ func (l *Conn) processMessages() {
if msgCtx, ok := l.messageContexts[message.MessageID]; ok {
msgCtx.sendResponse(&PacketResponse{message.Packet, nil}, time.Duration(l.getTimeout()))
} else {
l.err = fmt.Errorf("ldap: received unexpected message %d, %v", message.MessageID, l.IsClosing())
l.setError(fmt.Errorf("ldap: received unexpected message %d, %v", message.MessageID, l.IsClosing()))
l.Debug.PrintPacket(message.Packet)
}
case MessageTimeout:
@@ -590,7 +634,7 @@ func (l *Conn) reader() {
cleanstop := false
defer func() {
if err := recover(); err != nil {
l.err = fmt.Errorf("ldap: recovered panic in reader: %v", err)
l.setError(fmt.Errorf("ldap: recovered panic in reader: %v", err))
}
if !cleanstop {
l.Close()

View File

@@ -565,12 +565,20 @@ func DecodeControl(packet *ber.Packet) (Control, error) {
case 1:
// just type, no criticality or value
packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")"
ControlType = packet.Children[0].Value.(string)
ct, ok := packet.Children[0].Value.(string)
if !ok {
return nil, fmt.Errorf("control type is not a string: %T", packet.Children[0].Value)
}
ControlType = ct
case 2:
packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")"
if packet.Children[0].Value != nil {
ControlType = packet.Children[0].Value.(string)
ct, ok := packet.Children[0].Value.(string)
if !ok {
return nil, fmt.Errorf("control type is not a string: %T", packet.Children[0].Value)
}
ControlType = ct
} else if packet.Children[0].Data != nil {
ControlType = packet.Children[0].Data.String()
} else {
@@ -579,9 +587,9 @@ func DecodeControl(packet *ber.Packet) (Control, error) {
// Children[1] could be criticality or value (both are optional)
// duck-type on whether this is a boolean
if _, ok := packet.Children[1].Value.(bool); ok {
if crit, ok := packet.Children[1].Value.(bool); ok {
packet.Children[1].Description = "Criticality"
Criticality = packet.Children[1].Value.(bool)
Criticality = crit
} else {
packet.Children[1].Description = "Control Value"
value = packet.Children[1]
@@ -589,10 +597,18 @@ func DecodeControl(packet *ber.Packet) (Control, error) {
case 3:
packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")"
ControlType = packet.Children[0].Value.(string)
ct, ok := packet.Children[0].Value.(string)
if !ok {
return nil, fmt.Errorf("control type is not a string: %T", packet.Children[0].Value)
}
ControlType = ct
packet.Children[1].Description = "Criticality"
Criticality = packet.Children[1].Value.(bool)
crit, ok := packet.Children[1].Value.(bool)
if !ok {
return nil, fmt.Errorf("criticality is not a bool: %T", packet.Children[1].Value)
}
Criticality = crit
packet.Children[2].Description = "Control Value"
value = packet.Children[2]
@@ -606,6 +622,9 @@ func DecodeControl(packet *ber.Packet) (Control, error) {
case ControlTypeManageDsaIT:
return NewControlManageDsaIT(Criticality), nil
case ControlTypePaging:
if value == nil {
return nil, fmt.Errorf("paging control value is missing")
}
value.Description += " (Paging)"
c := new(ControlPaging)
if value.Value != nil {
@@ -617,11 +636,21 @@ func DecodeControl(packet *ber.Packet) (Control, error) {
value.Value = nil
value.AppendChild(valueChildren)
}
if len(value.Children) == 0 {
return nil, fmt.Errorf("paging control value is empty")
}
value = value.Children[0]
value.Description = "Search Control Value"
if len(value.Children) < 2 {
return nil, fmt.Errorf("paging control value has %d children, expected 2", len(value.Children))
}
value.Children[0].Description = "Paging Size"
value.Children[1].Description = "Cookie"
c.PagingSize = uint32(value.Children[0].Value.(int64))
pagingSize, ok := value.Children[0].Value.(int64)
if !ok {
return nil, fmt.Errorf("paging size is not an integer: %T", value.Children[0].Value)
}
c.PagingSize = uint32(pagingSize)
c.Cookie = value.Children[1].Data.Bytes()
value.Children[1].Value = c.Cookie
return c, nil
@@ -729,7 +758,16 @@ func DecodeControl(packet *ber.Packet) (Control, error) {
c.ControlType = ControlType
c.Criticality = Criticality
if value != nil {
c.ControlValue = value.Value.(string)
// A non-conforming or malicious server can send a non-string
// (or nil) value here; the previous unchecked cast panicked
// the calling goroutine, see #561. Fall back to the raw bytes
// when the value isn't a string so we surface an error
// instead of crashing.
if s, ok := value.Value.(string); ok {
c.ControlValue = s
} else if value.Data != nil {
c.ControlValue = value.Data.String()
}
}
return c, nil
}
@@ -921,28 +959,44 @@ func (c *ControlServerSideSorting) GetControlType() string {
}
func NewControlServerSideSorting(value *ber.Packet) (*ControlServerSideSorting, error) {
sortKeys := []*SortKey{}
val, err := ber.DecodePacketErr(value.Data.Bytes())
if err != nil {
return nil, fmt.Errorf("decode packet err: %s", err)
}
val := value.Children[1].Children
if len(val) != 1 {
if len(val.Children) == 0 {
return nil, fmt.Errorf("no sequence value in packet")
}
sequences := val[0].Children
var sortKeys []*SortKey
for i, sequence := range sequences {
sortKey := new(SortKey)
if len(sequence.Children) < 2 {
return nil, fmt.Errorf("attributeType or matchingRule is missing from sequence %d", i)
for i, sequence := range val.Children {
if len(sequence.Children) < 1 || len(sequence.Children) > 3 {
return nil, fmt.Errorf("attributeType is missing from sequence %d", i)
}
sortKey.AttributeType = sequence.Children[0].Value.(string)
sortKey.MatchingRule = sequence.Children[1].Value.(string)
sortKey := new(SortKey)
if len(sequence.Children) == 3 {
sortKey.Reverse = sequence.Children[2].Value.(bool)
for _, child := range sequence.Children {
switch {
case child.ClassType == ber.ClassUniversal && child.Tag == ber.TagOctetString:
// A constructed-form OCTET STRING matches this case but leaves
// Value nil; guard the assertion so a malformed attributeType is
// rejected below rather than panicking.
if attrType, ok := child.Value.(string); ok {
sortKey.AttributeType = attrType
}
case child.ClassType == ber.ClassContext && child.Tag == 0:
sortKey.MatchingRule = child.Data.String()
case child.ClassType == ber.ClassContext && child.Tag == 1:
b := child.Data.Bytes()
sortKey.Reverse = len(b) > 0 && b[0] != 0
}
}
if sortKey.AttributeType == "" {
return nil, fmt.Errorf("attributeType is missing from sequence %d", i)
}
sortKeys = append(sortKeys, sortKey)
@@ -959,7 +1013,6 @@ func (c *ControlServerSideSorting) Encode() *ber.Packet {
packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control")
control := ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, c.GetControlType(), "Control Type")
value := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value")
seqs := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "SortKeyList")
for _, f := range c.SortKeys {
@@ -968,9 +1021,11 @@ func (c *ControlServerSideSorting) Encode() *ber.Packet {
seq.AppendChild(
ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, f.AttributeType, "attributeType"),
)
seq.AppendChild(
ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, f.MatchingRule, "orderingRule"),
)
if f.MatchingRule != "" {
seq.AppendChild(
ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, f.MatchingRule, "orderingRule"),
)
}
if f.Reverse {
seq.AppendChild(
ber.NewBoolean(ber.ClassContext, ber.TypePrimitive, 1, f.Reverse, "reverseOrder"),
@@ -980,7 +1035,7 @@ func (c *ControlServerSideSorting) Encode() *ber.Packet {
seqs.AppendChild(seq)
}
value.AppendChild(seqs)
value := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, string(seqs.Bytes()), "Control Value")
packet.AppendChild(control)
packet.AppendChild(value)
@@ -1060,6 +1115,8 @@ func NewControlServerSideSortingResult(pkt *ber.Packet) (*ControlServerSideSorti
return nil, err
}
control.Result = ControlServerSideSortingCode(codeInt)
return control, nil
}

View File

@@ -52,6 +52,9 @@ func (l *Conn) Del(delRequest *DelRequest) error {
return err
}
if len(packet.Children) < 2 {
return fmt.Errorf("ldap: malformed response: expected at least 2 children, got %d", len(packet.Children))
}
if packet.Children[1].Tag == ApplicationDelResponse {
err := GetLDAPError(packet)
if err != nil {

View File

@@ -1,14 +1,16 @@
package ldap
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
ber "github.com/go-asn1-ber/asn1-ber"
"sort"
"strings"
"unicode"
"unicode/utf8"
ber "github.com/go-asn1-ber/asn1-ber"
)
// AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514
@@ -94,9 +96,21 @@ func (d *DN) String() string {
func stripLeadingAndTrailingSpaces(inVal string) string {
noSpaces := strings.Trim(inVal, " ")
// Re-add the trailing space if it was an escaped space
if len(noSpaces) > 0 && noSpaces[len(noSpaces)-1] == '\\' && inVal[len(inVal)-1] == ' ' {
noSpaces = noSpaces + " "
// Re-add the trailing space only if it was escaped. A trailing space is
// escaped when it is preceded by an odd number of backslashes; an even
// number leaves the space unescaped (each "\\" is a literal backslash), so
// the space is insignificant and stays stripped. Counting only the final
// backslash treated "\\ " (a literal backslash plus an insignificant
// space) as an escaped space, keeping a spurious trailing space in the
// decoded value.
if len(noSpaces) > 0 && inVal[len(inVal)-1] == ' ' {
backslashes := 0
for i := len(noSpaces) - 1; i >= 0 && noSpaces[i] == '\\'; i-- {
backslashes++
}
if backslashes%2 == 1 {
noSpaces = noSpaces + " "
}
}
return noSpaces
@@ -116,6 +130,16 @@ func decodeString(str string) (string, error) {
// If the character is not an escape character, just add it to the
// builder and continue
if char != '\\' {
// RFC 4514 section 2.4: these characters must appear escaped
// (either as "\X" or as "\XX" hex) when present in an AttributeValue.
// Reject the raw form here so that callers don't silently accept
// input that violates the grammar.
switch char {
case '"', ';', '<', '>':
return "", fmt.Errorf("got unescaped character: '%s'", string(char))
case 0:
return "", fmt.Errorf("got unescaped NULL character")
}
builder.WriteRune(char)
continue
}
@@ -233,10 +257,18 @@ func decodeEncodedString(str string) (string, error) {
return "", fmt.Errorf("failed to decode BER encoding: %w", err)
}
packet, err := ber.DecodePacketErr(decoded)
// RFC 4514 section 2.4: the value following '#' is the hex encoding of the
// BER encoding of a single AttributeValue. Read exactly one element and
// reject any leftover octets, otherwise bytes appended after the value are
// silently dropped and two different DN strings decode to the same value.
reader := bytes.NewBuffer(decoded)
packet, err := ber.ReadPacket(reader)
if err != nil {
return "", fmt.Errorf("failed to decode BER encoding: %w", err)
}
if reader.Len() != 0 {
return "", errors.New("failed to decode BER encoding: trailing bytes after value")
}
return packet.Data.String(), nil
}
@@ -352,10 +384,19 @@ func (r *RelativeDN) Equal(other *RelativeDN) bool {
}
func (r *RelativeDN) hasAllAttributes(attrs []*AttributeTypeAndValue) bool {
// Each candidate attribute must match a distinct attribute of the receiver.
// Without consuming matches this is a set containment test, so a multi-valued
// RDN that repeats an attributeTypeAndValue would compare equal to one that
// repeats a different pair the same number of times.
matched := make([]bool, len(r.Attributes))
for _, attr := range attrs {
found := false
for _, myattr := range r.Attributes {
for i, myattr := range r.Attributes {
if matched[i] {
continue
}
if myattr.Equal(attr) {
matched[i] = true
found = true
break
}
@@ -415,10 +456,16 @@ func (r *RelativeDN) EqualFold(other *RelativeDN) bool {
}
func (r *RelativeDN) hasAllAttributesFold(attrs []*AttributeTypeAndValue) bool {
// See hasAllAttributes: matches are consumed so multiplicity is respected.
matched := make([]bool, len(r.Attributes))
for _, attr := range attrs {
found := false
for _, myattr := range r.Attributes {
for i, myattr := range r.Attributes {
if matched[i] {
continue
}
if myattr.EqualFold(attr) {
matched[i] = true
found = true
break
}

View File

@@ -89,6 +89,14 @@ func (l *Conn) Extended(er *ExtendedRequest) (*ExtendedResponse, error) {
}
for _, child := range extResp.Children {
// responseName [10] and responseValue [11] are context-class and
// optional. The preceding resultCode is a universal ENUMERATED whose
// tag number (10) is the same as responseName, so a child must be
// matched on its class as well, otherwise the resultCode is read as
// the responseName whenever the server omits the latter.
if child.ClassType != ber.ClassContext {
continue
}
switch child.Tag {
case ber.TagEnumerated:
response.Name = child.Data.String()

View File

@@ -131,7 +131,7 @@ func DecompileFilter(packet *ber.Packet) (_ string, err error) {
buf.WriteString(childStr)
case FilterSubstrings:
buf.WriteString(ber.DecodeString(packet.Children[0].Data.Bytes()))
buf.WriteString(EscapeFilter(ber.DecodeString(packet.Children[0].Data.Bytes())))
buf.WriteByte('=')
for i, child := range packet.Children[1].Children {
if i == 0 && child.Tag != FilterSubstringsInitial {
@@ -143,22 +143,22 @@ func DecompileFilter(packet *ber.Packet) (_ string, err error) {
}
}
case FilterEqualityMatch:
buf.WriteString(ber.DecodeString(packet.Children[0].Data.Bytes()))
buf.WriteString(EscapeFilter(ber.DecodeString(packet.Children[0].Data.Bytes())))
buf.WriteByte('=')
buf.WriteString(EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())))
case FilterGreaterOrEqual:
buf.WriteString(ber.DecodeString(packet.Children[0].Data.Bytes()))
buf.WriteString(EscapeFilter(ber.DecodeString(packet.Children[0].Data.Bytes())))
buf.WriteString(">=")
buf.WriteString(EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())))
case FilterLessOrEqual:
buf.WriteString(ber.DecodeString(packet.Children[0].Data.Bytes()))
buf.WriteString(EscapeFilter(ber.DecodeString(packet.Children[0].Data.Bytes())))
buf.WriteString("<=")
buf.WriteString(EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())))
case FilterPresent:
buf.WriteString(ber.DecodeString(packet.Data.Bytes()))
buf.WriteString(EscapeFilter(ber.DecodeString(packet.Data.Bytes())))
buf.WriteString("=*")
case FilterApproxMatch:
buf.WriteString(ber.DecodeString(packet.Children[0].Data.Bytes()))
buf.WriteString(EscapeFilter(ber.DecodeString(packet.Children[0].Data.Bytes())))
buf.WriteString("~=")
buf.WriteString(EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())))
case FilterExtensibleMatch:
@@ -181,14 +181,14 @@ func DecompileFilter(packet *ber.Packet) (_ string, err error) {
}
if len(attr) > 0 {
buf.WriteString(attr)
buf.WriteString(EscapeFilter(attr))
}
if dnAttributes {
buf.WriteString(":dn")
}
if len(matchingRule) > 0 {
buf.WriteString(":")
buf.WriteString(matchingRule)
buf.WriteString(EscapeFilter(matchingRule))
}
buf.WriteString(":=")
buf.WriteString(EscapeFilter(value))

View File

@@ -89,6 +89,9 @@ func (l *Conn) ModifyDN(m *ModifyDNRequest) error {
return err
}
if len(packet.Children) < 2 {
return fmt.Errorf("ldap: malformed response: expected at least 2 children, got %d", len(packet.Children))
}
if packet.Children[1].Tag == ApplicationModifyDNResponse {
err := GetLDAPError(packet)
if err != nil {

View File

@@ -121,6 +121,9 @@ func (l *Conn) Modify(modifyRequest *ModifyRequest) error {
return err
}
if len(packet.Children) < 2 {
return fmt.Errorf("ldap: malformed response: expected at least 2 children, got %d", len(packet.Children))
}
if packet.Children[1].Tag == ApplicationModifyResponse {
err := GetLDAPError(packet)
if err != nil {
@@ -159,6 +162,10 @@ func (l *Conn) ModifyWithResult(modifyRequest *ModifyRequest) (*ModifyResult, er
return nil, err
}
if len(packet.Children) < 2 {
return nil, fmt.Errorf("ldap: malformed response: expected at least 2 children, got %d", len(packet.Children))
}
switch packet.Children[1].Tag {
case ApplicationModifyResponse:
if err = GetLDAPError(packet); err != nil {

View File

@@ -93,6 +93,9 @@ func (l *Conn) PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*Pa
result := &PasswordModifyResult{}
if len(packet.Children) < 2 {
return nil, fmt.Errorf("ldap: malformed response: expected at least 2 children, got %d", len(packet.Children))
}
if packet.Children[1].Tag == ApplicationExtendedResponse {
if err = GetLDAPError(packet); err != nil {
result.Referral = getReferral(err, packet)
@@ -106,7 +109,10 @@ func (l *Conn) PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*Pa
extendedResponse := packet.Children[1]
for _, child := range extendedResponse.Children {
if child.Tag == ber.TagEmbeddedPDV {
passwordModifyResponseValue := ber.DecodePacket(child.Data.Bytes())
passwordModifyResponseValue, err := ber.DecodePacketErr(child.Data.Bytes())
if err != nil {
return nil, fmt.Errorf("ldap: failed to decode PasswordModifyResponseValue: %s", err)
}
if len(passwordModifyResponseValue.Children) == 1 {
if passwordModifyResponseValue.Children[0].Tag == ber.TagEOC {
result.GeneratedPassword = ber.DecodeString(passwordModifyResponseValue.Children[0].Data.Bytes())

View File

@@ -101,6 +101,14 @@ func getReferral(err error, packet *ber.Packet) (referral string) {
continue
}
// A Referral is a SEQUENCE SIZE (1..MAX) OF uri, but a non-conforming or
// malicious server can send an empty SEQUENCE. Skip it instead of indexing
// child.Children[0], which would panic the goroutine that called Modify or
// PasswordModify.
if len(child.Children) == 0 {
continue
}
if referral, ok = child.Children[0].Value.(string); ok {
return referral
}

View File

@@ -66,12 +66,28 @@ func (r *searchResponse) Next() bool {
return true
}
// send enqueues a result on the result channel, giving up when ctx is
// cancelled so an abandoned consumer cannot block the search goroutine
// forever on a full buffer. It reports whether the result was handed off to
// the channel; a true return does not mean the consumer received it. The
// give-up is best-effort: if ctx is already cancelled but buffer space is
// available, the result may still be enqueued. Callers that terminate the
// stream regardless of the outcome may ignore the return value.
func (r *searchResponse) send(ctx context.Context, res *SearchSingleResult) bool {
select {
case r.ch <- res:
return true
case <-ctx.Done():
return false
}
}
func (r *searchResponse) start(ctx context.Context, searchRequest *SearchRequest) {
go func() {
defer func() {
close(r.ch)
if err := recover(); err != nil {
r.conn.err = fmt.Errorf("ldap: recovered panic in searchResponse: %v", err)
r.conn.setError(fmt.Errorf("ldap: recovered panic in searchResponse: %v", err))
}
}()
@@ -84,14 +100,14 @@ func (r *searchResponse) start(ctx context.Context, searchRequest *SearchRequest
// encode search request
err := searchRequest.appendTo(packet)
if err != nil {
r.ch <- &SearchSingleResult{Error: err}
r.send(ctx, &SearchSingleResult{Error: err})
return
}
r.conn.Debug.PrintPacket(packet)
msgCtx, err := r.conn.sendMessage(packet)
if err != nil {
r.ch <- &SearchSingleResult{Error: err}
r.send(ctx, &SearchSingleResult{Error: err})
return
}
defer r.conn.finishMessage(msgCtx)
@@ -106,19 +122,19 @@ func (r *searchResponse) start(ctx context.Context, searchRequest *SearchRequest
case packetResponse, ok := <-msgCtx.responses:
if !ok {
err := NewError(ErrorNetwork, errors.New("ldap: response channel closed"))
r.ch <- &SearchSingleResult{Error: err}
r.send(ctx, &SearchSingleResult{Error: err})
return
}
packet, err = packetResponse.ReadPacket()
r.conn.Debug.Printf("%d: got response %p", msgCtx.id, packet)
if err != nil {
r.ch <- &SearchSingleResult{Error: err}
r.send(ctx, &SearchSingleResult{Error: err})
return
}
if r.conn.Debug {
if err := addLDAPDescriptions(packet); err != nil {
r.ch <- &SearchSingleResult{Error: err}
r.send(ctx, &SearchSingleResult{Error: err})
return
}
ber.PrintPacket(packet)
@@ -133,22 +149,26 @@ func (r *searchResponse) start(ctx context.Context, searchRequest *SearchRequest
},
}
if len(packet.Children) != 3 {
r.ch <- result
if !r.send(ctx, result) {
return
}
continue
}
decoded, err := DecodeControl(packet.Children[2].Children[0])
if err != nil {
werr := fmt.Errorf("failed to decode search result entry: %w", err)
result.Error = werr
r.ch <- result
r.send(ctx, result)
return
}
result.Controls = append(result.Controls, decoded)
r.ch <- result
if !r.send(ctx, result) {
return
}
case ApplicationSearchResultDone:
if err := GetLDAPError(packet); err != nil {
r.ch <- &SearchSingleResult{Error: err}
r.send(ctx, &SearchSingleResult{Error: err})
return
}
if len(packet.Children) == 3 {
@@ -157,33 +177,37 @@ func (r *searchResponse) start(ctx context.Context, searchRequest *SearchRequest
decodedChild, err := DecodeControl(child)
if err != nil {
werr := fmt.Errorf("failed to decode child control: %w", err)
r.ch <- &SearchSingleResult{Error: werr}
r.send(ctx, &SearchSingleResult{Error: werr})
return
}
result.Controls = append(result.Controls, decodedChild)
}
r.ch <- result
r.send(ctx, result)
}
foundSearchSingleResultDone = true
case ApplicationSearchResultReference:
ref := packet.Children[1].Children[0].Value.(string)
r.ch <- &SearchSingleResult{Referral: ref}
if !r.send(ctx, &SearchSingleResult{Referral: ref}) {
return
}
case ApplicationIntermediateResponse:
decoded, err := DecodeControl(packet.Children[1])
if err != nil {
werr := fmt.Errorf("failed to decode intermediate response: %w", err)
r.ch <- &SearchSingleResult{Error: werr}
r.send(ctx, &SearchSingleResult{Error: werr})
return
}
result := &SearchSingleResult{}
result.Controls = append(result.Controls, decoded)
r.ch <- result
if !r.send(ctx, result) {
return
}
default:
err := fmt.Errorf("unknown tag: %d", packet.Children[1].Tag)
r.ch <- &SearchSingleResult{Error: err}
r.send(ctx, &SearchSingleResult{Error: err})
return
}
}

View File

@@ -624,7 +624,10 @@ func (l *Conn) Search(searchRequest *SearchRequest) (*SearchResult, error) {
// SearchAsync performs a search request and returns all search results asynchronously.
// This means you get all results until an error happens (or the search successfully finished),
// e.g. for size / time limited requests all are received until the limit is reached.
// To stop the search, call cancel function of the context.
// To stop the search, call the cancel function of the context; Next may
// still deliver a few results received before the cancellation took effect.
// Cancellation is not reported as an error: Err returns nil, same as a
// successfully completed search.
func (l *Conn) SearchAsync(
ctx context.Context, searchRequest *SearchRequest, bufferSize int) Response {
r := newSearchResponse(l, bufferSize)
@@ -635,7 +638,8 @@ func (l *Conn) SearchAsync(
// Syncrepl is a short name for LDAP Sync Replication engine that works on the
// consumer-side. This can perform a persistent search and returns an entry
// when the entry is updated on the server side.
// To stop the search, call cancel function of the context.
// To stop the search, call the cancel function of the context; cancellation
// is not reported as an error, Err returns nil.
func (l *Conn) Syncrepl(
ctx context.Context, searchRequest *SearchRequest, bufferSize int,
mode ControlSyncRequestMode, cookie []byte, reloadHint bool,

View File

@@ -24,11 +24,18 @@ func (l *Conn) Unbind() error {
return ErrConnUnbound
}
_, err := l.doRequest(unbindRequest{})
msgCtx, err := l.doRequest(unbindRequest{})
if err != nil {
return err
}
// Finish the message context so its done channel is closed. Without
// this, a server-initiated disconnect racing with Close can deadlock:
// processMessages cleanup tries to deliver closeErr to the orphaned
// context via sendResponse, which blocks forever on the unclosed done
// channel, preventing chanConfirm from being signalled.
l.finishMessage(msgCtx)
// Sending an unbindRequest will make the connection unusable.
// Pending requests will fail with:
// LDAP Result Code 200 "Network Error": ldap: response channel closed

View File

@@ -19,5 +19,11 @@ func (l *Conn) WhoAmI(controls []Control) (*WhoAmIResult, error) {
return nil, err
}
return &WhoAmIResult{AuthzID: resp.Value.Data.String()}, nil
// responseValue is OPTIONAL (RFC 4532); Extended leaves Value nil when the
// server omits it. Guard the dereference and report an empty authzId.
result := &WhoAmIResult{}
if resp.Value != nil {
result.AuthzID = resp.Value.Data.String()
}
return result, nil
}

6
vendor/modules.txt vendored
View File

@@ -451,7 +451,7 @@ github.com/ggwhite/go-masker
## explicit; go 1.15
github.com/go-acme/lego/v4/acme
github.com/go-acme/lego/v4/challenge
# github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667
# github.com/go-asn1-ber/asn1-ber v1.5.8
## explicit; go 1.13
github.com/go-asn1-ber/asn1-ber
# github.com/go-chi/chi/v5 v5.3.1
@@ -539,8 +539,8 @@ github.com/go-jose/go-jose/v4/json
## explicit; go 1.17
github.com/go-kit/log
github.com/go-kit/log/level
# github.com/go-ldap/ldap/v3 v3.4.13
## explicit; go 1.24.0
# github.com/go-ldap/ldap/v3 v3.4.14
## explicit; go 1.25.0
github.com/go-ldap/ldap/v3
# github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3
## explicit; go 1.14