# 500 pages do not expose internal details

- **Pattern:** `ab-002280` (`saas-error-handling.error-boundaries.five-hundred-page-no-internals`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002280
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002280)

## Why it matters

Rendering `error.message` or `error.stack` to the DOM or returning it in an API JSON response exposes your internal architecture to anyone who triggers a 500 — framework versions, file system paths, database error codes, and third-party service identifiers are all visible in unfiltered stack traces. OWASP A05 (Security Misconfiguration) and CWE-209 (Generation of Error Message Containing Sensitive Information) classify this as an active information-disclosure vulnerability, not merely a cosmetic issue. Attackers routinely trigger intentional errors to enumerate your stack before targeting known CVEs. CWE-215 (Insertion of Sensitive Information Into Debugging Code) further applies when dev-mode conditionals are left in production builds.

## Severity rationale

Critical because stack traces and raw error messages in production responses give attackers a direct map of your framework, file paths, and database schema.

## Remediation

Replace all `{error.message}` or `{error.stack}` renders with a static user-safe message. Log internally, never to the DOM.

```tsx
// app/error.tsx — safe version
'use client'
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  useEffect(() => { reportError(error) }, [error])
  return (
    <div>
      <h2>Something went wrong</h2>
      <p>We've been notified. Please try again.</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  )
}
```

For API routes:

```ts
} catch (error) {
  console.error('[API Error]', error) // server-side only
  return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
```

Verify with `NODE_ENV=production` by triggering a deliberate error and inspecting both the response body and rendered DOM for any technical details.

## Detection

- **ID:** `five-hundred-page-no-internals`
- **Severity:** `critical`
- **What to look for:** Examine the server error page(s): `app/error.tsx`, `app/global-error.tsx`, `pages/_error.tsx`, `pages/500.tsx`, or equivalent custom error pages for non-Next.js frameworks. Check whether the error object's properties (`error.message`, `error.stack`, `error.cause`) are rendered to the DOM. Check for conditional logic that might expose stack traces in production (e.g., `process.env.NODE_ENV === 'development'` checks that render the full error). Also check API route error handlers — do they return the raw `error.message` or stack in JSON responses?
- **Pass criteria:** Count all error pages and API error response handlers. Pass if error pages and API error responses do not render or return the `error.stack`, raw `error.message`, database error details, or file system paths to end users. No more than 0 locations may expose internal error details. Extract and quote the first error response pattern you find to verify it does not leak internals. A generic "Something went wrong" message with a request ID or support code is acceptable and good practice. Report even on pass: "X error response locations examined, 0 expose internal details."
- **Fail criteria:** Fail if `{error.message}` or `{error.stack}` is rendered in JSX. Fail if API routes return `{ error: error.message }` or `{ stack: error.stack }` directly. Do NOT pass when error.message is displayed in a toast or alert — the message may contain internal details that vary by error type.
- **Skip (N/A) when:** Never — every web application can encounter a 500 condition and must handle it safely.
- **Detail on fail:** Identify the exact location where internals are exposed (e.g., "app/error.tsx renders {error.message} directly; api/users/route.ts returns raw error.message in catch block"). Max 500 chars.
- **Remediation:** Exposing stack traces and internal error messages in production gives attackers a roadmap to your application's internals — framework versions, file paths, database schemas, and third-party service names are all visible in unfiltered error output.

  Replace internal error details with user-safe messages:

  ```tsx
  // app/error.tsx — safe version
  'use client'
  export default function Error({ error, reset }: { error: Error; reset: () => void }) {
    // Log internally, never render error.message or error.stack to the DOM
    useEffect(() => { reportError(error) }, [error])
    return (
      <div>
        <h2>Something went wrong</h2>
        <p>We've been notified and are looking into it. Please try again.</p>
        <button onClick={() => reset()}>Try again</button>
      </div>
    )
  }
  ```

  For API routes:

  ```ts
  } catch (error) {
    console.error('[API Error]', error) // server-side only
    return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
  }
  ```

  Verification: trigger a deliberate error in production mode (`NODE_ENV=production`) and inspect the response body and rendered DOM for any technical details.

---

## External references

- cwe CWE-209
- cwe CWE-215
- owasp A05

Taxons: observability, cryptography-and-secrets

HTML version: https://auditbuffet.com/patterns/ab-002280
