Hello Meshery Security Team,
Reporting a High-severity, authenticated, *readable* SSRF in the Prometheus/Grafana
telemetry-connection handlers, following your SECURITY.md process. The attached zip
(meshery-ssrf-poc.zip) contains the full writeup (REPORT.md: description, code issues,
impact, PoC, captured output) plus two runnable Go tests that drive your real
PrometheusClient code, and their captured output.
SUMMARY
-------
The Prometheus/Grafana connection handlers accept a fully attacker-controlled URL with
NO host/scheme/IP validation, and fetch it with a plain &http.Client{} that has no SSRF
guard (a repo-wide search for isPrivate/IsLoopback/169.254/RFC1918/net.ParseIP over the
outbound paths returns nothing). The server fetches the URL on registration (blind) and
again on query, where the upstream response body is REFLECTED back to the caller. An
authenticated user can make Meshery read arbitrary internal/loopback/link-local endpoints
and exfiltrate the responses.
REACHABLE CHAIN (verified in source and executed against the real client)
-------------------------------------------------------------------------
- POST /api/telemetry/metrics/config (PrometheusConfigHandler, prometheus_handlers.go:163)
promURL := req.FormValue("prometheusURL") // attacker-controlled, no validation
... PrometheusClient.Validate(ctx, promURL, promKey) // GET promURL + "/api/v1/status/config"
... SaveUserCredential(... url: promURL ...) // persisted; returns connectionID
- GET /api/prometheus/query_range/{connectionID} (PrometheusQueryRangeHandler, prometheus_handlers.go:343)
url := connection.Metadata["url"]
data, _ := QueryRange(ctx, url, &reqQuery) // GET url + "/api/v1/query_range?..."
utils.WriteEscaped(w, data, "") // <-- upstream body returned to attacker
- Sink: grafana_helper.go:72 http.NewRequest(GET, queryURL) -> httpClient.Do (plain &http.Client{})
- Grafana variant identical (grafana/config + grafana/query/{id}); generic
POST /api/integrations/connections also stores an arbitrary connection URL.
All routes are behind AuthMiddleware/ProviderAuth. The attacker registers and queries
THEIR OWN connection (no victim, no BOLA). On the default local provider the auth bar is
a single built-in session.
IMPACT
------
1) Internal-network read (confirmed): GETs to localhost, RFC1918, the in-cluster
Kubernetes API, neighbouring pods — with responses reflected back.
2) AWS IMDSv1 credential theft -> cloud account compromise: the query path appends
"/api/v1/query_range", but a trailing '#' in the registered URL both defeats the
handler's TrimSuffix path-strip (it only trims when the string ENDS with the path, and
it ends with '#') and turns the appended suffix into a fragment that is never sent ->
full path control:
prometheusURL =
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>#
-> server requests exactly /latest/meta-data/iam/security-credentials/<role>
-> IAM role credentials reflected to the attacker.
Honest scope: only the Authorization header is attacker-influenceable (via prometheusKey);
the code does not set Metadata-Flavor/Metadata headers, so GCP/Azure metadata are NOT
directly readable and AWS IMDSv2 (PUT token) is NOT reachable. The cloud-cred path is
AWS-IMDSv1-specific; the internal-network read applies everywhere.
ESCALATION
----------
- Redirect-following (PROVEN, poc/OUTPUT-redirect.txt): the outbound &http.Client{} follows
cross-host 302s, so an attacker registers a benign host that redirects to any internal URL.
This removes the need for the '#' path trick AND defeats the obvious fix of validating only
the registration host (the fetched target is the redirect destination). Any future allowlist
must re-check every redirect hop at connect time.
- In-cluster -> cluster + cloud-account takeover: Meshery usually runs in-cluster. On EKS/EC2
with IMDSv1 reachable, the stolen worker-node IAM role can pull all ECR images, enumerate
compute, and impersonate the Kubernetes ServiceAccount of any pod on the node -> cluster
control (preconditions: IMDSv1 + metadata hop-limit >= 2; IMDSv2/hop-limit-1 mitigate this
specific leg). The kubelet read-only port (http://<node>:10255/pods, where enabled) is a
GET-readable source of pod env secrets. This makes the realistic impact Critical in common
in-cluster deployments.
SEVERITY
--------
High. CVSS:3.1 AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N = 8.5 (Critical impact on AWS IMDSv1;
~9.4 on deployments with open self-registration).
POC (attached)
--------------
- poc/prometheus_ssrf_test.go.txt -- drop into server/models/, `go test -run TestZZPrometheusSSRF -v`.
Drives the REAL NewPrometheusClient/Validate/QueryRange (no reimplementation).
- poc/OUTPUT.txt -- captured run: (1) server fetches loopback host with no guard,
(2) internal body reflected verbatim, (3) '#'-path-control reaches an exact IMDS path.
- Live curl reproduction is in the advisory.
FIX
---
Apply a shared SSRF-guarding http.Client (custom DialContext that resolves + rejects
loopback / link-local
169.254.0.0/16 / RFC1918, http(s)-only scheme) to the Grafana/
Prometheus clients (cmd/main.go:326-330), the design-import client, and the component
import fetch. Validate the URL at registration AND at request time (the stored
Metadata.url is the real sink), reject fragments, and strip the path with
scheme+"://"+host rather than TrimSuffix.
All testing was performed only against local loopback servers I control; no third-party
or production system was accessed. Happy to help validate a patch, and I'd appreciate a
CVE/credit if you agree it's warranted.
Im aware there is no reward policy , but if there is an exemption in this case , i will appreciate it , no hard feelings 😶🌫️Best regards,
Aditya Bisht
adityab...@gmail.com
You received this message because you are subscribed to the Google Groups "Meshery Security and Vulnerability Reports" group.
To unsubscribe from this group and stop receiving emails from it, send an email to
.