The scanner didn't find a bug — it found your developers checking AWS keys into JavaScript
Tenable WAS plugin 114129 (Generic Secret Disclosure) is not a CVE and there is no NVD record. It is a *detection signature* in the Tenable Web App Scanning engine, part of the Data Exposure family, first published 2023-12-11 and refreshed 2025-10-21. The check crawls your web application and flags any HTTP response — HTML, inline <script>, sourcemapped JS bundle, or HTML comment — that contains string patterns matching API keys, OAuth client secrets, JWT signing keys, AWS/GCP/Azure access keys, private keys (-----BEGIN), Slack/Stripe/Twilio/SendGrid tokens, database DSNs, or generic high-entropy tokens. Any modern web asset with an unminified React/Vue/Angular bundle, a .env-leaked build, or a copy-pasted curl example in a comment is eligible.
Tenable rates this CVSS v3.0 8.6 (High) and v4.0 8.7 (High) using vector AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N — unauthenticated network attacker, no user interaction, scope change, high confidentiality impact. That score is *directionally correct but structurally deceptive*: the finding itself is trivial to exploit (View Source is the exploit), but the true impact is a step function of what leaked. A leaked Google Analytics measurement ID is noise; a leaked AWS root key, Okta API token, GitHub PAT with repo scope, or Stripe live secret is a fleet-scale incident. Because there is no vendor CVE baseline, this is an initial assessment — treat the plugin verdict as a *pointer to a triage decision*, not a single severity.
5 steps from start to impact.
Discover the target surface
subfinder, amass, and Shodan/Censys queries surface the full domain footprint in minutes.- Public-facing web assets discoverable via DNS/CT logs
- None — modern orgs have hundreds of public web hostnames
Harvest client-side artifacts
.map sourcemap files. Tools: trufflehog filesystem, gitleaks, secretfinder.py, nuclei templates in exposures/tokens/, SecretScanner, and the same regex packs Tenable uses. Everything is scriptable and runs offline once assets are pulled.- HTTP(S) reachability to the web asset
- Aggressive minification/obfuscation of bundles reduces regex hits
- Bundlers that strip
process.envat build time
.map fetches; SecOps rarely alerts on these.Extract secret and identify blast radius
AKIA... or sk_live_... or ghp_.... They fingerprint it: prefix identifies the vendor, and a one-shot API call (aws sts get-caller-identity, curl api.stripe.com/v1/charges, curl -H 'Authorization: token …' api.github.com/user) confirms validity and enumerates scope.- A parseable, high-entropy secret is present in the fetched artifacts
- Secret is dead/rotated
- Secret is a public/publishable key by design (Stripe
pk_, Firebase web config, Segment write key)
GetCallerIdentity from unfamiliar IP, GitHub audit log for unfamiliar geographies, Stripe unusual API caller.Pivot on the credential
pacu or enumerate-iam for AWS; gitrob/octopoacher for GitHub PAT lateral movement; roadrecon for Azure AD tokens. This is where blast radius diverges — a workflow-scoped GitHub Actions token yields code; an AWS admin role yields the tenant.- Secret has non-trivial scope
- Downstream system is reachable from the internet
- Least-privilege IAM
- Short-lived / rotated credentials
- MFA/session binding on the underlying identity
UnauthorizedAccess:IAMUser/*, GitHub secret_scanning_alert, Okta impossible-travel, cloud-provider anomaly detection.Achieve objective
- Credential authorizes the target action
- Data-egress monitoring, DLP, canary tokens in high-value buckets
pk_, Firebase web, Segment writeKey) → close as intended; (b) low-scope third-party keys → rotate at leisure; (c) high-scope (cloud, CI, identity, signing) → treat as active incident. Per noisgate mitigation SLA for HIGH: within 30 days for the class, but individual (c)-tier hits are effective-CRITICAL and get the ≤3-day / immediate track.pre-commit hooks with gitleaks, and CI-stage trufflehog on build artifacts. Deploy within 30 days per HIGH SLA — this is the only durable control that prevents recurrence./static/js/*.map on the public origin. Most WAS 114129 hits come from sourcemaps, not the bundle itself..env.example, unused files, README examples). First alert on use = immediate triage signal that a real credential is out there too.aws sts assume-role with 15-minute TTLs. Removes the value of a leaked key even if one escapes. Roll out within the 180-day HIGH remediation window.- WAF rules — the secret is IN the response body your server is intentionally sending; a WAF has no signal that says 'this string is a credential'.
- Minification / obfuscation — regex/entropy scanners defeat minification trivially; obfuscation only slows a human, not
trufflehog. - IP allowlisting the WAS scanner — you'll stop the *detection*, not the *disclosure*. Attackers scan too.
- Rotating the secret without fixing the pipeline — the next build will re-embed it. Fix the root cause (env var management, build-time secret injection) or you'll see the same finding next quarter.
- Password managers / vaults — irrelevant. The secret already left the vault the moment a developer pasted it into source.
The supporting signals.
| Signature type | Tenable WAS plugin (not a CVE) — Data Exposure family, detection-only |
|---|---|
| Class / CWE | CWE-200 Exposure of Sensitive Information to an Unauthorized Actor |
| Vendor CVSS | v3.0 8.6 High AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N · v4.0 8.7 High |
| In-the-wild status | Not a KEV-eligible finding (no CVE). Class-level exploitation is continuous and industrialized — see Truffle Security, GitGuardian State of Secrets Sprawl 2025 reporting >23M leaked secrets in public repos in 2024 alone. |
| PoC availability | Public tools: trufflehog, gitleaks, SecretFinder, nuclei exposures/tokens templates |
| EPSS | N/A — no CVE; EPSS does not score detection signatures |
| KEV | Not applicable |
| Exposure telemetry | GitGuardian: ~1 in 10 developers exposed a secret in 2024. Shodan/Censys do not classify at this granularity but sourcemap exposure is trivially internet-wide. |
| Signature first published | 2023-12-11 · last updated 2025-10-21 |
| Reporting | Tenable Research — internal detection engineering, not a coordinated disclosure |
Sources.
Why this verdict
- Zero exploitation friction: unauthenticated, no CVSS
PR, no user interaction, no exploit code.curl+grepis the full kill chain to step 3. - Role multiplier — worst plausible outcome dominates: if the leaked artifact is a cloud root key, GitHub PAT with
repo, Okta admin API token, Stripesk_live, or a code-signing / JWT signing key, the chain ends in tenant / supply-chain compromise. Per the noisgate deployment-role floor rule, that outcome is plausible for any organization running a modern web SPA with a CI/CD pipeline (≥10% of enterprise web builds have historically leaked *some* credential per GitGuardian) — floor is HIGH. - High false-positive plausibility keeps the floor from CRITICAL: many hits are publishable-by-design keys (Firebase web config, Stripe
pk_, SegmentwriteKey, Sentry DSN, Google Maps client keys). These do not warrant emergency response. - Detection-signature status, not a patchable CVE: there is no version to bump — remediation is process (rotate + hunt the root cause), which makes SLA framing about *credential rotation deadlines*, not patch cycles.
Why not higher?
Not CRITICAL because a large fraction of real fires are publishable / low-scope keys (Stripe pk_, Firebase web config, analytics IDs). Blanket-treating every hit as CRITICAL burns the incident-response team on noise and desensitizes triage; the correct response is per-finding classification with a fast escalation path to CRITICAL for high-scope hits.
Why not lower?
Not MEDIUM because when the fire *is* a live cloud/CI/identity credential, the blast radius is fleet-scale and the exploitation cost is zero. Anchoring at MEDIUM would let a real Uber-2022-class exposure sit in a 365-day queue. The class has driven repeated public breaches — MEDIUM under-prices demonstrated real-world impact.
Crowdsourced verification payload.
Run this from an auditor workstation (or CI stage) with network reach to the target site. It fetches the HTML + linked JS bundles + sourcemaps and greps for common secret patterns — the same class of check Tenable WAS 114129 performs. Requires Python 3.9+ and requests. Invoke: python check_secret_disclosure.py https://app.example.com. No privileges required on the target.
#!/usr/bin/env python3
# noisgate — verify Tenable WAS 114129 (Generic Secret Disclosure) exposure
# Usage: python check_secret_disclosure.py <base_url>
# Exit: 0 PATCHED, 1 VULNERABLE, 2 UNKNOWN
import re, sys, urllib.parse as up
try:
import requests
except ImportError:
print('UNKNOWN: install requests (pip install requests)'); sys.exit(2)
if len(sys.argv) != 2:
print('UNKNOWN: usage: check_secret_disclosure.py <base_url>'); sys.exit(2)
BASE = sys.argv[1].rstrip('/')
HEADERS = {'User-Agent': 'noisgate-secret-check/1.0'}
TIMEOUT = 15
# High-signal secret patterns. Extend as needed.
PATTERNS = {
'AWS access key': re.compile(r'AKIA[0-9A-Z]{16}'),
'AWS secret key': re.compile(r'(?i)aws(.{0,20})?[\'"][0-9a-zA-Z/+]{40}[\'"]'),
'GitHub PAT (classic)': re.compile(r'ghp_[0-9A-Za-z]{36}'),
'GitHub fine-grained': re.compile(r'github_pat_[0-9A-Za-z_]{82}'),
'GitHub OAuth': re.compile(r'gho_[0-9A-Za-z]{36}'),
'Slack bot/user token': re.compile(r'xox[baprs]-[0-9A-Za-z-]{10,}'),
'Stripe live secret': re.compile(r'sk_live_[0-9a-zA-Z]{24,}'),
'Google API key': re.compile(r'AIza[0-9A-Za-z_\-]{35}'),
'Twilio SID': re.compile(r'AC[0-9a-f]{32}'),
'SendGrid API key': re.compile(r'SG\.[0-9A-Za-z_\-]{22}\.[0-9A-Za-z_\-]{43}'),
'PEM private key': re.compile(r'-----BEGIN (RSA|EC|OPENSSH|DSA|PGP) PRIVATE KEY-----'),
'JWT': re.compile(r'eyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}'),
'Generic bearer secret': re.compile(r'(?i)(secret|token|api[_-]?key|passwd|password)[\'"\s:=]{1,5}[\'"][A-Za-z0-9_\-]{24,}[\'"]'),
}
def fetch(url):
try:
r = requests.get(url, headers=HEADERS, timeout=TIMEOUT, allow_redirects=True)
return r.text if r.status_code == 200 else ''
except Exception:
return ''
def scan(text, source):
hits = []
for name, pat in PATTERNS.items():
for m in pat.findall(text)[:3]:
snippet = m if isinstance(m, str) else str(m)
hits.append((source, name, snippet[:80]))
return hits
html = fetch(BASE)
if not html:
print(f'UNKNOWN: could not fetch {BASE}'); sys.exit(2)
findings = scan(html, BASE)
# Extract linked JS + sourcemaps
js_urls = set(re.findall(r'<script[^>]+src=["\']([^"\']+\.js[^"\']*)', html, re.I))
for js in list(js_urls)[:25]: # cap to keep runtime bounded
full = up.urljoin(BASE + '/', js)
body = fetch(full)
findings += scan(body, full)
m = re.search(r'//[#@]\s*sourceMappingURL=(\S+)', body)
if m:
smap = up.urljoin(full, m.group(1))
findings += scan(fetch(smap), smap)
if findings:
print('VULNERABLE: potential secret disclosure detected')
for src, name, sample in findings[:20]:
print(f' [{name}] {src} :: {sample}')
sys.exit(1)
print('PATCHED: no matching secret patterns in fetched surface')
sys.exit(0)