Jan 23, 2026·5 min read·26 visits
Moonraker versions 0.9.3 and below fail to sanitize usernames before passing them to an LDAP query. This allows attackers to inject LDAP filters. By observing subtle differences in HTTP 401 error responses, an attacker can map out the directory structure and harvest valid usernames. Fixed in version 0.10.0.
A classic LDAP injection vulnerability in the Moonraker API server allows unauthenticated attackers to query the backend directory service via the login endpoint. By crafting malicious usernames, attackers can trigger a blind injection oracle to enumerate users and extract attribute data.
There is an unwritten rule in software development: as soon as a project designed for hobbyists decides to add 'Enterprise' features, a security researcher gets their wings. Moonraker is the Python-based API server that powers Klipper, the high-performance 3D printing firmware that enthusiasts swear by. It’s the brain that lets you upload G-code, monitor temperatures, and watch your print fail in real-time.
Somewhere along the line, someone decided that local authentication wasn't enough. They needed to authenticate their 3D printer against Active Directory. Why? Perhaps to ensure that only the VP of Engineering can print a low-poly Pikachu. Regardless of the reason, Moonraker implemented an LDAP authentication component. And like so many before them, they treated user input like a trusted friend rather than a toxic payload.
The vulnerability here is text-book CWE-90: LDAP Injection. It stems from the exact same root cause as SQL injection—mixing data with code. In the world of LDAP, search filters are defined by parentheses and logical operators like & (AND), | (OR), and ! (NOT).
The developers constructed the LDAP search filter using a Python f-string, directly embedding the username provided in the HTTP request into the filter query. They assumed the username would be alphanumeric. They assumed wrong.
When an attacker provides a username containing characters like *, (, or ), they aren't just providing a name; they are rewriting the logic of the database query. Because the application didn't escape these characters, the backend LDAP server interprets them as control codes.
Let's look at the crime scene in moonraker/components/ldap.py. The vulnerable code takes the user input and drops it straight into the filter string. This is the digital equivalent of leaving your front door unlockable because 'nobody would try the handle.'
def _perform_ldap_auth(self, username, password) -> None:
# ... setup ...
attr_name = "sAMAccountName" if self.active_directory else "uid"
# VULNERABILITY: Direct interpolation of 'username'
ldfilt = f"(&(objectClass=Person)({attr_name}={username}))"
if self.user_filter:
# ALSO BAD: Direct replace
ldfilt = self.user_filter.replace("USERNAME", username)
try:
with ldap3.Connection(server, **conn_args) as conn:
# The query executes with the manipulated filter
ret = conn.search(search_base, ldfilt, attributes=['*'])The fix is simple and boring, which is exactly how security patches should be. They imported escape_filter_chars from the ldap3 library and sanitized the input before it ever touched the query string.
from ldap3.utils.conv import escape_filter_chars
def _perform_ldap_auth(self, username: str, password: str) -> None:
# ... setup ...
# SANITIZATION: Escape the nasty characters
escaped_user = escape_filter_chars(username)
ldfilt = f"(&(objectClass=Person)({attr_name}={escaped_user}))"
if self.user_filter:
ldfilt = self.user_filter.replace("USERNAME", escaped_user)So we can inject into the query. Now what? We can't see the LDAP server's console, and we (usually) don't get the query results back in the login error message. However, we have a Side Channel Oracle.
Moonraker returns distinct responses depending on why the login failed. In a secure system, a failed login should always say "Invalid Credentials." But here, the system leaks state:
If the server returns slightly different 401 errors (e.g., different timing, different error message content, or different headers) for these two states, we have a boolean oracle: True (User Exists) or False (User Missing).
*)(uid=*. The filter becomes (&(objectClass=Person)(uid=*)(uid=*)). This essentially asks: "Is there any user with a UID?"admin*)(telephonenumber=555*. If we get Case A, we know the admin's phone number starts with 555. If we get Case B, it doesn't.By iterating through the character set, we can slowly dump the entire directory, attribute by attribute, just by watching the error messages.
The CVSS score is a measly 2.7 (Low). Why? because the industry metric calculator assumes that reading LDAP attributes isn't that big of a deal compared to Remote Code Execution. But let's look at this through a hacker's lens.
This is a Reconnaissance Gold Mine. If I am targeting an organization, this vulnerability allows me to valid usernames, email addresses, phone numbers, and potentially internal group memberships. I can map your entire org chart without ever sending a valid password.
Once I have a valid list of users (obtained via this leak), I can switch from blind guessing to Password Spraying. I can target specific high-value users. While I can't print a gun with this bug alone, I can certainly find the person who has the permission to do so.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N/E:U| Product | Affected Versions | Fixed Version |
|---|---|---|
Moonraker Arksine | < 0.10.0 | 0.10.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-90 (LDAP Injection) |
| CVSS v4.0 | 2.7 (Low) |
| Attack Vector | Network (API) |
| Privileges Required | None |
| User Interaction | None |
| Impact | Confidentiality (Low) |
| Patch Status | Fixed in 0.10.0 |
Improper Neutralization of Special Elements used in an LDAP Query ('LDAP Injection')
A high-severity access control vulnerability in ToolHive CLI before v0.30.1 and ToolHive Studio before v0.38.0 allows local containerized MCP servers to bypass network isolation. This enables malicious workloads to establish TCP/IP connections to administrative and control plane endpoints exposed on the host loopback interface.
AnyCable is a real-time communication server. Prior to version 1.6.15, its Pusher-compatible REST API suffered from an authentication bypass vulnerability because it failed to verify that the request body matched the signature-validated body_md5 parameter. This allows attackers to perform replay attacks with modified body contents.
A denial-of-service vulnerability exists in AnyIO prior to version 4.14.2. Standard error streams of process-pool workers are connected to an operating system pipe that is never drained by the parent process. This allows a worker to fill the pipe buffer and deadlock indefinitely.
CVE-2026-63349 is a critical privilege-dropping bypass vulnerability in the AnyIO asynchronous framework (versions 4.14.0 and 4.14.1) on POSIX platforms. Due to a variable assignment typo, supplementary groups specified by the developer are not correctly propagated to the execution backend, resulting in subprocesses retaining the parent process's elevated supplementary group permissions.
CVE-2026-63406 is an information disclosure vulnerability in AnyCable-go prior to version 1.6.15. The built-in telemetry client is enabled by default with a hardcoded public authentication token ('secret'). This client digests highly sensitive configuration parameters and command-line arguments, including JWT secrets and RPC secrets, into a stable SHA-256 fingerprint. This fingerprint is sent over public networks, exposing those administrative secrets to offline dictionary and brute-force attacks if intercepted.
CVE-2026-84992 is a Cross-Site Scripting (XSS) vulnerability affecting md-editor-v3 before version 6.5.4. It occurs because the fenced-code block language parser directly interpolates unescaped language metadata into unquoted HTML attributes inside the custom rendering callback. This bypasses the built-in XSSPlugin which runs during the parsing phase, before rendering.