mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(core): Setup helmet.js for setting security headers (#9027)
This commit is contained in:
parent
46e432b177
commit
0ed46711f4
|
@ -65,7 +65,6 @@
|
||||||
"@types/basic-auth": "^1.1.3",
|
"@types/basic-auth": "^1.1.3",
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@types/compression": "1.0.1",
|
"@types/compression": "1.0.1",
|
||||||
"@types/connect-history-api-fallback": "^1.3.1",
|
|
||||||
"@types/convict": "^6.1.1",
|
"@types/convict": "^6.1.1",
|
||||||
"@types/cookie-parser": "^1.4.2",
|
"@types/cookie-parser": "^1.4.2",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
@ -115,7 +114,6 @@
|
||||||
"class-transformer": "0.5.1",
|
"class-transformer": "0.5.1",
|
||||||
"class-validator": "0.14.0",
|
"class-validator": "0.14.0",
|
||||||
"compression": "1.7.4",
|
"compression": "1.7.4",
|
||||||
"connect-history-api-fallback": "1.6.0",
|
|
||||||
"convict": "6.2.4",
|
"convict": "6.2.4",
|
||||||
"cookie-parser": "1.4.6",
|
"cookie-parser": "1.4.6",
|
||||||
"csrf": "3.1.0",
|
"csrf": "3.1.0",
|
||||||
|
@ -132,6 +130,7 @@
|
||||||
"formidable": "3.5.1",
|
"formidable": "3.5.1",
|
||||||
"google-timezones-json": "1.1.0",
|
"google-timezones-json": "1.1.0",
|
||||||
"handlebars": "4.7.8",
|
"handlebars": "4.7.8",
|
||||||
|
"helmet": "7.1.0",
|
||||||
"infisical-node": "1.3.0",
|
"infisical-node": "1.3.0",
|
||||||
"inquirer": "7.3.3",
|
"inquirer": "7.3.3",
|
||||||
"ioredis": "5.3.2",
|
"ioredis": "5.3.2",
|
||||||
|
|
|
@ -32,7 +32,7 @@ export abstract class AbstractServer {
|
||||||
|
|
||||||
protected externalHooks: ExternalHooks;
|
protected externalHooks: ExternalHooks;
|
||||||
|
|
||||||
protected protocol: string;
|
protected protocol = config.getEnv('protocol');
|
||||||
|
|
||||||
protected sslKey: string;
|
protected sslKey: string;
|
||||||
|
|
||||||
|
@ -65,7 +65,6 @@ export abstract class AbstractServer {
|
||||||
const proxyHops = config.getEnv('proxy_hops');
|
const proxyHops = config.getEnv('proxy_hops');
|
||||||
if (proxyHops > 0) this.app.set('trust proxy', proxyHops);
|
if (proxyHops > 0) this.app.set('trust proxy', proxyHops);
|
||||||
|
|
||||||
this.protocol = config.getEnv('protocol');
|
|
||||||
this.sslKey = config.getEnv('ssl_key');
|
this.sslKey = config.getEnv('ssl_key');
|
||||||
this.sslCert = config.getEnv('ssl_cert');
|
this.sslCert = config.getEnv('ssl_cert');
|
||||||
|
|
||||||
|
|
|
@ -6,20 +6,16 @@
|
||||||
import { Container, Service } from 'typedi';
|
import { Container, Service } from 'typedi';
|
||||||
import { exec as callbackExec } from 'child_process';
|
import { exec as callbackExec } from 'child_process';
|
||||||
import { access as fsAccess } from 'fs/promises';
|
import { access as fsAccess } from 'fs/promises';
|
||||||
import { join as pathJoin } from 'path';
|
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
import helmet from 'helmet';
|
||||||
import { engine as expressHandlebars } from 'express-handlebars';
|
import { engine as expressHandlebars } from 'express-handlebars';
|
||||||
import type { ServeStaticOptions } from 'serve-static';
|
|
||||||
|
|
||||||
import { type Class, InstanceSettings } from 'n8n-core';
|
import { type Class, InstanceSettings } from 'n8n-core';
|
||||||
|
|
||||||
import type { IN8nUISettings } from 'n8n-workflow';
|
import type { IN8nUISettings } from 'n8n-workflow';
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import timezones from 'google-timezones-json';
|
import timezones from 'google-timezones-json';
|
||||||
import history from 'connect-history-api-fallback';
|
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { Queue } from '@/Queue';
|
import { Queue } from '@/Queue';
|
||||||
|
@ -31,6 +27,7 @@ import {
|
||||||
inE2ETests,
|
inE2ETests,
|
||||||
N8N_VERSION,
|
N8N_VERSION,
|
||||||
TEMPLATES_DIR,
|
TEMPLATES_DIR,
|
||||||
|
Time,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { CredentialsController } from '@/credentials/credentials.controller';
|
import { CredentialsController } from '@/credentials/credentials.controller';
|
||||||
import type { APIRequest, CurlHelper } from '@/requests';
|
import type { APIRequest, CurlHelper } from '@/requests';
|
||||||
|
@ -248,30 +245,6 @@ export class Server extends AbstractServer {
|
||||||
const { restEndpoint, app } = this;
|
const { restEndpoint, app } = this;
|
||||||
setupPushHandler(restEndpoint, app);
|
setupPushHandler(restEndpoint, app);
|
||||||
|
|
||||||
const nonUIRoutes: Readonly<string[]> = [
|
|
||||||
'assets',
|
|
||||||
'healthz',
|
|
||||||
'metrics',
|
|
||||||
'e2e',
|
|
||||||
this.restEndpoint,
|
|
||||||
this.endpointPresetCredentials,
|
|
||||||
isApiEnabled() ? '' : publicApiEndpoint,
|
|
||||||
...config.getEnv('endpoints.additionalNonUIRoutes').split(':'),
|
|
||||||
].filter((u) => !!u);
|
|
||||||
const nonUIRoutesRegex = new RegExp(`^/(${nonUIRoutes.join('|')})/?.*$`);
|
|
||||||
|
|
||||||
// Make sure that Vue history mode works properly
|
|
||||||
this.app.use(
|
|
||||||
history({
|
|
||||||
rewrites: [
|
|
||||||
{
|
|
||||||
from: nonUIRoutesRegex,
|
|
||||||
to: ({ parsedUrl }) => parsedUrl.pathname!.toString(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (config.getEnv('executions.mode') === 'queue') {
|
if (config.getEnv('executions.mode') === 'queue') {
|
||||||
await Container.get(Queue).init();
|
await Container.get(Queue).init();
|
||||||
}
|
}
|
||||||
|
@ -381,19 +354,10 @@ export class Server extends AbstractServer {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxAge = Time.days.toMilliseconds;
|
||||||
|
const cacheOptions = inE2ETests ? {} : { maxAge };
|
||||||
const { staticCacheDir } = Container.get(InstanceSettings);
|
const { staticCacheDir } = Container.get(InstanceSettings);
|
||||||
if (frontendService) {
|
if (frontendService) {
|
||||||
const staticOptions: ServeStaticOptions = {
|
|
||||||
cacheControl: false,
|
|
||||||
setHeaders: (res: express.Response, path: string) => {
|
|
||||||
const isIndex = path === pathJoin(staticCacheDir, 'index.html');
|
|
||||||
const cacheControl = isIndex
|
|
||||||
? 'no-cache, no-store, must-revalidate'
|
|
||||||
: 'max-age=86400, immutable';
|
|
||||||
res.header('Cache-Control', cacheControl);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const serveIcons: express.RequestHandler = async (req, res) => {
|
const serveIcons: express.RequestHandler = async (req, res) => {
|
||||||
// eslint-disable-next-line prefer-const
|
// eslint-disable-next-line prefer-const
|
||||||
let { scope, packageName } = req.params;
|
let { scope, packageName } = req.params;
|
||||||
|
@ -402,7 +366,7 @@ export class Server extends AbstractServer {
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
try {
|
try {
|
||||||
await fsAccess(filePath);
|
await fsAccess(filePath);
|
||||||
return res.sendFile(filePath);
|
return res.sendFile(filePath, cacheOptions);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
res.sendStatus(404);
|
res.sendStatus(404);
|
||||||
|
@ -411,19 +375,68 @@ export class Server extends AbstractServer {
|
||||||
this.app.use('/icons/@:scope/:packageName/*/*.(svg|png)', serveIcons);
|
this.app.use('/icons/@:scope/:packageName/*/*.(svg|png)', serveIcons);
|
||||||
this.app.use('/icons/:packageName/*/*.(svg|png)', serveIcons);
|
this.app.use('/icons/:packageName/*/*.(svg|png)', serveIcons);
|
||||||
|
|
||||||
|
const isTLSEnabled = this.protocol === 'https' && !!(this.sslKey && this.sslCert);
|
||||||
|
const isPreviewMode = process.env.N8N_PREVIEW_MODE === 'true';
|
||||||
|
const securityHeadersMiddleware = helmet({
|
||||||
|
contentSecurityPolicy: false,
|
||||||
|
xFrameOptions: isPreviewMode || inE2ETests ? false : { 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
|
||||||
|
? {
|
||||||
|
maxAge: 180 * Time.days.toSeconds,
|
||||||
|
includeSubDomains: false,
|
||||||
|
preload: false,
|
||||||
|
}
|
||||||
|
: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route all UI urls to index.html to support history-api
|
||||||
|
const nonUIRoutes: Readonly<string[]> = [
|
||||||
|
'assets',
|
||||||
|
'types',
|
||||||
|
'healthz',
|
||||||
|
'metrics',
|
||||||
|
'e2e',
|
||||||
|
this.restEndpoint,
|
||||||
|
this.endpointPresetCredentials,
|
||||||
|
isApiEnabled() ? '' : publicApiEndpoint,
|
||||||
|
...config.getEnv('endpoints.additionalNonUIRoutes').split(':'),
|
||||||
|
].filter((u) => !!u);
|
||||||
|
const nonUIRoutesRegex = new RegExp(`^/(${nonUIRoutes.join('|')})/?.*$`);
|
||||||
|
const historyApiHandler: express.RequestHandler = (req, res, next) => {
|
||||||
|
const {
|
||||||
|
method,
|
||||||
|
headers: { accept },
|
||||||
|
} = req;
|
||||||
|
if (
|
||||||
|
method === 'GET' &&
|
||||||
|
accept &&
|
||||||
|
(accept.includes('text/html') || accept.includes('*/*')) &&
|
||||||
|
!nonUIRoutesRegex.test(req.path)
|
||||||
|
) {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
securityHeadersMiddleware(req, res, () => {
|
||||||
|
res.sendFile('index.html', { root: staticCacheDir, maxAge, lastModified: true });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.app.use(
|
this.app.use(
|
||||||
'/',
|
'/',
|
||||||
express.static(staticCacheDir),
|
express.static(staticCacheDir, cacheOptions),
|
||||||
express.static(EDITOR_UI_DIST_DIR, staticOptions),
|
express.static(EDITOR_UI_DIST_DIR, cacheOptions),
|
||||||
|
historyApiHandler,
|
||||||
);
|
);
|
||||||
|
|
||||||
const startTime = new Date().toUTCString();
|
|
||||||
this.app.use('/index.html', (req, res, next) => {
|
|
||||||
res.setHeader('Last-Modified', startTime);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.app.use('/', express.static(staticCacheDir));
|
this.app.use('/', express.static(staticCacheDir, cacheOptions));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -544,9 +544,6 @@ importers:
|
||||||
compression:
|
compression:
|
||||||
specifier: 1.7.4
|
specifier: 1.7.4
|
||||||
version: 1.7.4
|
version: 1.7.4
|
||||||
connect-history-api-fallback:
|
|
||||||
specifier: 1.6.0
|
|
||||||
version: 1.6.0
|
|
||||||
convict:
|
convict:
|
||||||
specifier: 6.2.4
|
specifier: 6.2.4
|
||||||
version: 6.2.4
|
version: 6.2.4
|
||||||
|
@ -595,6 +592,9 @@ importers:
|
||||||
handlebars:
|
handlebars:
|
||||||
specifier: 4.7.8
|
specifier: 4.7.8
|
||||||
version: 4.7.8
|
version: 4.7.8
|
||||||
|
helmet:
|
||||||
|
specifier: 7.1.0
|
||||||
|
version: 7.1.0
|
||||||
infisical-node:
|
infisical-node:
|
||||||
specifier: 1.3.0
|
specifier: 1.3.0
|
||||||
version: 1.3.0
|
version: 1.3.0
|
||||||
|
@ -764,9 +764,6 @@ importers:
|
||||||
'@types/compression':
|
'@types/compression':
|
||||||
specifier: 1.0.1
|
specifier: 1.0.1
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
'@types/connect-history-api-fallback':
|
|
||||||
specifier: ^1.3.1
|
|
||||||
version: 1.3.5
|
|
||||||
'@types/convict':
|
'@types/convict':
|
||||||
specifier: ^6.1.1
|
specifier: ^6.1.1
|
||||||
version: 6.1.1
|
version: 6.1.1
|
||||||
|
@ -9555,13 +9552,6 @@ packages:
|
||||||
'@types/node': 18.16.16
|
'@types/node': 18.16.16
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/connect-history-api-fallback@1.3.5:
|
|
||||||
resolution: {integrity: sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==}
|
|
||||||
dependencies:
|
|
||||||
'@types/express-serve-static-core': 4.17.43
|
|
||||||
'@types/node': 18.16.16
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/connect@3.4.35:
|
/@types/connect@3.4.35:
|
||||||
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
|
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -13027,11 +13017,6 @@ packages:
|
||||||
resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==}
|
resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/connect-history-api-fallback@1.6.0:
|
|
||||||
resolution: {integrity: sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==}
|
|
||||||
engines: {node: '>=0.8'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/consola@3.2.3:
|
/consola@3.2.3:
|
||||||
resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==}
|
resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==}
|
||||||
engines: {node: ^14.18.0 || >=16.10.0}
|
engines: {node: ^14.18.0 || >=16.10.0}
|
||||||
|
@ -16404,6 +16389,11 @@ packages:
|
||||||
resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==}
|
resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==}
|
||||||
dev: false
|
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:
|
/help-me@4.2.0:
|
||||||
resolution: {integrity: sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==}
|
resolution: {integrity: sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
Loading…
Reference in a new issue