VAITP Dataset

← Back to the dataset

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.

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 ::