How to Fix a Serverless Function Leaking Database Connection Credentials in Stack Trace
Quick answer
An unhandled error in your serverless function's logs shows the full database connection string, including the plaintext password, exposed right there in the...
An unhandled error in your serverless function's logs shows the full database connection string, including the plaintext password, exposed right there in the stack trace or error message. This is a genuine security exposure, not just noisy logging β anyone with read access to your function's logs (which is often a wider group of people than has access to your actual secrets manager) can now see live database credentials.
The Problem
A connection failure or unhandled exception logs far more than intended:
Error: connect ECONNREFUSED postgres://app_user:S3cr3tP@ssw0rd@db.example.com:5432/mydb
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:...)
Or, from an ORM's own error formatting that includes the full connection config object it was constructed with:
SequelizeConnectionError: password authentication failed
config: {
username: 'app_user',
password: 'S3cr3tP@ssw0rd',
host: 'db.example.com'
}
Why It Happens
Many database drivers and ORMs include the connection string or full configuration object directly in their error messages or stack traces, specifically to help with debugging β but this convenience becomes a real security problem once those logs flow into a serverless platform's logging service (CloudWatch, Vercel logs, or similar), which typically has broader read access across a team than your actual secrets store does. This happens because:
- The database connection string with embedded credentials is passed directly to the driver, and the driver's own error formatting includes that full string verbatim when a connection fails.
- An ORM's error object includes the full configuration it was instantiated with, and default error logging (including many "helpful" verbose logging setups) serializes the entire error object, credentials and all, without any redaction.
- Generic error-handling middleware logs
error.toString()orJSON.stringify(error)indiscriminately without any awareness that specific error types might carry sensitive embedded data. - Serverless platforms' default logging captures console output broadly, meaning even a single unguarded
console.error(err)call can end up shipping sensitive data into a logging system with much wider access than intended.
The Fix
Immediately rotate any credential that has been exposed in logs β this is the same principle as any other credential leak: the log entry existing at all means the credential should be treated as compromised, regardless of how quickly you fix the logging issue itself:
# Example: rotate a PostgreSQL user's password immediately
ALTER USER app_user WITH PASSWORD 'new_randomly_generated_password';
Update your secrets store and redeploy with the new credential before addressing the logging issue itself, since the exposure has already happened and needs to be closed off first.
Going forward, avoid constructing connection strings with embedded credentials in a way that flows directly into error paths β use separate connection parameters instead of one interpolated string, which many drivers support and which avoids the credential ever appearing as a single greppable substring in error output:
// Instead of a single connection string with embedded credentials
const connectionString = `postgres://${user}:${password}@${host}:5432/${db}`;
// Use separate config fields where the driver supports it
const pool = new Pool({
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
});
Add explicit error sanitization in your error-handling code, stripping known-sensitive fields before anything gets logged, rather than trusting the driver's default error formatting to be safe:
function sanitizeError(err) {
const safe = {
message: err.message?.replace(/:[^:@]+@/, ':***@'), // strip password from any embedded connection string
name: err.name,
code: err.code,
};
return safe;
}
try {
await connectToDatabase();
} catch (err) {
console.error('Database connection failed:', sanitizeError(err));
}
For a more systematic, less error-prone approach across an entire codebase, use a structured logging library with built-in redaction support for known sensitive field names, so this protection applies consistently everywhere rather than requiring every individual error handler to remember to sanitize manually:
const pino = require('pino');
const logger = pino({
redact: ['password', 'connectionString', '*.password', 'config.password'],
});
logger.error({err, config: dbConfig}, 'Database connection failed');
// password field is automatically replaced with [Redacted] in the output
Still Not Working?
If you're not sure whether other credentials might already be exposed in historical logs beyond the one you just found, search your logging platform's history for common credential patterns to check the actual scope of exposure before assuming it was an isolated incident:
# CloudWatch Logs Insights example query
fields @message
| filter @message like /postgres:\/\/[^:]+:[^@]+@/
| sort @timestamp desc
| limit 100
If this search reveals a pattern of repeated exposure rather than a single isolated event, treat it as a broader incident β rotate every credential that appears, and prioritize implementing the systematic redaction approach above rather than only patching the specific error path you happened to notice first.