Skip to main content
Technology & EngineeringVibe Coding Security391 lines

Error Handling and Information Leakage

Quick Summary10 lines
AI-generated error handling is designed for debugging, not production. It returns full stack traces to the client, logs passwords in request bodies, exposes database table names in error messages, and leaves debug endpoints accessible. Every piece of information leaked in an error response is reconnaissance for an attacker.

## Key Points

- Whether an email exists in the system (user enumeration)
- Database table and column names
- Full stack traces with file paths
- Internal query structure
skilldb get vibe-coding-security-skills/error-handling-information-leakageFull skill: 391 lines
Paste into your CLAUDE.md or agent config

Error Handling and Information Leakage

AI-generated error handling is designed for debugging, not production. It returns full stack traces to the client, logs passwords in request bodies, exposes database table names in error messages, and leaves debug endpoints accessible. Every piece of information leaked in an error response is reconnaissance for an attacker.

This skill teaches you to handle errors safely: informative for developers in logs, opaque for users in responses.

The Information Leakage Pattern

What AI generates:

app.post('/api/login', async (req, res) => {
  try {
    const user = await db.query('SELECT * FROM users WHERE email = $1', [req.body.email]);
    if (!user.rows.length) {
      return res.status(404).json({ error: 'User not found with email: ' + req.body.email });
    }
    const valid = await bcrypt.compare(req.body.password, user.rows[0].password_hash);
    if (!valid) {
      return res.status(401).json({ error: 'Invalid password for user: ' + req.body.email });
    }
    res.json({ token: generateToken(user.rows[0]) });
  } catch (err) {
    res.status(500).json({
      error: err.message,
      stack: err.stack,
      query: 'SELECT * FROM users WHERE email = ...',
    });
  }
});

This code leaks:

  • Whether an email exists in the system (user enumeration)
  • Database table and column names
  • Full stack traces with file paths
  • Internal query structure

Safe Error Responses

The Error Boundary Pattern

// Define application error types
class AppError extends Error {
  constructor(
    public statusCode: number,
    public userMessage: string,  // Safe to show to users
    public internalMessage: string, // For logs only
    public errorCode: string,    // Machine-readable error code
  ) {
    super(internalMessage);
    this.name = 'AppError';
  }
}

class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(
      404,
      'The requested resource was not found',  // Generic — no details
      `${resource} with id ${id} not found`,    // Specific — for logs
      'RESOURCE_NOT_FOUND'
    );
  }
}

class AuthenticationError extends AppError {
  constructor(reason: string) {
    super(
      401,
      'Invalid credentials',              // Same message for all auth failures
      `Authentication failed: ${reason}`,  // Specific reason in logs
      'AUTH_FAILED'
    );
  }
}

class ValidationError extends AppError {
  constructor(details: string) {
    super(
      400,
      'The request contains invalid data',
      `Validation failed: ${details}`,
      'VALIDATION_ERROR'
    );
  }
}

Global Error Handler

// Express global error handler — LAST middleware registered
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  // Generate unique error ID for correlation
  const errorId = crypto.randomUUID();

  if (err instanceof AppError) {
    // Known application error — log at appropriate level
    logger.warn({
      errorId,
      errorCode: err.errorCode,
      message: err.internalMessage,
      path: req.path,
      method: req.method,
      userId: req.user?.id,
    });

    return res.status(err.statusCode).json({
      error: err.userMessage,
      code: err.errorCode,
      errorId, // User can reference this for support
    });
  }

  // Unknown error — log full details, return nothing useful
  logger.error({
    errorId,
    message: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method,
    userId: req.user?.id,
    // DO NOT log request body — it may contain passwords
  });

  // Generic response — no internal details
  return res.status(500).json({
    error: 'An internal error occurred',
    code: 'INTERNAL_ERROR',
    errorId,
  });
});

Login Endpoint Done Right

app.post('/api/login', async (req, res, next) => {
  try {
    const { email, password } = LoginSchema.parse(req.body);

    const user = await db.query(
      'SELECT id, password_hash, role FROM users WHERE email = $1',
      [email]
    );

    // SAME response for "user not found" and "wrong password"
    if (!user.rows.length) {
      // Still hash something to prevent timing attacks
      await bcrypt.hash('dummy', 10);
      throw new AuthenticationError(`No user with email ${email}`);
    }

    const valid = await bcrypt.compare(password, user.rows[0].password_hash);
    if (!valid) {
      throw new AuthenticationError(`Wrong password for ${email}`);
    }

    // Both errors produce identical response: { error: "Invalid credentials" }
    const token = generateToken(user.rows[0]);
    res.json({ token });
  } catch (err) {
    next(err);
  }
});

Structured Logging Without PII

What NOT to Log

// DANGEROUS: AI logs everything for debugging
logger.info('Login attempt', {
  email: req.body.email,
  password: req.body.password,    // NEVER log passwords
  creditCard: req.body.card,       // NEVER log payment data
  ssn: req.body.ssn,              // NEVER log PII identifiers
  token: req.headers.authorization, // NEVER log auth tokens
  cookie: req.headers.cookie,      // NEVER log session cookies
});

Safe Logging Pattern

import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  redact: {
    paths: [
      'req.headers.authorization',
      'req.headers.cookie',
      'req.body.password',
      'req.body.token',
      'req.body.creditCard',
      'req.body.ssn',
      '*.password',
      '*.secret',
      '*.token',
    ],
    censor: '[REDACTED]',
  },
  serializers: {
    req: (req) => ({
      method: req.method,
      url: req.url,
      // Only log safe headers
      headers: {
        'content-type': req.headers['content-type'],
        'user-agent': req.headers['user-agent'],
        'x-request-id': req.headers['x-request-id'],
      },
    }),
  },
});

// Middleware to attach request ID
app.use((req, res, next) => {
  req.requestId = req.headers['x-request-id'] || crypto.randomUUID();
  res.setHeader('x-request-id', req.requestId);
  req.log = logger.child({ requestId: req.requestId });
  next();
});

Python Safe Logging

import logging
import re

class PiiFilter(logging.Filter):
    PATTERNS = [
        (re.compile(r'password["\s:=]+\S+', re.I), 'password=[REDACTED]'),
        (re.compile(r'token["\s:=]+\S+', re.I), 'token=[REDACTED]'),
        (re.compile(r'Bearer\s+\S+'), 'Bearer [REDACTED]'),
        (re.compile(r'\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b'), '[CARD_REDACTED]'),
    ]

    def filter(self, record):
        msg = record.getMessage()
        for pattern, replacement in self.PATTERNS:
            msg = pattern.sub(replacement, msg)
        record.msg = msg
        record.args = ()
        return True

logger = logging.getLogger(__name__)
logger.addFilter(PiiFilter())

Stack Trace Suppression

// Environment-aware error detail
function formatError(err: Error, isProduction: boolean) {
  if (isProduction) {
    return {
      error: 'An internal error occurred',
      code: 'INTERNAL_ERROR',
    };
  }
  // Development only
  return {
    error: err.message,
    stack: err.stack,
    name: err.name,
  };
}

// In Express
const isProduction = process.env.NODE_ENV === 'production';
app.use((err, req, res, next) => {
  const status = err.statusCode || 500;
  res.status(status).json(formatError(err, isProduction));
});

Debug Endpoint Removal

AI frequently creates debug endpoints and forgets to remove them.

// DANGEROUS: AI-generated debug endpoints that ship to production
app.get('/debug/env', (req, res) => {
  res.json(process.env); // Leaks ALL environment variables including secrets
});

app.get('/debug/db', async (req, res) => {
  const tables = await db.query("SELECT * FROM information_schema.tables");
  res.json(tables.rows); // Leaks entire database schema
});

app.get('/api/test', (req, res) => {
  res.json({ status: 'ok', config: app.get('config') }); // Leaks config
});

Prevention

# Search for debug endpoints before deploying
grep -rn "debug\|/test\|/dump\|/env\|information_schema\|process\.env" \
  --include="*.ts" --include="*.js" --include="*.py" \
  src/ routes/ api/

# CI check: fail if debug patterns exist in production code
// If you need debug endpoints, gate them strictly
if (process.env.NODE_ENV === 'development') {
  app.get('/debug/health', debugHealthHandler);
}
// Better: use a separate debug server on a different port
// that is never exposed externally

Version and Server Header Removal

// Express
app.disable('x-powered-by');

// Additional headers to remove or set
app.use((req, res, next) => {
  res.removeHeader('X-Powered-By');
  res.removeHeader('Server');
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  next();
});

// Nginx
server {
  server_tokens off;  # Removes version from Server header
  more_clear_headers Server;  # Removes Server header entirely
}

Custom Error Pages

// HTML applications need custom error pages
// AI generates default framework error pages that leak info

// 404 handler
app.use((req, res) => {
  if (req.accepts('html')) {
    res.status(404).sendFile(path.join(__dirname, 'public', '404.html'));
  } else {
    res.status(404).json({ error: 'Not found' });
  }
});

// 500 handler — never show the default Express error page
app.use((err, req, res, next) => {
  logger.error({ err, path: req.path });
  if (req.accepts('html')) {
    res.status(500).sendFile(path.join(__dirname, 'public', '500.html'));
  } else {
    res.status(500).json({ error: 'Internal server error' });
  }
});

The Error Handling Checklist

CheckRisk if Missing
Stack traces hidden in productionReveals code structure, file paths, dependencies
Same message for all auth failuresUser enumeration
No PII in logsCredential exposure, compliance violation
Debug endpoints removedFull system access
Server/version headers removedTargeted exploit selection
Error IDs in responsesUsers can't reference issues to support
Custom error pagesFramework version disclosure
Request bodies not logged rawPassword/token leakage in log storage

Every error message is a potential intelligence report for an attacker. Make your errors helpful for your team (in logs) and useless for attackers (in responses).

Install this skill directly: skilldb add vibe-coding-security-skills

Get CLI access →