sync: update from private repo (dd02d18)
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync
2026-06-16 06:24:20 +00:00
parent d6ce1ff1a3
commit cbdfdeaa18
9 changed files with 161 additions and 23 deletions
+58
View File
@@ -0,0 +1,58 @@
import { describe, expect, it, vi } from 'vitest';
import {
applySecurityHeaders,
securityHeadersMiddleware,
HSTS_MAX_AGE_SECONDS,
} from './security-headers.js';
import { type Request, type Response } from 'express';
function fakeRes() {
const headers: Record<string, string> = {};
const res = {
setHeader(name: string, value: string) {
headers[name] = value;
},
} as unknown as Response;
return { res, headers };
}
describe('applySecurityHeaders', () => {
it('sets the baseline hardening headers on every response', () => {
const { res, headers } = fakeRes();
applySecurityHeaders({ secure: false } as Request, res);
expect(headers['X-Content-Type-Options']).toBe('nosniff');
expect(headers['X-Frame-Options']).toBe('SAMEORIGIN');
expect(headers['Referrer-Policy']).toBe('strict-origin-when-cross-origin');
});
it('keeps X-Frame-Options at SAMEORIGIN so the app can serve its own iframes', () => {
// PDF preview and noVNC browser sessions embed same-origin iframes; DENY
// would break them.
const { res, headers } = fakeRes();
applySecurityHeaders({ secure: true } as Request, res);
expect(headers['X-Frame-Options']).not.toBe('DENY');
expect(headers['X-Frame-Options']).toBe('SAMEORIGIN');
});
it('emits HSTS only when the response is served over TLS', () => {
const plain = fakeRes();
applySecurityHeaders({ secure: false } as Request, plain.res);
expect(plain.headers['Strict-Transport-Security']).toBeUndefined();
const tls = fakeRes();
applySecurityHeaders({ secure: true } as Request, tls.res);
expect(tls.headers['Strict-Transport-Security']).toBe(
`max-age=${HSTS_MAX_AGE_SECONDS}; includeSubDomains`,
);
});
});
describe('securityHeadersMiddleware', () => {
it('applies the headers and calls next()', () => {
const { res, headers } = fakeRes();
const next = vi.fn();
securityHeadersMiddleware()({ secure: false } as Request, res, next);
expect(headers['X-Frame-Options']).toBe('SAMEORIGIN');
expect(next).toHaveBeenCalledOnce();
});
});
+45
View File
@@ -0,0 +1,45 @@
import { type Request, type Response, type NextFunction } from 'express';
/**
* Baseline security response headers applied to every response.
*
* These are deliberately conservative so they cannot break the SPA, which
* serves same-origin `<iframe>`s (PDF preview, noVNC browser sessions) and
* follows OAuth via top-level redirects rather than popups:
*
* - `X-Content-Type-Options: nosniff` — stop MIME-sniffing a response into
* something executable. (Untrusted workspace files layer `Content-Security-Policy:
* sandbox` on top via setUntrustedFileResponseHeaders.)
* - `X-Frame-Options: SAMEORIGIN` — block clickjacking from foreign origins
* while still allowing the app's own same-origin iframes.
* - `Referrer-Policy: strict-origin-when-cross-origin` — never leak the full
* path (which can carry task ids) to third-party origins.
* - `Strict-Transport-Security` — only when the response is served over TLS
* (native HTTPS, or a trusted proxy reporting `X-Forwarded-Proto: https`).
* Never emitted on plain-HTTP localhost dev so it can't pin a dev browser.
*
* Note: `req.secure` reflects `X-Forwarded-Proto` only when Express trusts the
* proxy. The server enables `trust proxy` together with `auth.secure_cookie`, so
* an operator terminating TLS at a reverse proxy should set `secure_cookie: true`
* to also get HSTS. Native HTTPS sets `req.secure` intrinsically and is unaffected.
*/
export const HSTS_MAX_AGE_SECONDS = 15552000; // 180 days
export function applySecurityHeaders(req: Request, res: Response): void {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
if (req.secure) {
res.setHeader(
'Strict-Transport-Security',
`max-age=${HSTS_MAX_AGE_SECONDS}; includeSubDomains`,
);
}
}
export function securityHeadersMiddleware() {
return (req: Request, res: Response, next: NextFunction): void => {
applySecurityHeaders(req, res);
next();
};
}
+6 -9
View File
@@ -29,6 +29,7 @@ import { mountAdminApi } from './admin-api.js';
import { createAdminGatewayApi } from './admin-gateway-api.js';
import { mountUsersApi } from './users-api.js';
import { mountShareApi } from './share-api.js';
import { securityHeadersMiddleware } from './security-headers.js';
import { mountLocalTasksApi } from './local-tasks-api.js';
import { findPieceFile } from './pieces-api.js';
import { mountLocalFilesApi } from './local-files-api.js';
@@ -191,15 +192,11 @@ export function createCoreServer(opts: CoreServerOptions): {
// Don't advertise the framework/version (minor info-leak, free to drop).
app.disable('x-powered-by');
// Baseline hardening header on every response. `nosniff` stops the browser
// from MIME-sniffing a response into something executable. File-serving
// endpoints add `Content-Security-Policy: sandbox` on top
// (see setUntrustedFileResponseHeaders) to neutralize stored XSS from
// agent/user-authored workspace files.
app.use((_req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
next();
});
// Baseline hardening headers on every response (nosniff, X-Frame-Options,
// Referrer-Policy, and HSTS over TLS). File-serving endpoints add
// `Content-Security-Policy: sandbox` on top (see setUntrustedFileResponseHeaders)
// to neutralize stored XSS from agent/user-authored workspace files.
app.use(securityHeadersMiddleware());
// リバースプロキシ背後で secure cookie / X-Forwarded-Proto を正しく処理
if (opts.authConfig?.secureCookie) {