GitHub Security Advisory: GHSA-5835-4gvc-32pc
Package: github.com/foxcpp/maddy (GO)
Severity: HIGH (CVSS 8.2)
CWE: CWE-90: Improper Neutralization of Special Elements used in an LDAP Query ('LDAP Injection')
Vulnerable versions: < 0.9.3
Patched in: 0.9.3

Summary: Maddy Mail Server has an LDAP Filter Injection via Unsanitized Username

Details:
### Summary

The `auth.ldap` module constructs LDAP search filters and DN strings by directly interpolating user-supplied usernames via `strings.ReplaceAll()` without any LDAP filter escaping. An attacker who can reach the SMTP submission (AUTH PLAIN) or IMAP LOGIN interface can inject arbitrary LDAP filter expressions through the username field, enabling identity spoofing, LDAP directory enumeration, and attribute value extraction. The `go-ldap/ldap/v3` library—already imported in the same file—provides `ldap.EscapeFilter()` specifically for this purpose, but it is never called.

### Patched version

Upgrade to maddy 0.9.3.

### Details

**Affected file:** `internal/auth/ldap/ldap.go`

Three locations substitute the raw, attacker-controlled `username` into LDAP filter or DN strings with no escaping:

**1. `Lookup()` — line 228 (filter injection)**

```go
func (a *Auth) Lookup(_ context.Context, username string) (string, bool, error) {
    // ...
    req := ldap.NewSearchRequest(
        a.baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
        2, 0, false,
        strings.ReplaceAll(a.filterTemplate, "{username}", username),  // <-- NO ESCAPING
        []string{"dn"}, nil)
```

**2. `AuthPlain()` — line 255 (DN template injection)**

```go
func (a *Auth) AuthPlain(username, password string) error {
    // ...
    if a.dnTemplate != "" {
        userDN = strings.ReplaceAll(a.dnTemplate, "{username}", username)  // <-- NO ESCAPING
```

**3. `AuthPlain()` — line 260 (filter injection)**

```go
    } else {
        req := ldap.NewSearchRequest(
            a.baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
            2, 0, false,
            strings.ReplaceAll(a.filterTemplate, "{username}", username),  // <-- NO ESCAPING
            []string{"dn"}, nil)
```

The `go-ldap/ldap/v3` library (v3.4.10, imported at line 17) provides `ldap.EscapeFilter()` which escapes `(`, `)`, `*`, `\`, and NUL per RFC 4515. It is never called on user input.

**No input validation or filter escaping occurs at any point from the protocol handler to the LDAP query.**

### PoC

**Prerequisites:**
- A maddy instance configured with `auth.ldap` using a `filter` directive
- An LDAP directory (e.g., OpenLDAP) with at least one user
- Network access to maddy's SMTP submission port (587) or IMAP port (993/143)

**Step 1: Vulnerable maddy configuration**

```
auth.ldap ldap_auth {
    urls ldap://ldapserver:389
    bind plain "cn=admin,dc=example,dc=org" "adminpassword"
    base_dn "ou=people,dc=example,dc=org"
    filter "(&(objectClass=inetOrgPerson)(uid={username}))"
}

submission tcp://0.0.0.0:587 {
    auth &ldap_auth
    # ...
}
```

Assume the LDAP directory contains users `alice` (password: `alice_pass`) and `bob` (password: `bob_pass`).

**Step 2: Verify normal authentication works**

```bash
# Encode AUTH PLAIN: \x00alice\x00alice_pass
AUTH_BLOB=$(printf '\x00alice\x00alice_pass' | base64)

# Connect via SMTP submission with STARTTLS
openssl s_client -co