Secrets That Survive Everything
The Runtime Security Gap Left UnguardedPress enter or click to view image in full sizeYears of shift 2026-5-19 09:1:24 Author: infosecwriteups.com(查看原文) 阅读量:23 收藏

Hemanth Gorijala

The Runtime Security Gap Left Unguarded

Press enter or click to view image in full size

Years of shift-left investment, and a hardcoded key still survives to production.

By Hemanth Gorijala

The Finding That Changed How I Think About Secrets

During a security assessment, I found credentials sitting in a client-side JavaScript bundle, visible to every visitor who opened DevTools.

Real Azure credentials, and I started asking: what could be done with them?

That question led to a full account takeover chain built from four values sitting in a JavaScript file the application was already serving to every visitor. The affected organization had static secret scanning on pull requests and repository scanning across their codebase. None of it had flagged the credentials, because none of it scans what an application serves. Only what developers commit.

The same pattern appeared in a second application later in the same engagement, with a different codebase and a different team, and it ended in the same outcome.

Responsible Disclosure

Both findings described in this post were identified and further investigated during authorized security assessments. Findings were reported immediately to the affected organization, remediated, and verified through retest before this publication. No user data was accessed beyond what was necessary to confirm exploitability.

The screenshots in this post come from InsecureShield, a controlled demo application built for security demonstrations. All credentials shown are synthetic and have been auth-tested as non-functional against their real providers. The patterns illustrated are reproduced from real engagements. The data shown is not.

From Finding to Full Access on Azure AD + APIM

Azure API Management (APIM) is a gateway service that sits in front of backend APIs and enforces access through two separate credential types: Azure AD bearer tokens (JWTs issued by the identity provider) and APIM subscription keys (gateway-specific pass keys scoped to API products). Both are required to reach protected endpoints. A typical Azure-native single-page application authenticates a user via Azure AD, receives a token, and then calls the backend API through the APIM gateway sending both the bearer token and the subscription key on every request.

Press enter or click to view image in full size

Azure AD + APIM Request Flow

Azure AD + APIM request flow. Two-credential gateway: Bearer token plus subscription key on every request.

The exposed credentials were not just an API key. Sitting in the client-side JavaScript bundle of a production application, Azure AD authenticated, protected by APIM, serving real users, were four values.

Press enter or click to view image in full size

The Azure credential set. Two of the four should never be in the browser.

The AppID is legitimately public in OAuth2 public client flows. The AppKey is not. It is the client secret for a confidential client flow, designed for server-to-server authentication where the application code is not visible to end users. Client secrets are architecturally valid for server-side web applications. The problem is not that the secret exists. It is that it appeared in browser-accessible JavaScript, where it is visible to every visitor who opens developer tools.

Press enter or click to view image in full size

View-source: main.js secrets

Build-pipeline-generated /js/main.js on a controlled demo target. APIM subscription key, internal service endpoints, and a service-account key all visible to any visitor.

Together, these four values are everything needed to authenticate as the application itself. The tenant ID, required to call the token endpoint, was visible in the application’s login redirect URL, hardcoded alongside the other values in the same bundle. The tenant ID is a public identifier and is not counted among the four credential values, since its availability to any visitor is assumed.

I called the Azure AD token endpoint:

POST https://login.microsoftonline.com/{tenant_id}/oauth2/token

grant_type=client_credentials
client_id={AppID}
client_secret={AppKey}
resource={Resource}

It returned a fully authenticated Bearer access token. (The v1 endpoint format is shown. The v2 endpoint uses /oauth2/v2.0/token with a scope parameter in place of resource. Both remain in active use across enterprise Azure environments.)

I then read the JavaScript bundle carefully. Not just scanning for secrets, but reading it as a map of how the application was built. Buried in the minified code were API endpoint definitions. GET and POST endpoints with their complete schema structures showing exactly which parameters each endpoint expected. The frontend had documented its own backend.

Using the Bearer token and APIM subscription key together:

Authorization: Bearer {token}
Ocp-Apim-Subscription-Key: {SubscriptionKey}

I reconstructed and called the endpoints from the schemas. User profile data returned immediately. Then I found an account management endpoint, same token, same subscription key, same reconstructed schema. I reported the finding at this point and did not proceed further.

One APIM subscription key typically maps to a product containing multiple APIs.

SubscriptionKey  ->  APIM Product "Internal APIs"
|-- /users/*
|-- /payments/*
|-- /admin/*
`-- /reports/*

One exposed subscription key grants access to every API in that product. The scope of exposure is determined by how the APIM product is configured, a detail that is not visible from the client side and must be assessed in the APIM portal.

The exposed credentials alone constitute a significant finding regardless of what they unlock. A client secret in a public JavaScript file means any visitor can authenticate as the application. The blast radius depends entirely on what permissions that application has been granted.

In this case, the amplifying factor was the scope condition. Azure AD grants come in two flavors: application permissions (the token acts as the application itself, an autonomous identity with no signed-in user) and delegated permissions (the token acts on behalf of a specific signed-in user, scoped to what that user can do). The application here had been granted application permissions where delegated permissions would have been appropriate and sufficient. The application’s client identity had been given the ability to perform user-level operations across every account in the system. Without that misconfiguration, the Bearer token would have had limited reach. With it, the token was effectively an admin key to every user account in the system.

Full account takeover. From four values in a JavaScript file visible to every visitor of the application.

This was not a sophisticated attack. No exploit framework was needed. The chain came from reading the JavaScript file the application was already serving to everyone, and understanding what the credentials unlocked.

The CryptoJS Locked Box With the Key Taped On

A different application from the same engagement, a different codebase, and the same Azure AD credential pattern waiting at the end.

The frontend JavaScript contained a CryptoJS-encrypted configuration blob, a common pattern in single-page applications where configuration is bundled into the frontend and obfuscated using a client-side encryption library. The intent is that the configuration is protected because it is encrypted.

Client-side encryption can never protect a secret from the client. If the browser needs to decrypt the configuration, the decryption key has to be present in the browser. Anyone who opens DevTools has the same access.

The decryption key was hardcoded in the same JavaScript file, three lines away from the encrypted blob.

Press enter or click to view image in full size

View-source: main.js (CryptoJS blob and key)

The ciphertext, the key, and the call that joins them, all in the same file a few lines apart. Open the browser console, paste the three pieces, and the configuration object falls out in plaintext in under a minute.

The decrypt itself is a single line, run in the browser’s developer console using the same CryptoJS library the application ships.

CryptoJS.AES.decrypt(encryptedConfig, hardcodedKey).toString(CryptoJS.enc.Utf8)

What came out was a complete configuration object. The Azure AD client credentials for that application’s service principal (AppID, AppKey, Resource), the APIM subscription key, internal service endpoints the app talks to, third-party service keys, and the connection strings the build pipeline had wrapped. Everything the application needed at runtime, returned in plaintext to anyone who opened devtools. The encryption had provided exactly zero protection. The secret to unlock everything was sitting next to the lock.

The decrypted object contained the same pattern. AppID, AppKey, Resource, and Azure AD client credentials embedded in what developers believed was a secured configuration. As with the first finding, the service principal had been granted over-permissive scopes, allowing an application-level token to perform user-level data access operations.

I called the same Azure AD token endpoint with the decrypted credentials. A Bearer token came back. I read the bundle for endpoint definitions, the same pattern as before, schemas embedded in the minified code. Authenticated GET calls returned full user profile data such as names, email addresses, and account details, for any user in the application. I reported the finding and stopped.

The CryptoJS-encrypted configuration pattern is particularly dangerous because it creates a false sense of security. Developers implement it believing the configuration is protected. Security teams see encryption and move on. The decryption key sitting next to the ciphertext makes the entire construct meaningless, but only if you read the whole file rather than scan it for plaintext secrets.

The Pattern Repeats

After the second chain, I started wondering how many other applications in the same engagement had the same problem. The answer wasn’t reassuring.

The same construct recurred across multiple unrelated codebases: build-time injection of Azure AD client_credentials, CryptoJS-encrypted configuration with the decryption key three lines from the ciphertext, and plaintext credentials sitting directly in client-side JavaScript. Different teams, different stacks within the same organizational portfolio, all converging on the same outcome.

The credentials were recoverable from successful page responses and from a small number of error responses, including geo-restricted and authentication-required pages. The single-page application bundle loads its full configuration regardless of session state. Even when the application refuses to authenticate you, the JavaScript shell still loads with the credentials baked in.

Every one of these applications was deployed by an organization running automated secret scanning in CI/CD and SAST on every pull request. The credentials were still live in production.

These were not two isolated mistakes. They reflect how single-page applications routinely reach production at enterprise scale.

The Path Shift-Left Never Sees

After the second chain, the question I kept coming back to was how the credentials got there in the first place.

In each case, this was not negligence. These were mature, well-built applications with authentication layers, gateway controls, structured API design. Organizations that build this way have shift-left tooling. The secrets got through anyway.

The shift-left tools had done their job. The secrets found a path they weren’t watching, introduced through a build process, an environment injection, a third-party dependency update, or a deployment step that happened outside the scanned path. One common example involves REACT_APP_* environment variables bundled at build time via webpack’s DefinePlugin, live keys baked into the JavaScript bundle, never touching the repository, invisible to every pre-commit scanner. Another is CI/CD pipeline variable substitution. A placeholder like #{APIM_SUB_KEY}# lives in the repository while the real value is injected by the deployment pipeline at build time. The secret never exists in git. It materializes only in the build artifact, after every scanner has already run.

Source code (.ts, config files). ← Shift-left scans HERE

↓ [pipeline variable substitution / build]

dist/main.abc123.js (minified) ← Secret exists HERE - never scanned

↓ [deploy to CDN]

Live app at https://app.example.com ← Secret is live HERE - DAST ignores it

↓ [browser fetches config.json]

Runtime-fetched config ← Secret fetched HERE - invisible to everything

Shift-left tools scan what you commit. They do not scan what you serve.

Press enter or click to view image in full size

View-source: env.js runtime config

A second path compounds the structural one. Many SPA architectures require certain credentials in the browser to function: a Google Maps API key to render maps, a Firebase configuration to authenticate users, a Stripe publishable key to initialize the payment SDK. These keys cannot be removed without breaking the application. When shift-left scanners encounter them in source files or CI/CD configuration, the build fails and the pull request is blocked. Developers facing this friction do not remove the key. They suppress the scanner in one of these ways:

  • Adding files or directories to the scanner’s ignore list or allowlist
  • Excluding configuration directories or environment files from the scan scope
  • Dismissing alerts as “false positive” or “used in tests”
  • Adding inline suppression comments to affected lines
  • Allowlisting the specific secret pattern in the SAST ruleset

The suppression is deliberate and made under pipeline pressure. Once in place, the same credential reaches every subsequent deployment without detection because the scanner has been explicitly configured to stop observing it.

The Shift-Right Gap

The industry has spent years building the left side of this picture. The right side is almost empty.

Get Hemanth Gorijala’s stories in your inbox

Join Medium for free to get updates from this writer.

Remember me for faster sign in

Shift-left, pre-production

  • Secret scanning is mature, well-funded, and widely deployed. GitLeaks, TruffleHog, detect-secrets, and GitHub Advanced Security cover this layer.
  • SAST is well covered by Semgrep, SonarQube, and Checkmarx.
  • SCA is well covered by Snyk and Dependabot.

Shift-right, production runtime

  • DAST tools are built as web application scanners, designed to find injection vulnerabilities, authentication flaws, and misconfigurations, not hardcoded credentials in served responses.
  • Runtime secret scanning has limited purpose-built tooling.

This is not a criticism of DAST tools. They solve a different problem. Azure AD credentials hardcoded in a minified webpack bundle do not look like an injection vulnerability. They do not trigger an XSS finding. They do not appear in a DAST report. They are invisible to the entire shift-right tooling category because that category was never designed to find them.

Once a secret evades shift-left controls and reaches production, it disappears from every scanner’s view.

Today, only three sources find these credentials at all.

  • A manual penetration tester who reads JavaScript carefully
  • A security researcher or bug bounty hunter
  • An attacker

Two of those three report what they find. One does not.

The Attack Surface Existing Tools Miss

To understand why DAST stays silent, it helps to see exactly what it is not looking at. The available tooling doesn’t cover the actual attack surface of a modern web application:

  • Single-page applications built with React, Vue, or Angular where the entire application is a minified webpack bundle split across dozens of chunk files
  • Encrypted configuration objects where a client-side encryption library decrypts a configuration blob using a key hardcoded in the same file — the decryption key sitting next to the ciphertext makes the encryption meaningless, and the complete credential set is one step away from exposure
  • HTML view-source scanning for SSR state blobs — the __NEXT_DATA__ or window.__INITIAL_STATE__ objects injected into HTML by Next.js and Nuxt that regularly contain tokens and internal configuration
  • JSON API responses that return credentials in fields never meant to be client-facing
  • XML responses from enterprise services carrying connection strings and service credentials
  • Request headers on every outbound API call carrying subscription keys and bearer tokens in plaintext

Press enter or click to view image in full size

View-source: firebase.js service account

And critically, none of them have noise suppression adequate for real-world use. A tool that generates hundreds of false positives per scan is a tool that gets disabled. Noise suppression decides whether findings get acted on or ignored.

Scanning build artifacts in CI/CD before deployment closes part of this gap. It catches secrets that pipeline variable substitution baked in before they reach production. But it does not cover runtime-fetched configurations that applications load after deployment, lazy-loaded chunks served dynamically from CDN, or credentials embedded in third-party scripts that never touch your build pipeline. Artifact scanning is a useful layer. It is not the runtime layer.

The Bigger Picture

The shift-left investment the industry has made is real, justified, and valuable. Catching secrets before deployment is always better than catching them after.

But secrets are still reaching production. They evade controls not because the tools are bad, but because the path from development to deployment has more branches than any single scanning layer can cover. And once a secret reaches the runtime layer, today’s tooling goes silent.

Millions of dollars invested in shift-left scanning. No purpose-built, production-grade tooling on the shift-right side.

Two independent studies confirm this at internet scale.

In December 2025, Intruder published research scanning approximately 5 million applications and identifying around 42,000 exposed tokens. That work establishes population prevalence at internet scale and confirms that standard DAST and infrastructure scanners do not cover this surface.

In early 2026, Demir et al. published “Keys on Doormats” on arXiv, an academic measurement study that crawled 10 million pages across the public web and extracted 1,748 distinct credentials. 84 percent of the recovered credentials were embedded in JavaScript. The median credential persisted in the wild for 12 months. The study covered 14 cloud and SaaS providers. Different methodology, same conclusion. The runtime layer is unscanned at internet scale, and credentials persist long enough to be found and abused.

The chains in this post show what those credentials unlock once an attacker reads them. These patterns appear in production applications serving real users today. Both targets had secret scanning enabled. Neither had runtime coverage.

Attackers already know this. Most security teams don’t have an answer for it yet.

What You Can Check Today

For security teams, the immediate check is simple. Open DevTools on your own production application, go to the Network tab, and read what is being served. Look at the JavaScript bundle source, the API response payloads, and the request headers. Look specifically for patterns like client_id, client_secret, AppKey, SubscriptionKey, apiKey, or any high-entropy string in a configuration object. No tooling required. What you find in ten minutes is what an attacker finds in ten minutes.

If your organization runs Angular or React SPAs that talk to Azure AD plus APIM, the ten-minute DevTools check is the highest-value security review you can do this week.

Why Existing Tools Don’t Close the Gap

The runtime-scanning category is not empty. A skeptical reader at this point should be asking what existing tools already cover this surface and what they don’t.

Repo and file scanners like GitLeaks, detect-secrets, and GitHub Advanced Security are mature and well-deployed. They scan what is in source control. They do not scan what the application serves at runtime.

TruffleHog sits in its own category. It scans repos and filesystems, accepts URL lists through its HTTP source, and verifies whether discovered credentials are live, which most scanners don’t do. Pointed at downloaded JavaScript bundles or URL lists, it works well within those inputs. What it does not do is treat the live application as a black box, capturing whatever the application serves as it runs. The dynamically loaded chunks, JSON and XML response bodies, and request headers that appear during real execution stay outside the URL-list input.

Active crawlers like Cariddi, SecretFinder, LinkFinder, and jsleak fetch JavaScript files and run regex matches over the content. They are useful for the public surface of an application. They are static fetchers. They do not observe the application as it actually runs, and they do not capture API response bodies, request headers, or lazy-loaded chunks that materialize only during real execution.

Template scanners like Nuclei with community secret-leak templates hit specific URLs and regex-match responses. They depend on knowing the URLs in advance and on someone maintaining the templates. They are not passive observers of whatever the application serves.

DAST scanners like OWASP ZAP, Burp Scanner, and commercial equivalents look for injection vulnerabilities, authentication flaws, and misconfigurations. They were not built to find hardcoded credentials in served responses, and they don’t.

Each category solves a piece of the problem. What none of them do is the combination that runtime credential exposure actually requires. Four properties have to be present together.

  • Black-box operation. No source code, no API documentation, no prior knowledge of the application. The tool sees what an external attacker sees.
  • Observe runtime traffic. JS bundles, JSON and XML response bodies, request headers, HTML SSR state blobs, and dynamically loaded chunks. Whatever the application actually serves as it runs.
  • Detect encrypted-blob constructs. CryptoJS-style decryption keys hardcoded next to the ciphertext. A plaintext-only scanner walks right past these.
  • Aggressive noise suppression. Runtime traffic is full of UUIDs, cache busters, and high-entropy strings that aren’t credentials. A tool that generates hundreds of false positives per scan is a tool that gets disabled within a week.

Without all four together, the tool either misses what the application actually serves, or generates so much noise it gets turned off.

Four Layers of Defense

The disclosures and the research that followed point to the same hole in the stack. Secrets that reach production are not being scanned at the runtime layer. Closing this gap is not a single fix. It is a four-layer defense, two layers of prevention followed by two layers of detection.

Prevention:

[1] Backend for Frontend (BFF). The structural fix. The credential never enters the browser. The frontend sends only a session cookie to a BFF server, the BFF fetches the credential from a vault at runtime, and the BFF calls upstream APIs on behalf of the user. If a credential never leaves the server, it cannot leak from the browser.

Press enter or click to view image in full size

Current architecture vs. BFF pattern

BFF is the right destination, but it is not a same-day change. The frontend uses those credentials today to talk to backends. Removing them outright breaks the application. A real migration to BFF means standing up a server-side proxy, re-routing every API call from the SPA through it, replacing bearer-token authentication in the browser with session cookies, and reworking CORS, CSRF, and any WebSocket or server-sent-event paths the application currently uses. For a non-trivial SPA, that is a multi-quarter project that cuts across frontend, backend, identity, and infrastructure teams. During that migration, the credential remains in the browser. Detection layers below carry the load until the structural fix lands.

[2] Secrets manager and short-lived tokens. Azure Key Vault, AWS Secrets Manager, HashiCorp Vault. The BFF reads the credential at runtime, mints short-lived scope-limited tokens for downstream calls, and never embeds long-lived secrets anywhere a client can reach. A short-lived token leaked from a browser still leaks, but the blast radius is bounded by the token’s expiry and scope rather than by the lifespan of a hardcoded client secret.

Detection:

[3] Artifact scanning between build and deploy. A CI/CD job that scans dist/ output after the bundler runs but before the artifact ships. Catches credentials that build-time injection inlines into the bundle and never touches the repository. Most pipelines have stage-1 source scanning but stop short of stage-2 artifact scanning. Adding it is a CI/CD job, not a project.

Press enter or click to view image in full size

CI/CD pipeline with artifact-scan stage

[4] Runtime secret scanning. Passive observation of served traffic. The only layer that catches lazy-loaded chunks, SSR state, runtime-fetched config, and credentials in response headers. These materialize only during execution and cannot be predicted from static source files.

Layers 1 and 2 remove the credential from the surface. Layers 3 and 4 catch what slips through. Start all four at once rather than waiting for prevention to land first: Layers 3 and 4 can ship in days, Layer 2 in weeks, and Layer 1 in quarters. The detection layers are what protect you while the prevention layers are being built. Most organizations today run Layer 1 partially, skip Layers 2 and 3 entirely, and have no answer for Layer 4.

Closing the Runtime Gap

I’m continuing the work on the layer-4 side with SecretSifter, an open-source passive runtime secret scanner that reads HTTP traffic for the credentials shift-left tools cannot see. It ships as a Burp Suite extension, a browser extension, and a desktop application.

Press enter or click to view image in full size

SecretSifter findings panel

Try the Walk-Through Yourself

The screenshots and chains in this post are reproducible end-to-end against InsecureShield, an intentionally vulnerable insurance portal published at github.com/secretsifter/insecureshield-demo. Every credential, endpoint, and response in the screenshots above comes from that public demo. The credentials are synthetic and have been auth-tested against their real providers as non-functional.

A step-by-step walk-through, with both chains worked from view-source through token mint to data exfiltration, lives in the repository at docs/reproduction-guide.html.

git clone https://github.com/secretsifter/insecureshield-demo
cd insecureshield-demo
npm install
npm start

About the Author

Hemanth Gorijala is an application security practitioner and penetration tester. He conducts web application security assessments, reviews vulnerability reports in enterprise bug bounty programs, and maintains SecretSifter, a free, open-source runtime secret detection tool.


文章来源: https://infosecwriteups.com/secrets-that-survive-everything-28b0c6aa1aa4?source=rss----7b722bfd1b8d--bug_bounty
如有侵权请联系:admin#unsafe.sh