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
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 |
noisgate verdict.
Assessed HIGH because the finding is trivially reachable by any unauthenticated internet user (View Source is the exploit) and the affected component class routinely embeds credentials with fleet- or tenant-scale blast radius. The single most decisive factor is the role multiplier: any single hit that turns out to be an AWS/GitHub/Okta/CI credential collapses the whole chain to CRITICAL, and Tenable will not tell you which kind you have — you must triage each match.
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.
What to do — in priority order.
- Triage every hit within 24 hours and classify by secret type — Group findings into (a) publishable-by-design (Stripe
pk_, Firebase web, SegmentwriteKey) → 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. - Rotate the exposed secret before removing it from source — Order matters. If you delete the leaked value first and then rotate, attackers who already scraped it retain access. Issue a new credential, deploy the change, then invalidate the old value at the issuer (AWS IAM, GitHub, Okta, Stripe). Complete within the mitigation window.
- Enable secret scanning at every upstream stage — GitHub Advanced Security secret scanning + push protection, GitLab secret detection,
pre-commithooks withgitleaks, and CI-stagetrufflehogon build artifacts. Deploy within 30 days per HIGH SLA — this is the only durable control that prevents recurrence. - Kill sourcemap exposure in production — Configure webpack/Vite/Rollup to emit sourcemaps only for internal error-tracking upload (Sentry, Datadog) and NOT to
/static/js/*.mapon the public origin. Most WAS 114129 hits come from sourcemaps, not the bundle itself. - Deploy canary tokens in high-value repos and cloud accounts — Plant Thinkst Canarytokens styled AWS keys, GitHub PATs, and Slack tokens in likely leak paths (
.env.example, unused files, README examples). First alert on use = immediate triage signal that a real credential is out there too. - Move to short-lived credentials wherever possible — OIDC-federated GitHub Actions to cloud (no static keys), IMDSv2-only EC2 roles, workload identity for K8s,
aws sts assume-rolewith 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.
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)
If you remember one thing.
Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.