CVE-2025-29512: Stored XSS in the blacklist IP functionality
Overview
CVE-2025-29512 is a stored cross-site scripting (XSS) vulnerability affecting NodeBB version 4.0.4 and below. The issue resides in the IP blacklist administration panel /ip-blacklist
, where input provided to the “rules” <textarea>
is improperly sanitized. When a moderator inputs malicious HTML or JavaScript, the payload is stored and executed upon subsequent visits to the page.
This vulnerability allows moderators to execute arbitrary JavaScript in the context of other privileged users, leading to potential session hijacking, defacement, or unauthorized administrative actions. Since the injected script persists across sessions and there is no built-in mechanism to sanitize or remove the stored payload from the UI, exploitation can lead to persistent denial of access to the page or further abuse.
The issue has been addressed by the NodeBB team in a GitHub commit, where user-supplied input is now sanitized using the validator.escape() function to neutralize HTML special characters.
Steps
-
Create a normal user and promote or ensure the user is a moderator.
-
Navigate to
/ip-blacklist
. -
Enter the payload
</textarea><script>alert(“NodeBB Hacked!”)</script>
. -
Click the “Apply Blacklist” button.
-
Refresh the page and observe the XSS Proof of Concept (PoC).
-
Once you navigate back to the page, the script tags will be embedded into the page.
- To possibly remove the tags, simply click the “Apply Blacklist” button again with an arbitrary IP value.
-
Side note: You can also deny access to this page with other HTML injection attacks.
I found two ways to remove the injected data:-
Host another page I never reloaded that had the “Apply Blacklist” button. This would be used to cleanse the endpoint of any injections.
-
Manually delete the
ip-blacklist-rules
field in the MongoDB database:db.objects.deleteOne({ _key: "ip-blacklist-rules" });
-
There appears to be no built-in way to remove the script tags once embedded into the
<textarea>
, and the API does not manage IP blacklists. As a result, every time I navigated to the page, I could not make changes and encountered the following issue which removed functionality of the application.
-
Further Analysis: Code Review
Exploiting a vulnerability is one thing, but understanding the design and the fix for the issue is an entirely different skill.
After reporting the vulnerability to the security team, the developers chose to mitigate the issue by incorporating the validator
npm package. This package includes a function called validator.escape
. According to the npm documentation, the escape
function performs the following transformation:
Function | Description |
---|---|
escape(input) |
replace <, >, &, ‘, " and / with HTML entities. |
The Fix
The developers passed the user-provided rules
data through validator.escape
to sanitize it. Additionally, the data was wrapped with the String()
function to ensure it is always converted to a string data type.
src/controllers/globalmods.js
'use strict';
const validator = require('validator');
const user = require('../user');
const meta = require('../meta');
const analytics = require('../analytics');
res.render('ip-blacklist', {
title: '[[pages:ip-blacklist]]',
rules: validator.escape(String(rules)),
analytics: analyticsData,
breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:ip-blacklist]]' }]),
});
For more information, the git commit can be found here.