Bug bounty hunting is not always about chaining complex vulnerabilities for a critical RCE. Sometimes the most meaningful findings are sitting right there in the page source, quietly waiting for someone who knows what to look for.

This is the story of how I found an exposed, unrestricted Google Maps API key on the production checkout page of a major global brand, confirmed it was actively exploitable, mapped the full attack surface, and responsibly disclosed it through their official VDP portal. The brand name is not mentioned here as the report is currently under active review.

The Discovery

I was manually walking through the checkout flow of the target, a large e-commerce storefront built on Next.js. I had authorization to test it. I was using the FindSomething Chrome extension passively in the background while browsing — it automatically highlights API keys, tokens, and secrets found in page source and loaded scripts as you browse. While I was on the payment page, FindSomething flagged a Google API key pattern in the page.

Google API keys always begin with AIza, which makes them easy to spot. I switched to manual validation immediately.

I pulled the raw page source and confirmed the key location with a simple grep:

curl -sk "https://[REDACTED]/checkout/payment" | grep -oP '.{80}AIza[0–9A-Za-z_-]{35}.{80}'

Output:

fluidConfigure-[REDACTED].min.js","googleApiKey":"AIzaSy[REDACTED]","googleOptimizeContainerId":"","googleTagManagerId":"GTM-[REDACTED]"

The key was sitting in plain text inside the Next.js server-side rendered hydration JSON, passed as a configuration parameter to a third-party product configurator component loaded from an external CDN. It appeared twice in the same page — once escaped and once unescaped — because Next.js serializes the same config object in both formats during SSR hydration.

Finding a key in page source is only the beginning. The real question is whether it has any restrictions.

Why Restrictions Matter

Google accepts that Maps API keys will sometimes be visible in browser-accessible code for use cases like rendering a map widget. The key does not need to be completely hidden. But it absolutely must be restricted in two ways.

HTTP Referrer Restrictions ensure the key only works when requests come from specific domains like your own website. Any call from an arbitrary IP or external tool gets rejected.

API Scope Restrictions ensure the key only works for the specific APIs the application actually needs. If you only render a map, the key should only authorize the Maps JavaScript API — nothing else.

Without both of these controls in place, a key exposed in client-side HTML is a free pass for anyone to abuse your Google Cloud billing account.

Confirming the Key is Unrestricted

I called the Places Nearby Search API using the exposed key from my own machine with no referrer header:

curl "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=-33.8670522,151.1957362&radius=500&key=AIzaSy[REDACTED]"

Response:

{ "status": "OK", "results": [ { "name": "[Location]", "place_id": "[REDACTED]", "types": ["locality", "political"] } ] }

Status OK. Live data. From my own IP, with no referrer header, no authentication, nothing connecting me to the target domain. The key had absolutely no restrictions applied.

Mapping the Full Attack Surface

I tested every relevant Google Maps API against the key:

curl "https://places.googleapis.com/v1/places/[PLACE_ID]?fields=id,displayName&key=AIzaSy[REDACTED]"

curl "https://maps.googleapis.com/maps/api/directions/json?origin=CityA&destination=CityB&key=AIzaSy[REDACTED]"

curl "https://maps.googleapis.com/maps/api/distancematrix/json?origins=CityA&destinations=CityB&key=AIzaSy[REDACTED]"

curl "https://roads.googleapis.com/v1/snapToRoads?path=-33.867,151.196&key=AIzaSy[REDACTED]"

curl "https://maps.googleapis.com/maps/api/elevation/json?locations=-33.867,151.196&key=AIzaSy[REDACTED]"

curl -X POST "https://www.googleapis.com/geolocation/v1/geolocate?key=AIzaSy[REDACTED]" -H "Content-Type: application/json" -d '{}'

Results:

Places Nearby Search (Legacy) — Vulnerable, live data returned Places API (New) — Disabled on this GCP project Directions API — Disabled Distance Matrix — Disabled Roads API — Disabled Elevation API — Disabled Geolocation API — Disabled

Only one API was active. But those disabled API calls gave me something unexpected.

GCP Project ID Leaked via Error Responses

When you call a disabled API with a valid but unrestricted key, Google's error response includes the internal GCP Project ID in the response body:

{ "error": { "code": 403, "message": "API has not been used in project [GCP_PROJECT_ID] before or it is disabled…", "details": [{ "metadata": { "consumer": "projects/[GCP_PROJECT_ID]", "containerInfo": "[GCP_PROJECT_ID]" } }] } }

The internal GCP Project ID was now confirmed and enumerable by any external party making a single API call with this key.

Tracing the Exact Source in the Codebase

I pulled all JavaScript files loaded on the payment page and grepped each one for the key pattern to understand exactly where the exposure was coming from:

CHUNKS=( "chunk1-[HASH].js" "chunk2-[HASH].js" )

BASE="https://[REDACTED]/_next/static/chunks"

for chunk in "${CHUNKS[@]}"; do result=(curl−sk"(curlsk"BASE/$chunk" | grep -oP 'AIza[0–9A-Za-z_-]{35}') [ -n "$result" ] && echo "[+] Found in $chunk: $result" done

The key was not inside any JavaScript chunk file. It was only in the server-rendered HTML itself, inside the NEXT_DATA hydration JSON block that Next.js inlines directly into the page HTML before sending it to the browser. The JSON structure was:

{ "googleApiKey": "AIzaSy[REDACTED]", "googleOptimizeContainerId": "", "googleTagManagerId": "GTM-[REDACTED]" }

This config block was being passed by the application to a third-party product configurator component. The key belongs to the brand — the vendor just consumes whatever key gets passed to it. The misconfiguration is entirely on the GCP side. No restrictions were ever applied to the key.

The Real Impact

Quota exhaustion and billing abuse. Google Maps APIs have monthly free tier limits. Above those limits the account gets billed per request. An attacker can write a simple script hammering the Places API thousands of times per hour, exhausting the free tier and generating unexpected charges with no way to distinguish malicious from legitimate traffic until a billing alert fires.

Future API exposure. The key currently has only one API active. But because there are zero API restrictions on the key, any API the development team enables on this GCP project in the future is immediately abusable by anyone who already has this key — without the developers realizing the key has been public the entire time.

Infrastructure reconnaissance. The GCP Project ID is now enumerable by any external party making a single API call. Combined with the GTM container IDs visible in the same config block, an attacker builds a clearer picture of the target's cloud infrastructure than they should ever have from a public checkout page.

Why This Class of Bug Happens

In Next.js, environment variables prefixed with NEXT_PUBLIC_ are intentionally bundled into the client-side build and become visible in the browser. When developers integrate a third-party component that needs an API key, they set it as a NEXT_PUBLIC_ variable so the component can access it on the client side. The framework does exactly what it is designed to do. The security gap is in the GCP configuration — the key was shipped to production without anyone opening the Cloud Console and adding restrictions.

Responsible Disclosure

I documented the full finding with reproduction steps, API test results, confirmed GCP Project ID, and complete impact analysis. I submitted it through the brand's official VDP portal. Their platform encrypted the report with a CERT PGP key before transmission and issued a unique receipt ID as confirmation of submission.

I am not disclosing the brand name or any unredacted technical details until they confirm the fix. Standard 90-day responsible disclosure window applies.

How to Fix It

Open Google Cloud Console, go to APIs and Services, then Credentials. Find the key. Under Application Restrictions select HTTP Referrers and add your production domains. Under API Restrictions select Restrict Key and choose only the specific APIs your application actually uses.

If the key has been in production without restrictions, treat it as fully compromised. Generate a new key, apply all restrictions before deploying, then revoke the old one.

For API calls that do not need to happen in the browser, route them through a backend proxy. The key never leaves your server and the client only ever calls your own API endpoint.

Key Takeaways

Page source is always worth reading. Using FindSomething passively while browsing caught this in seconds. Grepping manually confirmed it. Thirty seconds of attention found a real finding on a global brand's production checkout page.

Finding a key is step one, not the finish line. A key in client-side code is suspicious, not automatically a vulnerability. The real work is confirming whether it is restricted.

Error responses leak more than you expect. Disabled API errors from Google include the internal GCP Project ID. That belongs in the report and adds real weight to the impact section.

Responsible disclosure is worth doing even without a bounty. This was a pure VDP with no financial reward. I submitted anyway. The finding gets fixed, it goes on the portfolio, and that compounds over time.

Once this case is closed and the fix is confirmed I will update this article with the brand name and full unredacted technical details.

I am a security engineer based in India with 12 years of IT experience, focused on bug bounty research alongside my professional work. I write about what I find and what I learn.