Sensitive Data Exposure in Vibe Coded Apps
How AI tools accidentally leak secrets through error messages, logs, and API responses
Sensitive data exposure happens when your app reveals secrets, user data, or system info through error messages, logs, or API responses. AI tools generate verbose debugging code that leaks this data. Sanitize errors, filter responses, and remove console.log before production.
Note: OWASP renamed "Sensitive Data Exposure" to "Cryptographic Failures" in 2021 to focus on root causes rather than symptoms.
What is Sensitive Data Exposure?
Sensitive data exposure is when your application accidentally tells attackers things it shouldn't. This includes user credentials, API keys, database queries, stack traces, internal file paths, and personal information (PII).
Unlike hardcoded secrets which are embedded in code, sensitive data exposure happens at runtime through error messages, logs, and API responses.
Three Ways Data Gets Exposed
- Error Messages: Stack traces and verbose errors reveal internal structure, file paths, and database schemas
- Logs: Console.log with passwords, tokens, and user data persists in cloud logging services
- API Responses: Full database objects returned instead of filtered, public-safe fields
Why AI Tools Generate Leaky Code
AI-generated code prioritizes debugging and development convenience over production security. When you ask Cursor, Claude Code, or Bolt to "make it work," they generate code that helps you debug - which also helps attackers.
- Helpful debugging code: AI adds console.log everywhere to help you understand what's happening
- Complete error messages: AI returns full error objects to make debugging easier
- Flexible API responses: AI returns entire database objects for "completeness"
- Development-first thinking: Code that works in development isn't production-safe
Pattern 1: Verbose Error Messages
AI tools generate error handlers that expose internal details to help with debugging. These details are goldmines for attackers.
// VULNERABLE: AI generates helpful error messages
export async function POST(request: Request) {
try {
const body = await request.json()
const user = await db.user.findUnique({
where: { email: body.email }
})
} catch (error) {
// Exposes internal details to attackers!
return NextResponse.json({
error: error.message,
stack: error.stack,
query: `SELECT * FROM users WHERE email = '${body.email}'`
}, { status: 500 })
}
}// SECURE: Generic error to client, detailed log server-side
export async function POST(request: Request) {
try {
const body = await request.json()
const user = await db.user.findUnique({
where: { email: body.email }
})
} catch (error) {
// Log detailed error server-side only
console.error('User lookup failed:', {
error: error.message,
timestamp: new Date().toISOString(),
emailPrefix: body.email?.substring(0, 3) + '***'
})
// Return generic error to client
return NextResponse.json({
error: 'An error occurred. Please try again.'
}, { status: 500 })
}
}What attackers learn from verbose errors: Database type and version, table and column names, internal file paths, component versions, and sometimes even SQL queries they can use for SQL injection.
Pattern 2: Console.log with Sensitive Data
AI tools liberally add console.log to help you understand the code. In production, these logs persist in cloud services like Vercel, AWS CloudWatch, and Datadog - visible to anyone with log access.
// VULNERABLE: AI adds console.log for debugging
export async function login(email: string, password: string) {
console.log('Login attempt:', { email, password }) // PASSWORD IN LOGS!
const user = await authenticate(email, password)
console.log('User authenticated:', user) // Full user object!
const token = generateToken(user)
console.log('Generated token:', token) // TOKEN IN LOGS!
return token
}// SECURE: Never log sensitive data
export async function login(email: string, password: string) {
// Log only non-sensitive identifiers
console.log('Login attempt for:', email.substring(0, 3) + '***')
const user = await authenticate(email, password)
console.log('User authenticated:', user.id) // Only ID, not full object
const token = generateToken(user)
// Never log tokens - log success/failure only
console.log('Token generated successfully for user:', user.id)
return token
}Remember: Logs persist forever. Anyone with log access (DevOps, support, compromised accounts) can see everything you logged.
Pattern 3: Over-Exposed API Responses
AI tools return full database objects because it's easier and more "flexible." This leaks password hashes, internal flags, and data users shouldn't see.
// VULNERABLE: AI returns full database object
export async function GET(request: Request) {
const { userId } = await request.json()
// Returns EVERYTHING including password hash!
const user = await db.user.findUnique({
where: { id: userId }
})
return NextResponse.json(user)
}
// Response includes:
// { id, email, password_hash, internal_role,
// stripe_customer_id, phone, address, ... }// SECURE: Return only needed fields
export async function GET(request: Request) {
const { userId } = await request.json()
const user = await db.user.findUnique({
where: { id: userId },
select: {
id: true,
name: true,
email: true,
avatarUrl: true,
// Explicitly exclude: password, tokens, internal flags
}
})
return NextResponse.json(user)
}
// Or use a DTO (Data Transfer Object)
function toPublicUser(user: User): PublicUser {
return { id: user.id, name: user.name, email: user.email }
}Pattern 4: Sensitive Data in URLs
AI tools sometimes put tokens, emails, and other sensitive data in URL query strings. This data ends up in server logs, browser history, proxy logs, and referrer headers.
// VULNERABLE: Sensitive data in URL
// GET /api/reset?token=abc123&[email protected]
export async function GET(request: Request) {
const url = new URL(request.url)
const token = url.searchParams.get('token')
const email = url.searchParams.get('email')
// These get logged in:
// - Server access logs
// - Browser history
// - Proxy logs
// - Referrer headers (if user clicks a link)
}// SECURE: Use POST body for sensitive data
// POST /api/reset
// Body: { token: "abc123", email: "[email protected]" }
export async function POST(request: Request) {
const { token, email } = await request.json()
// Body data not logged by default
// Not in browser history
// Not in referrer headers
}Per OWASP guidelines, never put tokens, passwords, session IDs, or PII in URL query strings. The PortSwigger Information Disclosure Guide provides detailed exploitation techniques attackers use.
Framework Error Configuration
Most frameworks have settings to hide detailed errors in production. Make sure these are configured correctly.
// next.config.js
module.exports = {
// Disable source maps in production (don't expose code)
productionBrowserSourceMaps: false,
}// Custom error handler for Express
app.use((err, req, res, next) => {
// Log detailed error server-side
console.error(err.stack)
// Return generic error based on environment
res.status(500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message // Only in development
})
})AI Fix Prompt
Copy this prompt into any AI tool to scan your code for sensitive data exposure:
Review my code for sensitive data exposure vulnerabilities:
1. **Error Messages (CWE-209)**: Find try/catch blocks that return error.message, error.stack, or detailed error info to clients. Replace with generic messages and log details server-side only.
2. **Console.log Statements (CWE-532)**: Find console.log that includes:
- Passwords, tokens, API keys
- Full user objects
- Database queries
- Session data
Remove or sanitize before production.
3. **API Responses (CWE-200)**: Find endpoints that return full database objects. Add explicit field selection (select: {id: true, name: true}) or use DTOs to return only public fields.
4. **Query Strings**: Find sensitive data passed in URL parameters (tokens, emails, session IDs). Move to request body for POST requests.
5. **Stack Traces**: Ensure production error pages don't expose stack traces. Configure framework error handling appropriately.
For each issue:
- Show the vulnerable code
- Show the secure replacement
- Note the CWE reference (CWE-200, CWE-209, CWE-532)Frequently Asked Questions
What is sensitive data exposure?
Sensitive data exposure happens when your app accidentally reveals secrets, user data, or system info through error messages, logs, or API responses. It includes exposing passwords, tokens, PII, stack traces, and internal system details to unauthorized parties.
How do I prevent stack traces in production?
Configure your framework to hide stack traces in production. In Next.js, set productionBrowserSourceMaps: false. In Express, use custom error handlers that log details server-side but return generic messages to clients. Never expose error.stack to users.
What data should never be in error messages?
Never include passwords, API keys, tokens, session IDs, database queries, file paths, user PII, or stack traces in error messages. Return generic messages like "An error occurred" to clients and log detailed info server-side.
How do I filter API responses to prevent data leakage?
Always use explicit field selection in database queries (select: {id: true, name: true}) instead of select(*). Create DTOs (Data Transfer Objects) to transform database objects to public-safe versions. Never return raw database objects directly.
Is console.log a security risk?
Yes. Console.log statements persist in cloud logging services like Vercel, AWS CloudWatch, and Datadog. Logging passwords, tokens, or full user objects exposes sensitive data. Sanitize or remove console.log before production deployment.