Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 2025-12-23 - Missing Security Headers
**Vulnerability:** The Express application was missing standard security headers (HSTS, CSP, X-Frame-Options, etc.), increasing risk of XSS, clickjacking, and MIME-sniffing.
**Learning:** Even with manual CORS handling, standard security headers are often overlooked in custom server implementations.
**Prevention:** Implemented a reusable `securityHeaders` middleware in `src/security-headers.ts` that enforces these headers on all responses.
26 changes: 26 additions & 0 deletions src/http-transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1876,4 +1876,30 @@ describeIfListen('HttpTransport', () => {
expect(isAllowed).toBe(true); // localhost always allowed
});
});

describe('Security Headers', () => {
it('should include standard security headers in responses', async () => {
const response = await request(app).get('/health');

expect(response.headers['x-content-type-options']).toBe('nosniff');
expect(response.headers['x-frame-options']).toBe('DENY');
expect(response.headers['content-security-policy']).toContain("default-src 'self'");
expect(response.headers['referrer-policy']).toBe('no-referrer');
expect(response.headers['permissions-policy']).toBe('interest-cohort=()');
});

it('should include HSTS header when X-Forwarded-Proto is https', async () => {
const response = await request(app)
.get('/health')
.set('X-Forwarded-Proto', 'https');

expect(response.headers['strict-transport-security']).toContain('max-age=31536000');
});

it('should not include HSTS header on plain HTTP', async () => {
const response = await request(app).get('/health');

expect(response.headers['strict-transport-security']).toBeUndefined();
});
});
});
4 changes: 4 additions & 0 deletions src/http-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type {
import { isInitializeRequest } from './jsonrpc-validator.js';
import { MetricsCollector } from './metrics.js';
import { ExternalOAuthProvider } from './oauth-provider.js';
import { securityHeaders } from './security-headers.js';
import type { AuthInterceptor } from './types/profile.js';
import { HTTP_STATUS, MIME_TYPES, OAUTH_PATHS, TIMEOUTS, OAUTH_RATE_LIMIT } from './constants.js';
import { escapeHtmlSafe } from './validation-utils.js';
Expand Down Expand Up @@ -106,6 +107,9 @@ export class HttpTransport {
next();
});

// Security headers (standard hardening)
this.app.use(securityHeaders);

// DNS rebinding protection when binding to localhost
// Deny requests with mismatched Host headers to prevent DNS rebinding attacks
// Applies when server host is localhost/127.0.0.1, regardless of auth configuration
Expand Down
37 changes: 37 additions & 0 deletions src/security-headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Request, Response, NextFunction } from 'express';

/**
* Middleware to add security headers to all responses
*
* Why:
* - X-Content-Type-Options: nosniff - Prevents MIME-sniffing
* - X-Frame-Options: DENY - Prevents clickjacking
* - Content-Security-Policy: default-src 'self' - Reduces XSS risk
* - Referrer-Policy: no-referrer - Protects user privacy
* - Permissions-Policy: interest-cohort=() - Disables FLoC
*/
export function securityHeaders(req: Request, res: Response, next: NextFunction): void {
// Prevent browser from guessing the MIME type
res.setHeader('X-Content-Type-Options', 'nosniff');

// Prevent the site from being embedded in a frame (clickjacking protection)
res.setHeader('X-Frame-Options', 'DENY');

// Restrict resources to the same origin (XSS mitigation)
// Note: This is a strict policy; might need adjustment if external resources are needed later
res.setHeader('Content-Security-Policy', "default-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'");

// Do not send referrer header
res.setHeader('Referrer-Policy', 'no-referrer');

// Disable FLoC (Federated Learning of Cohorts)
res.setHeader('Permissions-Policy', 'interest-cohort=()');

// HSTS (Strict-Transport-Security)
// Only set if the request is secure (HTTPS) or signaled as such by a proxy
if (req.secure || req.headers['x-forwarded-proto'] === 'https') {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}

next();
}
Loading