Spring4shell in Express
How Spring4shell Manifests in Express
Spring4shell (CVE-2022-22965) is a remote code execution (RCE) vulnerability in Spring Framework's data binding. While it targets Java/Spring, the underlying pattern—unsafe deserialization of user-controlled data altering object prototypes—has direct analogs in Express.js applications. In Express, this typically manifests through prototype pollution via JSON deserialization in middleware like express.json() or body-parser.
Attack Pattern: An attacker sends a crafted JSON payload with keys like __proto__, constructor, or prototype. If the application merges this payload into existing objects (e.g., via Object.assign(), lodash.merge(), or manual iteration), the attacker can pollute Object.prototype. Once polluted, all objects in the Node.js process inherit malicious properties, potentially leading to RCE if the application later uses these properties in dangerous contexts (e.g., eval(), child_process.exec(), or template engines).
Express-Specific Code Path: Consider an Express route that updates a user profile:
app.put('/user/:id', (req, res) => {
// Vulnerable: merging user-supplied JSON directly
const updatedUser = Object.assign({}, req.user, req.body);
saveToDatabase(updatedUser);
res.json(updatedUser);
});If req.body contains { "__proto__": { "isAdmin": true } }, Object.assign will pollute Object.prototype.isAdmin. Any subsequent object check like if (user.isAdmin) could now be true for all users. Worse, if the app uses a library that executes code based on object properties (e.g., a misconfigured template engine), RCE becomes possible.
Another common vector is nested prototype pollution in configuration objects:
app.use(express.json());
app.post('/config', (req, res) => {
// Vulnerable: deep merge without protection
const newConfig = merge(deepClone(appConfig), req.body); // merge from lodash
applyConfig(newConfig);
res.send('OK');
});Here, a payload like { "__proto__": { "exec": "require('child_process').exec('malicious')" } } could inject an exec property into all objects. If applyConfig later does config.exec('some command'), arbitrary code executes.
Express-Specific Detection
Detecting prototype pollution in Express requires testing how the API handles JSON with prototype-related keys. middleBrick’s Input Validation and Data Exposure checks actively probe for this by sending payloads like:
{ "__proto__": { "polluted": true } }{ "constructor": { "prototype": { "isAdmin": true } } }- Nested variants (e.g.,
{ "settings": { "__proto__": { "admin": true } } })
The scanner then analyzes the response for signs of pollution (e.g., unexpected fields in other objects, changes in application behavior) or direct code execution echoes. It also reviews OpenAPI specs for parameters that accept arbitrary JSON objects—a high-risk indicator.
Manual Detection Steps:
- Identify merge points: Search your codebase for
Object.assign,merge,extend, or libraries likelodash.merge,qsparsing nested objects. - Test with curl: Send a payload to an endpoint that accepts JSON:
Then, in a separate request, check ifcurl -X PUT http://localhost:3000/user/123 \ -H "Content-Type: application/json" \ -d '{"__proto__":{"isAdmin":true}}'isAdminappears on unrelated objects (e.g., in a different user's profile response). - Check for gadget chains: If your Express app uses libraries like
serialize-javascriptormomentwith prototype pollution, they may provide gadget chains to escalate to RCE.
Scanning with middleBrick: Use the CLI to automate this testing across all endpoints:
middlebrick scan https://your-api.com --format jsonThe report will flag any endpoint where prototype pollution is possible, categorize it under Input Validation (A03:2021 – Injection), and provide severity scores. The OpenAPI analysis cross-references your spec: if a parameter is type: object without a strict schema, it’s flagged as a potential vector. For CI/CD integration, add the GitHub Action to scan staging APIs before deploy.Express-Specific Remediation
Remediation focuses on preventing prototype pollution at the deserialization layer and defense in depth. Never trust user-supplied JSON to merge into existing objects without sanitization.
1. Use Safe Merge Functions: Replace Object.assign and lodash.merge with versions that ignore prototype keys. Libraries like lodash.merge have a prototype option, but better to use dedicated sanitizers:
const safeMerge = require('safe-merge'); // or implement manually
app.put('/user/:id', (req, res) => {
// Sanitize req.body before merging
const sanitizedBody = sanitize(req.body); // removes __proto__, constructor, prototype
const updatedUser = Object.assign({}, req.user, sanitizedBody);
// ...
});A simple sanitizer:
function sanitize(obj) {
if (obj && typeof obj === 'object') {
const copy = Array.isArray(obj) ? [] : {};
for (const key in obj) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
copy[key] = sanitize(obj[key]);
}
return copy;
}
return obj;
}2. Schema Validation: Use Joi (via celebrate) or Zod to strictly define expected shapes. This blocks unexpected keys entirely:
const Joi = require('joi');
const userSchema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required()
// No allowance for arbitrary keys
});
app.put('/user/:id', (req, res) => {
const { error } = userSchema.validate(req.body, { abortEarly: false });
if (error) return res.status(400).json({ errors: error.details });
// Safe to use req.body now
});3. Freeze Prototypes (Defense in Depth): At application startup, freeze Object.prototype to make pollution impossible:
Object.freeze(Object.prototype);
Object.freeze(Array.prototype);
// Note: This may break some legacy libraries that rely on prototype modification.4. Avoid Dynamic Code Execution: Ensure no part of your stack uses eval(), new Function(), or template engines that execute code from user input (e.g., vm.runInNewContext).
5. Dependency Auditing: Some libraries (e.g., older lodash versions) are vulnerable to prototype pollution themselves. Use npm audit and update to patched versions. middleBrick’s Inventory Management check can flag vulnerable dependencies if you provide a package.json via OpenAPI spec extensions.
Monitoring: After fixes, use middleBrick’s Pro plan continuous monitoring to re-scan endpoints on a schedule. The Dashboard tracks your Input Validation score over time, ensuring regressions are caught early. If you integrate via the GitHub Action, set a threshold to fail builds if the risk score drops below 'A' for this category.