mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-01 03:27:01 +00:00
🦺 feat: Configurable Baseline HTTP Security Headers (#14445)
Adds helmet's CSP-independent headers (HSTS, X-Frame-Options, X-Content-Type-Options, COOP, CORP, Referrer-Policy) on every response, with contentSecurityPolicy explicitly disabled. Every header that can break a deployment is configurable, so there is no allow-list to go stale the way #7377's hardcoded CSP directives did. HSTS includeSubDomains defaults off rather than matching helmet's on-by-default: it would otherwise pin every sibling subdomain to HTTPS for a year in every visitor's browser, and undoing that requires serving max-age=0 from each affected host.
This commit is contained in:
parent
3d2da403ce
commit
2ef12b1e1d
10 changed files with 431 additions and 0 deletions
|
|
@ -17,6 +17,7 @@ const {
|
|||
apiNotFound,
|
||||
ErrorController,
|
||||
QUERY_DEVTOOLS_HEADER,
|
||||
createSecurityHeaders,
|
||||
performStartupChecks,
|
||||
handleJsonParseError,
|
||||
initializeFileStorage,
|
||||
|
|
@ -353,6 +354,12 @@ if (cluster.isMaster) {
|
|||
app.disable('x-powered-by');
|
||||
app.set('trust proxy', trusted_proxy);
|
||||
|
||||
/* Registered ahead of every route so health checks carry the headers too. */
|
||||
const securityHeaders = createSecurityHeaders();
|
||||
if (securityHeaders) {
|
||||
app.use(securityHeaders);
|
||||
}
|
||||
|
||||
if (isEnabled(process.env.TRUST_TENANT_HEADER)) {
|
||||
logger.warn(
|
||||
'[Security] TRUST_TENANT_HEADER is active. Ensure your reverse proxy strips and sets ' +
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const {
|
|||
createMetrics,
|
||||
ErrorController,
|
||||
memoryDiagnostics,
|
||||
createSecurityHeaders,
|
||||
performStartupChecks,
|
||||
handleJsonParseError,
|
||||
GenerationJobManager,
|
||||
|
|
@ -155,6 +156,12 @@ const startServer = async () => {
|
|||
app.disable('x-powered-by');
|
||||
app.set('trust proxy', trusted_proxy);
|
||||
|
||||
/* Registered ahead of every route so health checks carry the headers too. */
|
||||
const securityHeaders = createSecurityHeaders();
|
||||
if (securityHeaders) {
|
||||
app.use(securityHeaders);
|
||||
}
|
||||
|
||||
if (isEnabled(process.env.TRUST_TENANT_HEADER)) {
|
||||
logger.warn(
|
||||
'[Security] TRUST_TENANT_HEADER is active. Ensure your reverse proxy strips and sets ' +
|
||||
|
|
|
|||
|
|
@ -170,6 +170,22 @@ describe('Startup readiness wiring', () => {
|
|||
expect(timeoutConfigIndex).toBeLessThan(shutdownIndex);
|
||||
});
|
||||
|
||||
it('registers security headers ahead of the health endpoints in both server entries', () => {
|
||||
const experimental = fs.readFileSync(path.join(__dirname, 'experimental.js'), 'utf8');
|
||||
|
||||
for (const [name, contents] of [
|
||||
['index.js', source],
|
||||
['experimental.js', experimental],
|
||||
]) {
|
||||
const headersIndex = contents.indexOf('const securityHeaders = createSecurityHeaders();');
|
||||
const healthIndex = contents.indexOf("app.get('/health'");
|
||||
|
||||
expect([name, headersIndex > -1]).toEqual([name, true]);
|
||||
expect([name, healthIndex > -1]).toEqual([name, true]);
|
||||
expect([name, headersIndex < healthIndex]).toEqual([name, true]);
|
||||
}
|
||||
});
|
||||
|
||||
it('mounts the chat-start readiness gate before agent routes', () => {
|
||||
const readinessGateIndex = source.indexOf(
|
||||
"app.use('/api/agents/chat', rejectChatStartsUntilReady);",
|
||||
|
|
@ -250,6 +266,27 @@ describe('Server Configuration', () => {
|
|||
expect(response.text).toBe('OK');
|
||||
});
|
||||
|
||||
it('should set baseline security headers on health checks', async () => {
|
||||
const response = await request(app).get('/health');
|
||||
|
||||
expect(response.headers['strict-transport-security']).toBe('max-age=31536000');
|
||||
expect(response.headers['x-frame-options']).toBe('SAMEORIGIN');
|
||||
expect(response.headers['x-content-type-options']).toBe('nosniff');
|
||||
expect(response.headers['cross-origin-opener-policy']).toBe('same-origin');
|
||||
expect(response.headers['cross-origin-resource-policy']).toBe('same-origin');
|
||||
expect(response.headers['referrer-policy']).toBe('no-referrer');
|
||||
});
|
||||
|
||||
it('should set baseline security headers on the index page without a CSP', async () => {
|
||||
const response = await request(app).get('/');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers['x-frame-options']).toBe('SAMEORIGIN');
|
||||
expect(response.headers['x-content-type-options']).toBe('nosniff');
|
||||
expect(response.headers['content-security-policy']).toBeUndefined();
|
||||
expect(response.headers['content-security-policy-report-only']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not cache index page', async () => {
|
||||
const response = await request(app).get('/');
|
||||
expect(response.status).toBe(200);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue