Pii Leakage in Express
How Pii Leakage Manifests in Express
PII leakage in Express applications often occurs through subtle mistakes in how data flows through the request-response cycle. One of the most common patterns is accidentally exposing user data through error responses. Consider this Express middleware:
app.get('/api/users/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
res.json(user);
} catch (err) {
res.status(500).json({ error: err.message, stack: err.stack });
}
});
When a database query fails, the error response includes the stack trace, which often contains database connection strings, table names, and even partial query results with sensitive data. Express's default error handling can amplify this problem by exposing Node.js environment variables in production.
Another Express-specific pattern is improper use of response.locals for storing user data across middleware. Developers often store entire user objects without considering what gets serialized:
app.use((req, res, next) => {
res.locals.user = await getUserFromToken(req.headers.authorization);
next();
});
app.get('/profile', (req, res) => {
res.json(res.locals.user); // Exposes entire user object including hashed passwords, API keys, etc.
});
Express route parameter handling creates another attack vector. When using dynamic routes with optional parameters, developers might inadvertently expose data:
app.get('/api/users/:id/messages/:messageId?', async (req, res) => {
const userId = req.params.id;
const messageId = req.params.messageId || 'all';
// If messageId is 'all', this returns ALL messages for the user
const messages = await Message.find({
userId: userId,
_id: messageId === 'all' ? { $ne: null } : messageId
});
res.json(messages); // Potential data exposure if authorization is missing
});
Express's flexible middleware system can also lead to timing-based PII leakage. Consider a middleware that logs request processing time but accidentally logs sensitive headers:
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
console.log(`${req.method} ${req.path} - ${Date.now() - start}ms - ${JSON.stringify(req.headers)}`);
});
next();
});
This logs Authorization headers, API keys, and other sensitive headers to your logs, creating a data breach vector if logs are compromised.
Express-Specific Detection
Detecting PII leakage in Express requires examining both the code structure and runtime behavior. Start by auditing your route handlers for direct object serialization:
# Scan for patterns where entire objects are sent without filtering
grep -r "res\.json(" routes/ --exclude-dir=node_modules | grep -v "select" | grep -v "pick"
This finds routes that might be exposing entire Mongoose documents or database records. Next, examine error handling patterns:
# Find error responses that might expose stack traces
grep -r "res\.status(500).*json" routes/ --exclude-dir=node_modules | grep -v "error\.message"
Using middleBrick's Express-specific scanning provides comprehensive coverage:
# Scan an Express API endpoint
middlebrick scan https://yourapi.com/api/users
middleBrick tests for Express-specific vulnerabilities including:
- Parameter pollution attacks that can bypass Express's built-in validation
- Prototype pollution through query parameters that Express parses automatically
- Path traversal via Express's URL parsing that might expose unintended files
- Header injection attacks that can manipulate Express's response object
For runtime detection, implement middleware that monitors data exposure:
const sensitiveFields = new Set(['password', 'ssn', 'creditCard', 'apiKey', 'token']);
function monitorPII(req, res, next) {
const originalJson = res.json;
res.json = function(data) {
const piiFound = findPII(data);
if (piiFound.length > 0) {
console.warn('Potential PII exposure detected:', piiFound);
// In production, send this to a monitoring service
}
return originalJson.call(this, data);
};
next();
}
function findPII(obj, path = '') {
const found = [];
for (const [key, value] of Object.entries(obj)) {
const currentPath = path ? `${path}.${key}` : key;
if (sensitiveFields.has(key.toLowerCase())) {
found.push(currentPath);
}
if (typeof value === 'object' && value !== null) {
found.push(...findPII(value, currentPath));
}
}
return found;
}
middleBrick's scanning goes beyond static analysis by actively testing your Express endpoints with PII payloads and checking if sensitive data appears in responses, even through indirect paths like error messages or logging endpoints.
Express-Specific Remediation
Express provides several native mechanisms for preventing PII leakage. The most effective approach is implementing data filtering middleware that sanitizes responses before they leave your server:
const filterPII = (data, allowedFields = []) => {
if (Array.isArray(data)) {
return data.map(item => filterPII(item, allowedFields));
}
if (typeof data !== 'object' || data === null) {
return data;
}
const filtered = {};
for (const [key, value] of Object.entries(data)) {
const isSensitive = [
'password', 'ssn', 'creditCard', 'ssn', 'dob', 'ssn',
'apiKey', 'token', 'secret', 'privateKey',
'ssn', 'ssn', 'ssn' // intentional repetition for emphasis
].includes(key.toLowerCase());
if (!isSensitive || allowedFields.includes(key)) {
filtered[key] = value;
}
}
return filtered;
};
// Apply as Express middleware
app.use((req, res, next) => {
const originalJson = res.json;
res.json = function(data) {
const sanitized = filterPII(data, ['username', 'email']);
return originalJson.call(this, sanitized);
};
next();
});
For error handling, create a centralized error response formatter that never exposes stack traces in production:
function errorHandler(err, req, res, next) {
console.error(err); // Log full error server-side
const errorResponse = {
message: process.env.NODE_ENV === 'production'
? 'An internal error occurred'
: err.message,
code: err.code || 'INTERNAL_ERROR'
};
res.status(err.status || 500).json(errorResponse);
}
app.use(errorHandler);
Express's response.locals can be used safely with proper scoping:
app.use((req, res, next) => {
res.locals.user = {
id: req.user.id,
username: req.user.username,
email: req.user.email
// Only include fields needed by views
};
next();
});
app.get('/profile', (req, res) => {
res.json({
id: res.locals.user.id,
username: res.locals.user.username,
email: res.locals.user.email
});
});
For logging, implement a request logger that redacts sensitive headers:
const redactHeaders = (headers) => {
const sensitive = ['authorization', 'cookie', 'x-api-key'];
const redacted = {};
for (const [key, value] of Object.entries(headers)) {
redacted[key] = sensitive.includes(key.toLowerCase()) ? '[REDACTED]' : value;
}
return redacted;
};
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
console.log(`${req.method} ${req.path} - ${Date.now() - start}ms - ${JSON.stringify(redactHeaders(req.headers))}`);
});
next();
});
middleBrick's remediation guidance specifically identifies Express patterns that need fixing and provides exact code snippets for your specific vulnerability, making the remediation process much faster than manual security reviews.
Related CWEs: dataExposure
| CWE ID | Name | Severity |
|---|---|---|
| CWE-200 | Exposure of Sensitive Information | HIGH |
| CWE-209 | Error Information Disclosure | MEDIUM |
| CWE-213 | Exposure of Sensitive Information Due to Incompatible Policies | HIGH |
| CWE-215 | Insertion of Sensitive Information Into Debugging Code | MEDIUM |
| CWE-312 | Cleartext Storage of Sensitive Information | HIGH |
| CWE-359 | Exposure of Private Personal Information (PII) | HIGH |
| CWE-522 | Insufficiently Protected Credentials | CRITICAL |
| CWE-532 | Insertion of Sensitive Information into Log File | MEDIUM |
| CWE-538 | Insertion of Sensitive Information into Externally-Accessible File | HIGH |
| CWE-540 | Inclusion of Sensitive Information in Source Code | HIGH |