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
- Attacker sends:
curl -X POST https://ldqvretuwballytbywfc.supabase.co/functions/v1/gee-ndvi-tiles
- Response includes
{"tileUrl":"...", "token":"ya29.a0..."} — live GEE OAuth token returned.
- Attacker uses the token directly against the GEE REST API to run expensive computations or export large datasets for 1 hour.
- 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
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.
Summary
gee-ndvi-tilesreturns 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:tokenis assigned fromawait getGeeAccessToken()at the top of the handler:getGeeAccessToken()exchanges theGEE_SERVICE_ACCOUNT_JSONsecret for a Google OAuth 2.0 access token scoped tohttps://www.googleapis.com/auth/earthengine. Access tokens issued by Google have a 1-hour TTL.The function runs with
verify_jwt = falseand wildcard CORS.Why this matters
The GEE REST API access token can be used to:
value:computeoperations consuming the service account's GEE quotaEach 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
curl -X POST https://ldqvretuwballytbywfc.supabase.co/functions/v1/gee-ndvi-tiles{"tileUrl":"...", "token":"ya29.a0..."}— live GEE OAuth token returned.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 themapData.nameURL path (themaps/TOKEN_IDidentifier), which is all the frontend needs. The service-account-level OAuth token should never leave the backend.Recommended fix
Return only
tileUrlto the frontend — nottoken:The
tileUrlalready 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 anAuthorizationheader 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
tokenfield is removed from the response ofgee-ndvi-tilestileUrlpathSuggested 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.