Skip to content

type(security): Security Headers Missing on HTML Error Responses #100

@jonasyr

Description

@jonasyr

Severity: Medium
Component: Express Error Handler / Helmet Configuration
Status: Unresolved

Description

While the application uses Helmet middleware for security headers, 404 error pages returning HTML content lack proper Content-Security-Policy (CSP) directives. This compounds the XSS vulnerability in Issue #96 and leaves error pages vulnerable to various injection attacks.

Evidence

From test files, 404 responses show:

  • Content-Type: text/html; charset=utf-8
  • Helmet headers present but CSP may not be strict enough for error pages

Current CSP from http_signals file:

Content-Security-Policy: default-src 'self';base-uri 'self';font-src 'self' https: data:;
  form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';
  script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';
  upgrade-insecure-requests

Issues

  1. HTML error pages may not properly enforce CSP
  2. No explicit X-Content-Type-Options: nosniff verification for all HTML responses
  3. Missing Content-Disposition: inline to prevent download misinterpretation

Recommended Fix

// apps/backend/src/middleware/errorHandler.ts
export const notFoundHandler = (req: Request, res: Response) => {
  // Enforce strict security headers for error responses
  res.setHeader('Content-Security-Policy', 
    "default-src 'none'; " +
    "script-src 'none'; " +
    "style-src 'none'; " +
    "img-src 'none'; " +
    "object-src 'none'; " +
    "base-uri 'none'; " +
    "form-action 'none'; " +
    "frame-ancestors 'none'"
  );
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  
  // Return JSON for API routes, sanitized HTML for others
  if (req.path.startsWith('/api/')) {
    return res.status(404).json({
      error: 'Not Found',
      code: 'NOT_FOUND'
    });
  }
  
  // For non-API routes, return minimal HTML
  const sanitizedPath = req.path.replace(/[<>"'&]/g, '');
  res.status(404).send(`<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>404 Not Found</title>
</head>
<body>
  <h1>404 Not Found</h1>
  <p>The requested resource was not found.</p>
</body>
</html>`);
};

Testing

# Verify CSP headers on error pages
curl -v http://localhost:3001/nonexistent-page 2>&1 | grep -i "content-security-policy"

# Verify X-Content-Type-Options
curl -v http://localhost:3001/nonexistent-page 2>&1 | grep -i "x-content-type-options"

# Verify no script execution possible
curl "http://localhost:3001/%3Cscript%3Ealert(1)%3C%2Fscript%3E" | grep -i "alert"
# Should NOT contain executable script tag

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions