TL;DR: A paid API key sat in the page, and it worked. I extracted the geocoding key from a review widget, verified it was live from an attacker origin, and fixed it with referrer restriction plus a server-side proxy.
The scenario
I ran this as a black-box review: no credentials, only the publicly visible surface. Here is what I saw before I touched anything.
A law firm site on a supported content platform, protected behind basic authentication, with a business review widget on the practice pages. Assessment covered exposed credentials in client HTML, header policy, mail authentication, user enumeration and the login perimeter.
I removed all identifying information. The target is described only by sector and technology class so the pattern can be reused.
The pattern
The pattern I recognised — and that I keep finding in similar estates:
A key in client-side code is not automatically a vulnerability, because the browser needs some identifier to call a third-party service. The distinction is between a key that only scopes the request and a key that can be billed. A geocoding key that resolves addresses is billable, restricted by an origin list at best, and when the origin list is missing it is an open invitation to consume somebody else's quota.
Review widgets are the usual source. The pattern is a plugin that embeds a configured key in the page to render a small map or a rating component. The key ships in the HTML because the request has to originate from the visitor's browser. Whether that matters depends entirely on how the key was created at the provider: unrestricted, it can be used from anywhere for anything the account allows.
The second lesson from this engagement is about layering. The site sat behind basic authentication, which is genuinely uncommon and genuinely effective against scanning. It also had user enumeration available through two separate paths, and headers absent, and no mail enforcement. Good and bad decisions coexisted because they were made by different people at different times, and none of them had an owner. Perimeter strength is not a property of a site; it is a property of the most recent decision made about it.
Findings
| Severity | Finding | Evidence |
|---|---|---|
| HOCH | Billable third-party API key rendered into public HTML | Key readable by any visitor and usable outside the site |
| MEDIUM | No security headers on the public surface | HSTS, frame protection and content-type protection absent |
| MEDIUM | No enforcement policy for the sending domain | Authentication missing for the mail domain |
| MEDIUM | User enumeration through two independent paths | Author identifier and slug both derivable |
| LOW | Long-unmaintained plugin still installed | Years without a release, still active |
| INFO | Login and admin behind basic authentication | Effective against automated scanning |
The attack path
Reproduction in my lab
Every command below targets a lab container I control. Nothing here is aimed at a live system.
Look for credentials in the HTML of your own site
$ curl -s https://example.org/practice/reviews/ > /tmp/page.html
# key-shaped strings, common provider prefixes
$ grep -oE 'AIza[0-9A-Za-z_-]{35}' /tmp/page.html
$ grep -oiE '(key|api[_-]?key|token|app[_-]?id|client[_-]?id)="?[0-9A-Za-z_-]{20,}' /tmp/page.html | sort -u
$ grep -oE 'pk_(live|test)_[0-9A-Za-z]{20,}' /tmp/page.html
# and the widget configuration itself
$ grep -oiE '(google|maps|places|geocode)[a-z0-9_.-]*["\x27:=\s]+[0-9A-Za-z_-]{20,}' /tmp/page.html | sort -u
Detection in your own estate
Verify whether a key is actually usable and unrestricted
# a geocoding key with no origin restriction answers from anywhere.
# test with your OWN key against YOUR OWN project:
$ curl -s -o /dev/null -w '%{http_code}\n' \
"https://maps.googleapis.com/maps/api/geocode/json?address=Berlin&key=YOUR_OWN_KEY"
# the same request from a disallowed origin header is what the provider sees
$ curl -s -o /dev/null -w '%{http_code}\n' \
-H 'Referer: https://unrelated.example/' \
"https://maps.googleapis.com/maps/api/geocode/json?address=Berlin&key=YOUR_OWN_KEY"
# check billing and quota alerting exist before an incident does
Remediation
Restrict the key at the provider, not in the code
# in the provider console for the key, not in the repository:
# - application restrictions: HTTP referrers
# https://example.org/*
# https://www.example.org/*
# - API restrictions: enable only the geocoding API
# - quota: set a daily request cap
# - billing: alert on spend, do not only alert on failure
# if the browser must not hold a billable key, proxy through your own origin
# and cache the result server-side
GET /api/geocode?q=Berlin
-> server calls the provider with a server-held key
-> response cached, browser never sees the key
Ship the header baseline in the platform configuration
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
ServerTokens Prod
ServerSignature Off
Once an attacker gains access, they can perform the following actions:
Takeaways
- A key in client HTML is fine until it is billable. Check the provider settings, not the code.
- Restrict by referrer and by API; a geocoding key needs neither geolocation nor write access.
- Set a spend alert. The first signal of abuse should not be an invoice.
- Good and bad decisions coexist on one site. Audit the most recent decision, not the average one.