← Back to Feed CACHED · 2026-07-14 09:38:27 · CACHE_KEY tenable:114129
tenable:114129 · CWE-200 · Disclosed 2023-12-11

Tenable WAS 114129 — Generic Secret Disclosure (hardcoded credentials in client-side web assets)

ASSESSED — NOISGATE
Vendor
—
—
—
Reassessed
—
—
—
Verdict: —
Do you agree?
01 · The Real Story

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.

"Not a CVE — a scanner signature for hardcoded secrets in client-side code. Verdict floats with what leaked; assume the worst until you triage."
02 · The Attack Path

5 steps from start to impact.

STEP 01

Discover the target surface

Attacker enumerates your public web properties — marketing site, SPA login page, customer portal, status page, employee-facing SaaS. No authentication required. Tools like subfinder, amass, and Shodan/Censys queries surface the full domain footprint in minutes.
Conditions required:
  • Public-facing web assets discoverable via DNS/CT logs
Where this breaks in practice:
  • None — modern orgs have hundreds of public web hostnames
Detection/coverage: External attack surface management tools (Randori, RunZero, Bitsight) enumerate the same surface — use them defensively.
STEP 02

Harvest client-side artifacts

Attacker pulls HTML, all linked JavaScript bundles, and any .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.
Conditions required:
  • HTTP(S) reachability to the web asset
Where this breaks in practice:
  • Aggressive minification/obfuscation of bundles reduces regex hits
  • Bundlers that strip process.env at build time
Detection/coverage: WAF/CDN request-rate anomalies; unusual .map fetches; SecOps rarely alerts on these.
STEP 03

Extract secret and identify blast radius

Attacker finds a match — say 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.
Conditions required:
  • A parseable, high-entropy secret is present in the fetched artifacts
Where this breaks in practice:
  • Secret is dead/rotated
  • Secret is a public/publishable key by design (Stripe pk_, Firebase web config, Segment write key)
Detection/coverage: Vendor-side telemetry: AWS CloudTrail GetCallerIdentity from unfamiliar IP, GitHub audit log for unfamiliar geographies, Stripe unusual API caller.
STEP 04

Pivot on the credential

If the secret grants access to a downstream identity/CI/cloud/PKI system, attacker uses standard offensive tooling: 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.
Conditions required:
  • Secret has non-trivial scope
  • Downstream system is reachable from the internet
Where this breaks in practice:
  • Least-privilege IAM
  • Short-lived / rotated credentials
  • MFA/session binding on the underlying identity
Detection/coverage: GuardDuty UnauthorizedAccess:IAMUser/*, GitHub secret_scanning_alert, Okta impossible-travel, cloud-provider anomaly detection.
STEP 05

Achieve objective

Depending on scope: data exfil from S3/blob storage, source code theft, supply-chain poisoning via CI/CD credential, customer PII from Stripe/Twilio, or full tenant takeover. Median dwell time from a live AWS key to first data touch in reported incidents is under one hour.
Conditions required:
  • Credential authorizes the target action
Where this breaks in practice:
  • Data-egress monitoring, DLP, canary tokens in high-value buckets
Detection/coverage: Egress DLP, S3 bucket access anomaly, CloudTrail data events (if enabled — most orgs do not enable them).
03 · Compensating Control

1
Triage every hit within 24 hours and classify by secret type — Group findings into (a) publishable-by-design (Stripe 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.
2
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.
3
Enable secret scanning at every upstream stage — GitHub Advanced Security secret scanning + push protection, GitLab secret detection, 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.
4
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/*.map on the public origin. Most WAS 114129 hits come from sourcemaps, not the bundle itself.
5
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.
6
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-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.
What doesn't work
  • 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.
04 · Intelligence Metadata

The supporting signals.

Signature typeTenable WAS plugin (not a CVE) — Data Exposure family, detection-only
Class / CWECWE-200 Exposure of Sensitive Information to an Unauthorized Actor
Vendor CVSSv3.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 statusNot 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 availabilityPublic tools: trufflehog, gitleaks, SecretFinder, nuclei exposures/tokens templates
EPSSN/A — no CVE; EPSS does not score detection signatures
KEVNot applicable
Exposure telemetryGitGuardian: ~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 published2023-12-11 · last updated 2025-10-21
ReportingTenable Research — internal detection engineering, not a coordinated disclosure

Sources.

  1. Tenable Plugin 114129 — Generic Secret Disclosure
  2. Tenable WAS Data Exposure plugin family
  3. CWE-200 — Exposure of Sensitive Information
  4. GitGuardian State of Secrets Sprawl 2025
  5. TruffleHog — secret scanner
  6. Gitleaks
  7. Nuclei exposures/tokens templates
  8. GitHub Secret Scanning + Push Protection
05 · The Call

Final Verdict
= UNCHANGED to HIGH (7.5/10)

Why this verdict

  • Zero exploitation friction: unauthenticated, no CVSS PR, no user interaction, no exploit code. curl + grep is 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, Stripe sk_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_, Segment writeKey, 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.

06 · Verification

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.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/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)