๐จ JavaScript Vulnerability Examples and Fixes
Use this page when: reviewing Node/Express backends, admin dashboards, browser-heavy workflows, and old JavaScript where โjust one helperโ quietly became part of the trust boundary.
How to read these examples
- Vulnerable snippet shows the unsafe habit.
- Safer pattern shows the direction you want in production code.
- Why it matters ties the defect to attacker value and business impact.
- Review cue is phrased so it can become a pull-request comment or checklist item.
Example 1 โ DOM XSS via innerHTML
Vulnerable snippet
const params = new URLSearchParams(location.search);
document.getElementById('welcome').innerHTML = params.get('name');
Safer pattern
const params = new URLSearchParams(location.search);
document.getElementById('welcome').textContent = params.get('name') || '';
Why it matters
innerHTMLtreats attacker-controlled content as markup and script-capable DOM input.
Business impact
- Session theft, account takeover, malicious actions in the victim session, and browser-side trust collapse.
Review cue
- Prefer
textContentfor plain text. Use trusted sanitization only when rich HTML is actually required.
Example 2 โ NoSQL injection in login flow
Vulnerable snippet
app.post('/login', async (req, res) => {
const user = await db.collection('users').findOne({
email: req.body.email,
password: req.body.password
});
if (user) res.sendStatus(200);
});
Safer pattern
app.post('/login', async (req, res) => {
const email = String(req.body.email || '');
const password = String(req.body.password || '');
const user = await db.collection('users').findOne({ email });
if (!user) return res.sendStatus(401);
const ok = await bcrypt.compare(password, user.passwordHash);
if (!ok) return res.sendStatus(401);
res.sendStatus(200);
});
Why it matters
- When request bodies are used as query objects or compared directly, attacker-crafted structures can alter query meaning.
Business impact
- Authentication bypass, user enumeration, and exposure of account existence or account takeover paths.
Review cue
- Cast request fields to expected scalar types and separate identity lookup from password verification.
Example 3 โ Path traversal in file download endpoint
Vulnerable snippet
app.get('/download', (req, res) => {
const filePath = '/srv/public/' + req.query.name;
res.sendFile(filePath);
});
Safer pattern
const path = require('node:path');
app.get('/download', (req, res) => {
const base = '/srv/public';
const target = path.resolve(base, String(req.query.name || ''));
if (!target.startsWith(path.resolve(base) + path.sep)) {
return res.status(400).send('invalid path');
}
res.sendFile(target);
});
Why it matters
- Concatenating file paths allows
../traversal or unexpected absolute paths to escape the intended directory.
Business impact
- Disclosure of secrets, source code, configs, keys, or operational documents on the host.
Review cue
- Normalize and resolve paths against a fixed base, then verify the resolved prefix before serving a file.
Example 4 โ JWT misuse by decoding without verification
Vulnerable snippet
app.get('/admin', (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '');
const claims = jwt.decode(token);
if (claims?.role === 'admin') return res.send('ok');
res.sendStatus(403);
});
Safer pattern
app.get('/admin', (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '');
try {
const claims = jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'] });
if (claims.role !== 'admin') return res.sendStatus(403);
return res.send('ok');
} catch {
return res.sendStatus(401);
}
});
Why it matters
decode()reads claims but does not prove signature, issuer, audience, or token validity.
Business impact
- Privilege escalation, admin impersonation, and broken trust in every downstream authorization decision.
Review cue
- Only verified claims may influence authorization. Require explicit algorithm and key expectations.
Example 5 โ Prototype pollution through unsafe deep merge
Vulnerable snippet
const settings = merge(defaultSettings, req.body);
// later logic trusts merged object
Safer pattern
const allowed = (({ theme, locale, pageSize }) => ({ theme, locale, pageSize }))(req.body || {});
const settings = { ...defaultSettings, ...allowed };
Why it matters
- Deep-merging untrusted objects can modify inherited properties or unexpected control flags used later in the application.
Business impact
- Authorization confusion, logic corruption, denial of service, and hard-to-debug cross-request behavior.
Review cue
- Do not deep-merge untrusted objects into trusted configuration; use allowlisted fields and plain object construction.
Related pages
- Node / Next.js / React Security Review Guide
- Frontend Security Review Playbook
- Browser Security Foundations: CSP, CORS, Cookies, and Sessions
Author attribution: Ivan Piskunov, 2026 - Educational and defensive-engineering use.