LDAP Injection in Apache Airflow: How a Missing Escape Nearly Opened the Gates
CVE: CVE-2026-35030 | Severity: 🔴 Critical | Component: Apache Airflow (FAB Auth Manager)
Introduction
Imagine handing a bouncer a guest list and trusting them to check names — but the bouncer doesn't actually read the list correctly if someone writes their name in a clever way. That's essentially what happened here.
A critical security vulnerability was discovered and patched in Apache Airflow's Flask-AppBuilder (FAB) authentication manager. The bug lives in the LDAP authentication handler, and it's a textbook example of LDAP injection — a vulnerability class that's been well-understood for decades, yet still surfaces in production systems.
The affected file: providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
If your organization uses Apache Airflow with LDAP-based authentication (a very common enterprise configuration), this vulnerability could have allowed any unauthenticated user to log in as any account — including administrators — without knowing their password.
Let's dig in.
What Is LDAP Injection?
LDAP (Lightweight Directory Access Protocol) is the backbone of enterprise authentication. Systems like Active Directory, OpenLDAP, and others use it to store and query user credentials, group memberships, and organizational data.
When an application authenticates a user against an LDAP directory, it typically constructs a filter string — a query that says something like:
Find me the user whose username is "alice"
In LDAP filter syntax, that looks like:
(&(objectClass=person)(uid=alice))
The problem arises when the value alice comes directly from user input — a login form, an API call, a command-line argument — and gets embedded into the filter string without any sanitization or escaping. An attacker who understands LDAP filter syntax can craft a username that changes the meaning of the entire query.
This is LDAP injection. It's the LDAP equivalent of SQL injection, and it's just as dangerous.
The Vulnerability Explained
Where It Lived
The vulnerability existed at line 2421 of override.py, inside the LDAP authentication handler of Airflow's FAB security manager. The code was constructing an LDAP filter using a Python f-string, directly embedding the user-supplied username:
# VULNERABLE CODE (simplified illustration)
ldap_filter = f"(&(objectClass=person)(uid={username}))"
This looks innocent at first glance. But LDAP filter strings have their own grammar, and special characters like (, ), *, \, and NUL carry semantic meaning within that grammar.
The Attack: Crafting a Malicious Username
An attacker visiting the Airflow login page doesn't enter a normal username. Instead, they enter something like:
admin)(|(uid=*
When this gets interpolated into the filter string, the result is:
(&(objectClass=person)(uid=admin)(|(uid=*))
Let's break down what just happened:
- The attacker closed the intended
uid=attribute withadmin) - They injected a new condition
(|(uid=*)— an OR clause that matches any user - The resulting filter is now logically equivalent to: "Find any user whose UID is 'admin' OR whose UID is anything at all"
Depending on how the application processes the LDAP response, this can result in:
- Authentication bypass: The query returns a valid user record, and the application grants access without verifying the password
- Privilege escalation: The attacker can target specific privileged accounts by name
- User enumeration: Crafted queries can probe the directory for existing usernames and attributes
Real-World Impact
In a production Airflow deployment, successful exploitation means:
- 🔓 Full access to DAGs, pipelines, and task execution — an attacker could trigger, modify, or delete any workflow
- 🔓 Access to stored connections and secrets — Airflow stores database credentials, API keys, and cloud provider secrets
- 🔓 Lateral movement — with admin access to Airflow, an attacker can pivot to connected systems (databases, cloud infrastructure, data warehouses)
- 🔓 Data exfiltration — pipelines often have read access to sensitive business data
This isn't a theoretical risk. Airflow is widely deployed in data engineering and MLOps pipelines at organizations of all sizes. A compromised Airflow instance is often a gateway to the crown jewels.
The Fix
What Changed
The fix applies proper LDAP escaping to the username before it's used in the filter string. This ensures that any special characters in user input are treated as literal data, not as LDAP syntax.
The correct approach is to escape characters that have special meaning in LDAP filter strings. According to RFC 4515, the following characters must be escaped:
| Character | Escaped Form |
|---|---|
* |
\2a |
( |
\28 |
) |
\29 |
\ |
\5c |
| NUL | \00 |
Before and After
Before (vulnerable):
# Direct interpolation — DANGEROUS
ldap_filter = f"(&(objectClass=person)(uid={username}))"
After (fixed):
from ldap3.utils.conv import escape_filter_chars
# Escape special characters before interpolation — SAFE
safe_username = escape_filter_chars(username)
ldap_filter = f"(&(objectClass=person)(uid={safe_username}))"
With the fix in place, if an attacker submits admin)(|(uid=*, it gets transformed into:
admin\29\28|\28uid=\2a
Which produces the harmless filter:
(&(objectClass=person)(uid=admin\29\28|\28uid=\2a))
This query will simply find no matching users (because no real user has a backslash-escaped username), and authentication fails safely.
Why This Fix Works
The escaping function converts every special LDAP character into its RFC 4515-compliant escaped form. The LDAP server receives the escaped string and treats the entire value as a literal string to match against — not as filter syntax to interpret. The attacker's ability to inject new filter clauses is completely neutralized.
Prevention & Best Practices
This vulnerability is a great teaching moment. Here's how to avoid it — and similar issues — in your own code.
1. Never Interpolate User Input Directly into Query Languages
This applies to SQL, LDAP, XPath, NoSQL queries, and anything else with a query grammar. Always use:
- Parameterized queries (for SQL)
- Escaping functions (for LDAP, XPath)
- Schema validation before the data ever reaches a query builder
# ❌ WRONG — for any query language
query = f"SELECT * FROM users WHERE name = '{user_input}'"
# ✅ RIGHT — parameterized
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))
2. Use Well-Maintained Libraries for LDAP Operations
Don't build LDAP filter strings manually. Use libraries that handle escaping for you:
- Python:
ldap3— useescape_filter_chars()fromldap3.utils.conv - Java: Use
javax.naming.directorywith proper attribute encoding - Go: Use
go-ldapwhich providesEscapeFilter() - .NET: Use
DirectorySearcherwithFilterproperty and escape withLDAP.EscapeFilterArgument()
3. Apply the Principle of Least Privilege to LDAP Bind Accounts
The service account your application uses to query LDAP should have read-only access to only the attributes it needs. This limits the blast radius if injection does occur.
# LDAP service account permissions — minimal footprint
Allow: read uid, cn, mail, memberOf
Deny: write, delete, modify schema
4. Validate Input Early
Before input ever reaches authentication logic, validate that usernames conform to expected patterns:
import re
def validate_username(username: str) -> bool:
# Only allow alphanumeric, dots, underscores, hyphens
pattern = r'^[a-zA-Z0-9._-]{1,64}$'
return bool(re.match(pattern, username))
if not validate_username(username):
raise ValueError("Invalid username format")
Note: validation is a defense-in-depth measure, not a substitute for escaping. Always escape even if you validate.
5. Enable Security Scanning in Your CI/CD Pipeline
Tools that can detect injection vulnerabilities in Python code:
| Tool | Type | Notes |
|---|---|---|
| Bandit | SAST | Python-specific, detects injection patterns |
| Semgrep | SAST | Highly configurable, great LDAP rules |
| Snyk Code | SAST | Good for dependency + code scanning |
| CodeQL | SAST | GitHub-native, deep taint analysis |
6. Reference Security Standards
This vulnerability maps to well-known security standards:
- OWASP Top 10: A03:2021 – Injection
- CWE-90: Improper Neutralization of Special Elements used in an LDAP Query ('LDAP Injection')
- OWASP Testing Guide: OTG-AUTHN-004 – Testing for LDAP Injection
Upgrading and Mitigation
If you're running Apache Airflow with LDAP authentication:
- Update immediately to the patched version that includes this fix
- Audit your LDAP configuration — review what attributes and directory paths your bind account can access
- Review your logs — look for unusual login attempts with special characters in usernames (
(,),*,\) - Consider MFA — even with LDAP, adding a second factor significantly reduces the risk of authentication bypass
Conclusion
This vulnerability is a reminder that injection attacks don't go away — they just find new surfaces. LDAP injection is decades old, yet it continues to appear in modern, widely-used software. The fix is straightforward: one function call to escape user input before it touches a query language.
The key takeaways:
- 🛡️ Always escape user input before embedding it in LDAP filters, SQL queries, or any query language
- 🛡️ Use libraries that handle escaping rather than building filter strings manually
- 🛡️ Validate input early, but don't rely on validation alone
- 🛡️ Integrate SAST tools into your CI/CD pipeline to catch injection patterns before they reach production
- 🛡️ Apply least privilege to service accounts to limit the impact of any successful injection
Security vulnerabilities like this one are caught and fixed every day by diligent developers, security researchers, and automated scanning tools. The goal isn't perfection — it's building systems where mistakes are caught quickly, fixed thoroughly, and learned from openly.
Stay secure, and keep escaping your inputs. 🔐
This vulnerability was identified and fixed as part of an automated security scanning process. For more information on proactive security scanning for your codebase, visit OrbisAI Security.