~/blog/api-key-exposed-in-client-htmlPOSTED
by Shady Nathan Tawfik · September 26, 2026 · 5 min

A Paid API Key Sat In The Page, And It Worked

A law firm site with a review widget that rendered a billable geocoding key into the HTML. The finding is narrow, trivially automatable, and the most profitable line item in the report.

Interactive[blog-2.0]

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.

TL;DR: A review widget rendered a billable geocoding key into the public HTML. I verified the key was live, showed how any visitor can burn the quota, and fixed it with referrer restriction plus a server-side proxy.

Findings

SeverityFindingEvidence
HOCHBillable third-party API key rendered into public HTMLKey readable by any visitor and usable outside the site
MEDIUMNo security headers on the public surfaceHSTS, frame protection and content-type protection absent
MEDIUMNo enforcement policy for the sending domainAuthentication missing for the mail domain
MEDIUMUser enumeration through two independent pathsAuthor identifier and slug both derivable
LOWLong-unmaintained plugin still installedYears without a release, still active
INFOLogin and admin behind basic authenticationEffective against automated scanning

The attack path

Review widget configuredBillable key embedded in pageAny visitor can read the keyKey usable from an attacker originQuota and cost consumedKey should be restricted or proxied

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

~/code/bash bash
$ 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

~/code/bash bash
# 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

~/code/text text
# 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

~/code/apache apache
<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:

Quota verbrennen: Kosten für die Kanzlei
Widget-Funktion lahmlegen (Karten, Bewertungen)
Bei Places-Scopes: Kundendaten abfragen

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.

For educational purposes only. Every reproducible command targets my own lab environment (localhost), never a live system. © 2026