Back to Blog
critical SEVERITY8 min read

LDAP Injection in Apache Airflow: How a Missing Escape Nearly Opened the Gates

A critical LDAP injection vulnerability in Apache Airflow's Flask-AppBuilder security manager allowed attackers to bypass authentication and gain unauthorized access by crafting malicious usernames. The flaw stemmed from unsanitized user input being directly interpolated into LDAP filter strings — a classic but devastating mistake. This post breaks down how the attack works, what was fixed, and how you can prevent similar issues in your own code.

O
By Orbis AppSec
Published May 8, 2026Reviewed June 3, 2026

Answer Summary

CVE-2026-35030 is a critical LDAP injection vulnerability (CWE-90) in Apache Airflow's Flask-AppBuilder security manager, written in Python. Attackers could supply a crafted username containing LDAP metacharacters to manipulate the authentication filter string and bypass login entirely. The fix applies `ldap3`'s `escape_filter_chars()` function to sanitize user-controlled input before it is interpolated into any LDAP filter expression, preventing filter logic from being altered by attacker-supplied data.

Vulnerability at a Glance

cweCWE-90
fixEscape all user-controlled input with `escape_filter_chars()` before embedding it in LDAP filter expressions
riskComplete authentication bypass; unauthorized administrative access to Apache Airflow
languagePython
root causeUser-supplied username interpolated directly into an LDAP filter string without escaping special characters
vulnerabilityLDAP Injection

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:

  1. The attacker closed the intended uid= attribute with admin)
  2. They injected a new condition (|(uid=*) — an OR clause that matches any user
  3. 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 — use escape_filter_chars() from ldap3.utils.conv
  • Java: Use javax.naming.directory with proper attribute encoding
  • Go: Use go-ldap which provides EscapeFilter()
  • .NET: Use DirectorySearcher with Filter property and escape with LDAP.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:


Upgrading and Mitigation

If you're running Apache Airflow with LDAP authentication:

  1. Update immediately to the patched version that includes this fix
  2. Audit your LDAP configuration — review what attributes and directory paths your bind account can access
  3. Review your logs — look for unusual login attempts with special characters in usernames ((, ), *, \)
  4. 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.

Frequently Asked Questions

What is LDAP injection?

LDAP injection is an attack where an adversary inserts LDAP metacharacters (such as `*`, `(`, `)`, `\`, or `NUL`) into input that is concatenated into an LDAP filter string, altering the filter's logic in unintended ways — for example, forcing an authentication check to always succeed.

How do you prevent LDAP injection in Python?

Always escape user-controlled strings with a function like `ldap3.utils.conv.escape_filter_chars()` before embedding them in an LDAP filter. Never build filter strings through raw string concatenation or f-strings with untrusted input.

What CWE is LDAP injection?

LDAP injection is classified as CWE-90: Improper Neutralization of Special Elements used in an LDAP Query.

Is input length validation enough to prevent LDAP injection?

No. Length limits reduce attack surface but do not neutralize LDAP metacharacters. An attacker can craft a short payload like `*)(&` that still breaks filter logic. Proper character escaping is required.

Can static analysis detect LDAP injection?

Yes. Tools such as Semgrep, Bandit, and commercial SAST platforms can trace tainted user input to LDAP filter sinks and flag unescaped concatenation. Orbis AppSec detected this exact pattern automatically and opened a remediation PR.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #66417

Related Articles

critical

How LDAP injection happens in C with OpenLDAP and how to fix it

A high-severity LDAP injection vulnerability was discovered in the OpenSIPS H350 module, where the `ldap_rfc4515_escape()` function failed to escape the NUL byte (`\0`) — one of the special characters defined in RFC 4515. This gap meant that crafted SIP URI values could bypass the escaping logic and manipulate LDAP filter queries. The fix adds explicit NUL byte escaping and replaces potentially unsafe `strncpy` calls with `memcpy` to ensure correct buffer handling.

critical

How LDAP injection happens in Python Apache Airflow FAB security manager and how to fix it

A critical LDAP injection vulnerability was discovered in Apache Airflow's FAB (Flask-AppBuilder) security manager, specifically in the `_search_ldap()` method of `override.py`. The `AUTH_LDAP_SEARCH_FILTER` configuration value was interpolated directly into LDAP filter strings without validation, enabling attackers who could influence that configuration value to craft malicious filters that bypass authentication or exfiltrate directory data. The fix adds structural validation of the filter stri

critical

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.

critical

How buffer overflow happens in C sprintf() and how to fix it

A critical buffer overflow vulnerability was discovered in the ArrowTest() function in main/main.c, where sprintf() was writing formatted strings to a 24-byte buffer without bounds checking. By replacing sprintf() with snprintf() and specifying the buffer size, the vulnerability was eliminated, preventing attackers from corrupting heap memory through oversized width or height parameters.

critical

How buffer overflow happens in C libficus.c sprintf() and how to fix it

A buffer overflow vulnerability was discovered in `runtime/ficus/impl/libficus.c` where `sprintf()` was used to write a formatted compiler version string into a fixed-size stack buffer without any bounds checking. The fix replaces both vulnerable `sprintf()` calls with `snprintf()`, passing `sizeof(cver)` as the maximum write length to ensure the buffer can never be overrun. This change eliminates the risk of stack memory corruption that could be triggered by an attacker with control over the bu

critical

How buffer overflow via strcpy() happens in C Kconfig parsing and how to fix it

A critical buffer overflow vulnerability was discovered in the Linux kernel's Kconfig build system where `strcpy()` copied user-controlled symbol values into a fixed-size buffer without bounds checking. This flaw in `scripts/kconfig/symbol.c` could allow attackers to overwrite adjacent memory when processing malicious Kconfig files. The fix replaces the unsafe `strcpy()` with `memcpy()` using explicit length calculations.