diff --git a/packages/cli/package.json b/packages/cli/package.json index 325f54b07bec7..88c8c2d8673c4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -130,6 +130,7 @@ "formidable": "3.5.1", "google-timezones-json": "1.1.0", "handlebars": "4.7.8", + "helmet": "7.1.0", "infisical-node": "1.3.0", "inquirer": "7.3.3", "ioredis": "5.3.2", diff --git a/packages/cli/src/AbstractServer.ts b/packages/cli/src/AbstractServer.ts index 89f521a4f1c0d..cf622863a6ce4 100644 --- a/packages/cli/src/AbstractServer.ts +++ b/packages/cli/src/AbstractServer.ts @@ -32,7 +32,7 @@ export abstract class AbstractServer { protected externalHooks: ExternalHooks; - protected protocol: string; + protected protocol = config.getEnv('protocol'); protected sslKey: string; @@ -65,7 +65,6 @@ export abstract class AbstractServer { const proxyHops = config.getEnv('proxy_hops'); if (proxyHops > 0) this.app.set('trust proxy', proxyHops); - this.protocol = config.getEnv('protocol'); this.sslKey = config.getEnv('ssl_key'); this.sslCert = config.getEnv('ssl_cert'); diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 22229fc434f44..20304bec60dfd 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -9,6 +9,7 @@ import { access as fsAccess } from 'fs/promises'; import { promisify } from 'util'; import cookieParser from 'cookie-parser'; import express from 'express'; +import helmet from 'helmet'; import { engine as expressHandlebars } from 'express-handlebars'; import { type Class, InstanceSettings } from 'n8n-core'; import type { IN8nUISettings } from 'n8n-workflow'; @@ -366,6 +367,20 @@ export class Server extends AbstractServer { this.app.use('/icons/@:scope/:packageName/*/*.(svg|png)', serveIcons); this.app.use('/icons/:packageName/*/*.(svg|png)', serveIcons); + const isTLSEnabled = this.protocol === 'https' && !!(this.sslKey && this.sslCert); + const securityHeadersMiddleware = helmet({ + contentSecurityPolicy: false, + xFrameOptions: { action: 'sameorigin' }, + dnsPrefetchControl: false, + // This is only relevant for Internet-explorer, which we do not support + ieNoOpen: false, + // This is already disabled in AbstractServer + xPoweredBy: false, + // Enable HSTS headers only when n8n handles TLS. + // if n8n is behind a reverse-proxy, then these headers needs to be configured there + strictTransportSecurity: isTLSEnabled, + }); + // Route all UI urls to index.html to support history-api const nonUIRoutes: Readonly = [ 'assets', @@ -390,7 +405,9 @@ export class Server extends AbstractServer { ) { req.url = '/index.html'; res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); - res.sendFile('index.html', { root: staticCacheDir, maxAge, lastModified: true }); + securityHeadersMiddleware(req, res, () => { + res.sendFile('index.html', { root: staticCacheDir, maxAge, lastModified: true }); + }); } else { next(); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea0285ffbdc70..2053b57dd28d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -549,6 +549,9 @@ importers: handlebars: specifier: 4.7.8 version: 4.7.8 + helmet: + specifier: 7.1.0 + version: 7.1.0 infisical-node: specifier: 1.3.0 version: 1.3.0 @@ -16132,6 +16135,11 @@ packages: resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} dev: false + /helmet@7.1.0: + resolution: {integrity: sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==} + engines: {node: '>=16.0.0'} + dev: false + /help-me@4.2.0: resolution: {integrity: sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==} dependencies: