CVE-2026-24130
LDAP search filter injection in Moonraker allows brute-force user discovery.
- CVSS 2.7
- CWE-90
- Input Validation and Sanitization
- Remote
Moonraker is a Python web server providing API access to Klipper 3D printing firmware. In versions 0.9.3 and below, instances configured with the "ldap" component enabled are vulnerable to LDAP search filter injection techniques via the login endpoint. The 401 error response message can be used to determine whether or not a search was successful, allowing for brute force methods to discover LDAP entries on the server such as user IDs and user attributes. This issue has been fixed in version 0.10.0.
- CWE
- CWE-90
- CVSS base score
- 2.7
- Published
- 2026-01-22
- OWASP
- A03 Injection
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- SQL Injection
- Accessibility scope
- Remote
- Impact
- Information Disclosure
- Affected component
- Moonraker
- Fixed by upgrading
- Yes
Solution
Upgrade Moonraker to version 0.10.0 or later.
Vulnerable code sample
import ldap
import logging
class LdapComponent:
def __init__(self, config):
# In a real application, these would be configured and a
# real LDAP connection would be established.
self.ldap_conn = self._connect_to_ldap(config)
self.base_dn = config.get('base_dn', 'dc=example,dc=com')
self.user_filter = config.get('user_filter', '({user_attr}={user})')
self.user_attr = config.get('user_attr', 'uid')
self.search_filter = f"(&{self.user_filter}(objectClass=posixAccount))"
def _connect_to_ldap(self, config):
# This is a placeholder for the actual LDAP connection logic.
# It would return a connected ldap.ldapobject.LDAPObject.
# For this demonstration, it is None.
return None
def authenticate_user(self, username, password):
if self.ldap_conn is None:
raise Exception("LDAP not configured or connection failed")
# VULNERABLE LOGIC
# The 'username' received from the user is directly formatted into the
# search filter without any form of sanitization or escaping.
# An attacker can inject valid LDAP filter syntax into the 'username'
# field to manipulate the query. For example, a username of
# "admin)(uid=*" would result in a filter of
# "(&(uid=admin)(uid=*)(objectClass=posixAccount))"
# which would search for any user if the 'admin' user doesn't exist.
try:
filt = self.search_filter.format(user_attr=self.user_attr, user=username)
except KeyError:
raise Exception("Server Misconfiguration: Invalid user_filter")
try:
results = self.ldap_conn.search_s(
self.base_dn, ldap.SCOPE_SUBTREE, filt, [self.user_attr]
)
except ldap.FILTER_ERROR:
# This exception might occur if the injection is malformed.
# The server's response path differs from a valid login attempt.
logging.info(f"LDAP: Invalid filter for user '{username}'")
raise Exception("Invalid Username or Password")
except ldap.LDAPError as e:
logging.exception("LDAP Search Error")
raise Exception(f"LDAP Communication Error: {e}")
# INFORMATION LEAK
# The server's logic path changes based on the number of results.
# An attacker can use the server's final 401 response to determine
# if their injection resulted in 0, 1, or >1 results, allowing
# them to blindly enumerate data from the LDAP directory.
if not results:
# Path for 0 results.
raise Exception("Invalid Username or Password")
if len(results) > 1:
# Path for >1 results (e.g., from a successful '*' injection).
# This behavior is distinct from the "not found" case.
logging.warning(f"LDAP: Ambiguous user search for '{username}'")
raise Exception("Invalid Username or Password")
# Path for exactly 1 result.
user_dn = results[0][0]
if not user_dn:
raise Exception("Invalid Username or Password")
try:
# Attempt to bind with the found user DN and the provided password.
self.ldap_conn.simple_bind_s(user_dn, password)
return user_dn
except ldap.INVALID_CREDENTIALS:
raise Exception("Invalid Username or Password")
except ldap.LDAPError as e:
logging.exception("LDAP Bind Error")
raise Exception(f"LDAP Communication Error: {e}")Patched code sample
import ldap.filter
# To make this example runnable without a live LDAP server or the python-ldap
# library fully installed, we define mock exception classes that mirror the
# real library's exceptions mentioned in the logic.
class LDAPError(Exception):
"""Base class for mock LDAP exceptions."""
pass
class INVALID_CREDENTIALS(LDAPError):
"""Mock exception for bad password."""
pass
class NO_SUCH_OBJECT(LDAPError):
"""Mock exception for user not found."""
pass
def fixed_ldap_authentication(username: str, password: str, ldap_connection):
"""
Demonstrates the fixed LDAP authentication logic, which prevents both
LDAP search filter injection and user enumeration through error responses,
as described in the CVE.
Args:
username: The username provided by the user.
password: The password provided by the user.
ldap_connection: An active LDAP connection object.
Returns:
A dictionary indicating the result of the authentication attempt.
Crucially, all failure messages are generic.
"""
base_dn = "ou=users,dc=example,dc=org"
# --- FIX 1: Prevent LDAP Injection by Escaping User Input ---
# The vulnerability occurs when user input is directly used to build the
# search filter, allowing special characters like *, (, ), and \ to manipulate
# the LDAP query.
#
# VULNERABLE CODE EXAMPLE (DO NOT USE):
# search_filter = f"(&(uid={username})(objectClass=inetOrgPerson))"
#
# THE FIX:
# Sanitize the user-provided 'username' by escaping all special LDAP
# filter characters. This ensures the input is treated as a literal string
# value and cannot alter the structure of the search filter.
escaped_username = ldap.filter.escape_filter_chars(username)
search_filter = f"(&(uid={escaped_username})(objectClass=inetOrgPerson))"
# --- FIX 2: Prevent User Enumeration via Generic Error Responses ---
# The vulnerability allowed an attacker to distinguish between a "user not
# found" error and an "invalid password" error. The fix is to ensure that
# the server's response is identical in all authentication failure cases.
try:
# In a real implementation, this performs the search.
# If the user doesn't exist, the search returns an empty list.
# result = ldap_connection.search_s(base_dn, ldap.SCOPE_SUBTREE, search_filter)
#
# if not result:
# # This specific exception is caught below and handled generically.
# raise NO_SUCH_OBJECT
#
# # If user is found, get their Distinguished Name (DN) and try to bind.
# user_dn = result[0][0]
# ldap_connection.simple_bind_s(user_dn, password)
# If we reach here, authentication was successful.
return {"status": "success", "token": "a-valid-session-token"}
except (INVALID_CREDENTIALS, NO_SUCH_OBJECT, IndexError):
# This block catches the primary authentication failure modes:
# - NO_SUCH_OBJECT: The user search filter returned no results.
# - IndexError: The search result was unexpectedly empty.
# - INVALID_CREDENTIALS: The user existed, but the password was wrong.
#
# By catching all of them and returning the *exact same* generic message,
# we prevent an attacker from learning whether a username is valid.
return {"status": "error", "message": "Invalid username or password"}
except LDAPError:
# Catch any other unexpected LDAP errors (e.g., server unavailable)
# and return a different, but still generic, server-side error.
return {"status": "error", "message": "Authentication service error"}Payload
`admin*))(&(uid=*`
Cite this entry
@misc{vaitp:cve202624130,
title = {{LDAP search filter injection in Moonraker allows brute-force user discovery.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-24130},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-24130/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
