Skip to main content
interlace
Plugin: operabilityRules

no-verbose-error-messages

Prevent exposing stack traces to users in API responses

Prevents exposing stack traces and verbose error details to users through API responses. This rule is part of eslint-plugin-operability and provides LLM-optimized error messages.

⚠️ Quality/Security rule | 📋 Set to warn in recommended

Quick Summary

AspectDetails
CWE ReferenceCWE-209 (Info in Errors)
SeverityMedium (information disclosure)
Auto-Fix❌ No auto-fix (requires safe error handling)
CategoryQuality / Operability
Best ForExpress/Fastify/Koa applications with API endpoints

Vulnerability and Risk

Vulnerability: Exposing stack traces in API responses reveals internal application structure, file paths, library versions, and code organization to potential attackers.

Risk: Verbose error messages help attackers:

  • Map internal application structure
  • Identify vulnerable dependencies
  • Craft targeted exploits
  • Understand deployment environment

Rule Details

This rule reports a send / json call (res.send, res['send'], res.status(500).json) whose first argument carries an error's .stack:

  • read directly: err.stack, err['stack'];
  • through a const (const trace = err.stack) or destructured from the error (const { stack } = err);
  • as the value of any property of an object literal, under any key, nested up to four objects deep: { stack: err.stack }, { 'trace': err.stack }, { error: { stack: err.stack } };
  • interpolated into a template literal, joined with +, or a result of ??, || or a ternary;
  • inside an array literal, a spread of a const object that carries it, a computed key, or a getter's return value.

The stack is what leaks, not the key name: { stack: 'hidden' } is not reported, and neither is a value the rule cannot see into (a parameter, or { ...err }, which does not copy an Error's non-enumerable stack), or a destructured stack reassigned before it is sent. The receiver is not resolved: any object's send / json is read as a response.

Why This Matters

RiskImpactSolution
🔍 ReconnaissanceAttackers learn internal detailsReturn generic error messages
📂 Path DisclosureFile paths reveal deployment structureLog details server-side only
📚 Version LeakLibrary versions expose vulnerabilitiesUse error codes, not raw errors

Configuration

This rule has no configuration options.

{
  rules: {
    'operability/no-verbose-error-messages': 'error'
  }
}

Examples

❌ Incorrect

app.use((err, req, res, next) => {
  // Sending stack trace directly
  res.status(500).send(err.stack); // ❌ Stack trace exposed
});

app.get('/api/data', async (req, res) => {
  try {
    const data = await getData();
    res.json(data);
  } catch (error) {
    // Stack trace in JSON response
    res.status(500).json({
      message: error.message,
      stack: error.stack, // ❌ Stack trace in response
    });
  }
});

// Error object with stack property
app.use((err, req, res, next) => {
  res.json({
    error: true,
    stack: err.stack, // ❌ Exposed
    path: __dirname, // ❌ Also exposed
  });
});

✅ Correct

app.use((err, req, res, next) => {
  // Log full error server-side
  console.error('Request failed:', {
    // ✅ Server-side logging
    error: err,
    stack: err.stack,
    requestId: req.id,
  });

  // Return generic message to client
  res.status(500).json({
    error: 'Internal server error', // ✅ Generic message
    requestId: req.id, // ✅ Correlation ID for support
  });
});

// Better: Custom error handler with error codes
class AppError extends Error {
  constructor(
    public code: string,
    public statusCode: number,
    message: string,
  ) {
    super(message);
  }
}

app.use((err, req, res, next) => {
  console.error(err); // ✅ Full error logged server-side

  if (err instanceof AppError) {
    res.status(err.statusCode).json({
      code: err.code, // ✅ User-facing code
      message: err.message, // ✅ Safe message
    });
  } else {
    res.status(500).json({
      code: 'INTERNAL_ERROR',
      message: 'An unexpected error occurred',
    });
  }
});

Error Handling Best Practices

Use Error Codes

const ERROR_CODES = {
  VALIDATION_ERROR: { status: 400, message: 'Invalid input' },
  NOT_FOUND: { status: 404, message: 'Resource not found' },
  UNAUTHORIZED: { status: 401, message: 'Authentication required' },
  INTERNAL: { status: 500, message: 'Internal server error' },
};

function sendError(res, code, requestId) {
  const error = ERROR_CODES[code] || ERROR_CODES.INTERNAL;
  res.status(error.status).json({
    error: code,
    message: error.message,
    requestId,
  });
}

Environment-Aware Handler

app.use((err, req, res, next) => {
  console.error(err);

  const response = {
    error: 'Internal server error',
    requestId: req.id,
  };

  // Only include stack in development
  if (process.env.NODE_ENV === 'development') {
    response.stack = err.stack; // ⚠️ Development only
  }

  res.status(500).json(response);
});

Security Impact

VulnerabilityCWEOWASPCVSSImpact
Info Disclosure209A01:20214.3 MediumReconnaissance aid
Error Handling755A01:20213.7 LowInformation leakage

Known False Negatives

Indirect Stack Access

Why: Indirect property access not tracked.

// ❌ NOT DETECTED - indirect access
const key = 'stack';
res.json({ [key]: error[key] });

Mitigation: Use explicit error sanitization functions.

Further Reading

Use this rule

npm install -D oxlint-plugin-operability
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-operability"],  "rules": {    "operability/no-verbose-error-messages": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-operability'],    rules: {      'operability/no-verbose-error-messages': 'error',    },  },});

oxlint-plugin-operability re-exports eslint-plugin-operability/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.