Ldap Injection in Fiber
How Ldap Injection Manifests in Fiber
LDAP injection in Fiber applications typically occurs when user-supplied input is concatenated directly into LDAP filter strings without proper sanitization. Fiber's LDAP integration patterns often involve constructing queries using fmt.Sprintf or string concatenation, creating injection opportunities.
Consider this common Fiber pattern:
func getUser(c *fiber.Ctx) error {
username := c.Query("username")
filter := fmt.Sprintf("(uid=%s)", username)
// LDAP query construction
searchRequest := ldap.NewSearchRequest(
"dc=example,dc=com",
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
filter,
[]string{"uid", "cn", "mail"},
nil,
)
sr, err := connection.Search(searchRequest)
if err != nil {
return c.Status(500).SendString("LDAP search failed")
}
return c.JSON(sr.Entries)
}The vulnerability becomes apparent when examining how LDAP filter syntax can be manipulated. An attacker can inject characters like *, (, ), and | to modify query logic:
// Malicious input: admin*)(|(uid=*This transforms the filter into:
(uid=admin*)(|(uid=*Which effectively becomes a logical OR, potentially returning all user entries or bypassing authentication entirely.
Another Fiber-specific pattern involves using LDAP for authentication:
func login(c *fiber.Ctx) error {
username := c.FormValue("username")
password := c.FormValue("password")
filter := fmt.Sprintf("(uid=%s)", username)
searchRequest := ldap.NewSearchRequest(
"dc=example,dc=com",
ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false,
filter,
[]string{"dn", "uid"},
nil,
)
sr, err := connection.Search(searchRequest)
if err != nil || len(sr.Entries) != 1 {
return c.Status(401).SendString("Invalid credentials")
}
userDN := sr.Entries[0].DN
// Bind with user credentials
err = connection.Bind(userDN, password)
if err != nil {
return c.Status(401).SendString("Invalid credentials")
}
return c.SendString("Login successful")
}Even though the bind operation uses the extracted DN, the initial search remains vulnerable. An attacker could potentially enumerate valid usernames or cause denial of service through malformed queries.
Fiber-Specific Detection
Detecting LDAP injection in Fiber applications requires examining both code patterns and runtime behavior. Static analysis can identify vulnerable code constructs:
package main
import (
"go/ast"
"go/parser"
"go/token"
"log"
"path/filepath"
)
func findLdapInjectionVulnerabilities(rootDir string) []string {
var vulnerableFiles []string
filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil || !strings.HasSuffix(path, ".go") {
return nil
}
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, path, nil, parser.AllErrors)
if err != nil {
return nil
}
ast.Inspect(f, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.CallExpr:
// Look for ldap.NewSearchRequest calls
if isLdapSearchCall(x) {
// Check if filter argument contains string concatenation
if hasUnsafeStringConcat(x.Args[6]) {
vulnerableFiles = append(vulnerableFiles, path)
}
}
}
return true
})
return nil
})
return vulnerableFiles
}
func isLdapSearchCall(expr *ast.CallExpr) bool {
if sel, ok := expr.Fun.(*ast.SelectorExpr); ok {
if x, ok := sel.X.(*ast.Ident); ok && x.Name == "ldap" && sel.Sel.Name == "NewSearchRequest" {
return true
}
}
return false
}
func hasUnsafeStringConcat(arg ast.Expr) bool {
switch a := arg.(type) {
case *ast.CallExpr:
// Check for fmt.Sprintf, strings.Join, etc.
if isUnsafeFormattingCall(a) {
return true
}
case *ast.BinaryExpr:
// Check for string concatenation
if a.Op == token.ADD && isStringLiteral(a.X) || isStringLiteral(a.Y) {
return true
}
}
return false
}Runtime detection with middleBrick provides another layer of security validation. The scanner identifies LDAP injection vulnerabilities by:
- Analyzing parameter handling patterns in API endpoints
- Checking for LDAP-specific query construction methods
- Testing for LDAP filter syntax injection possibilities
- Examining authentication flows that use LDAP
middleBrick's LDAP injection detection specifically looks for:
// Testing for LDAP filter injection
func testLdapInjection(endpoint string) (bool, string) {
testCases := []string{
"*",
"*user",
"admin*)(uid=*",
"*)(uid=admin",
"*)(&)",
}
for _, test := range testCases {
response := makeRequest(endpoint, test)
if isUnexpectedSuccess(response) {
return true, fmt.Sprintf("LDAP injection possible with: %s", test)
}
}
return false, "No LDAP injection detected"
}The scanner also checks for proper error handling, as LDAP injection attempts often reveal information through error messages that expose directory structure or query syntax.
Fiber-Specific Remediation
Remediating LDAP injection in Fiber requires adopting safe query construction practices. The primary defense is using parameterized LDAP filters instead of string concatenation.
Using ldap3's filter construction utilities:
import "github.com/go-ldap/ldap/v3"
func getUserSafe(c *fiber.Ctx) error {
username := c.Query("username")
// Use proper filter escaping
escapedUsername := ldap.EscapeFilter(username)
filter := fmt.Sprintf("(uid=%s)", escapedUsername)
searchRequest := ldap.NewSearchRequest(
"dc=example,dc=com",
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
filter,
[]string{"uid", "cn", "mail"},
nil,
)
sr, err := connection.Search(searchRequest)
if err != nil {
return c.Status(500).SendString("LDAP search failed")
}
return c.JSON(sr.Entries)
}Better approach using filter builders:
import "github.com/go-ldap/ldap/v3"
func getUserSafeBuilder(c *fiber.Ctx) error {
username := c.Query("username")
// Construct filter using builder pattern
filter := ldap.NewFilter()
filter.AddCondition("uid", ldap.FilterEquality, username)
searchRequest := ldap.NewSearchRequest(
"dc=example,dc=com",
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
filter.String(),
[]string{"uid", "cn", "mail"},
nil,
)
sr, err := connection.Search(searchRequest)
if err != nil {
return c.Status(500).SendString("LDAP search failed")
}
return c.JSON(sr.Entries)
}For authentication scenarios, implement proper input validation and use context-aware LDAP operations:
func loginSecure(c *fiber.Ctx) error {
username := c.FormValue("username")
password := c.FormValue("password")
// Validate input length and allowed characters
if len(username) > 255 || len(password) > 255 {
return c.Status(400).SendString("Input too long")
}
if !isValidUsername(username) {
return c.Status(400).SendString("Invalid username format")
}
// Use search with proper escaping
escapedUsername := ldap.EscapeFilter(username)
filter := fmt.Sprintf("(uid=%s)", escapedUsername)
searchRequest := ldap.NewSearchRequest(
"dc=example,dc=com",
ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, 0, false,
filter,
[]string{"dn"},
nil,
)
sr, err := connection.Search(searchRequest)
if err != nil || len(sr.Entries) != 1 {
return c.Status(401).SendString("Invalid credentials")
}
userDN := sr.Entries[0].DN
// Use a dedicated bind context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = connection.Bind(userDN, password)
if err != nil {
return c.Status(401).SendString("Invalid credentials")
}
return c.SendString("Login successful")
}
func isValidUsername(username string) bool {
// Allow only alphanumeric, dot, underscore, hyphen
matched, _ := regexp.MatchString(`^[a-zA-Z0-9._-]+$`, username)
return matched
}Implementing rate limiting for LDAP operations prevents brute force attacks:
var ldapLimiter = middleware.NewRateLimit(10, time.Minute, "ldap:")
func loginWithRateLimit(c *fiber.Ctx) error {
if !ldapLimiter.Allow(c.IP()) {
return c.Status(429).SendString("Too many LDAP attempts")
}
return loginSecure(c)
}