Skip to content

gee-ndvi-tiles returns the GEE service-account OAuth access token in the HTTP response body #5

@tg12

Description

@tg12

Summary

gee-ndvi-tiles returns the GEE OAuth 2.0 access token as a first-class field in its HTTP response body. Any caller who hits this endpoint receives a live, short-lived (1-hour) bearer token that grants direct access to the Google Earth Engine REST API under the project owner's service account.

Evidence

supabase/functions/gee-ndvi-tiles/index.ts, near the end of the handler:

const tileUrl = `https://earthengine.googleapis.com/v1/${mapData.name}/tiles/{z}/{x}/{y}`;

return new Response(
  JSON.stringify({ tileUrl, token }),   // ← GEE OAuth access token returned here
  { headers: { ...corsHeaders, "Content-Type": "application/json" } }
);

token is assigned from await getGeeAccessToken() at the top of the handler:

const token = await getGeeAccessToken();

getGeeAccessToken() exchanges the GEE_SERVICE_ACCOUNT_JSON secret for a Google OAuth 2.0 access token scoped to https://www.googleapis.com/auth/earthengine. Access tokens issued by Google have a 1-hour TTL.

The function runs with verify_jwt = false and wildcard CORS.

Why this matters

The GEE REST API access token can be used to:

  • Query any Earth Engine dataset (satellite imagery, climate data, elevation data) at the project owner's expense
  • Run arbitrary GEE value:compute operations consuming the service account's GEE quota
  • List and read any maps or exports associated with the GEE project
  • Potentially access Google Cloud resources if the service account has broader permissions

Each request to this endpoint generates a fresh 1-hour token. An attacker who polls this endpoint even occasionally has a continuous supply of valid GEE credentials.

Attack or failure scenario

  1. Attacker sends: curl -X POST https://ldqvretuwballytbywfc.supabase.co/functions/v1/gee-ndvi-tiles
  2. Response includes {"tileUrl":"...", "token":"ya29.a0..."} — live GEE OAuth token returned.
  3. Attacker uses the token directly against the GEE REST API to run expensive computations or export large datasets for 1 hour.
  4. Attacker calls this endpoint again before expiry to refresh the token indefinitely.

Root cause

The tile URL template {z}/{x}/{y} requires the GEE map token to be passed to the map client library (e.g., Mapbox GL) to authenticate tile requests. The developer passed the OAuth access token to the frontend for this purpose — but an OAuth bearer token is the wrong credential to expose here. GEE generates a separate short-lived, map-specific token embedded in the mapData.name URL path (the maps/TOKEN_ID identifier), which is all the frontend needs. The service-account-level OAuth token should never leave the backend.

Recommended fix

Return only tileUrl to the frontend — not token:

return new Response(
  JSON.stringify({ tileUrl }),  // token removed
  { headers: { ...corsHeaders, "Content-Type": "application/json" } }
);

The tileUrl already encodes the map resource identifier. Mapbox GL tile requests to GEE are authenticated by that resource path, not by the OAuth bearer token. The frontend should not send an Authorization header when fetching GEE tiles; the map resource ID path is the auth mechanism.

Additionally, this function must be auth-gated (see the related JWT auth issue).

Acceptance criteria

  • The token field is removed from the response of gee-ndvi-tiles
  • The GEE service account access token is never returned in any HTTP response body
  • Tile fetching still works correctly using only the tileUrl path

Suggested labels

security, bug

Priority

P0

Severity

Critical — live Google service account OAuth token exposed in HTTP response to any caller, valid for 1 hour and renewable on demand.

Confidence

Confirmed — JSON.stringify({ tileUrl, token }) is present in the response body of an unauthenticated endpoint.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions