mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
feat(core): Setup helmet.js for setting security headers (#9027)
This commit is contained in:
committed by
GitHub
parent
46e432b177
commit
0ed46711f4
@@ -6,20 +6,16 @@
|
||||
import { Container, Service } from 'typedi';
|
||||
import { exec as callbackExec } from 'child_process';
|
||||
import { access as fsAccess } from 'fs/promises';
|
||||
import { join as pathJoin } from 'path';
|
||||
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 { ServeStaticOptions } from 'serve-static';
|
||||
|
||||
import { type Class, InstanceSettings } from 'n8n-core';
|
||||
|
||||
import type { IN8nUISettings } from 'n8n-workflow';
|
||||
|
||||
// @ts-ignore
|
||||
import timezones from 'google-timezones-json';
|
||||
import history from 'connect-history-api-fallback';
|
||||
|
||||
import config from '@/config';
|
||||
import { Queue } from '@/Queue';
|
||||
@@ -31,6 +27,7 @@ import {
|
||||
inE2ETests,
|
||||
N8N_VERSION,
|
||||
TEMPLATES_DIR,
|
||||
Time,
|
||||
} from '@/constants';
|
||||
import { CredentialsController } from '@/credentials/credentials.controller';
|
||||
import type { APIRequest, CurlHelper } from '@/requests';
|
||||
@@ -248,30 +245,6 @@ export class Server extends AbstractServer {
|
||||
const { restEndpoint, app } = this;
|
||||
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') {
|
||||
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);
|
||||
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) => {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { scope, packageName } = req.params;
|
||||
@@ -402,7 +366,7 @@ export class Server extends AbstractServer {
|
||||
if (filePath) {
|
||||
try {
|
||||
await fsAccess(filePath);
|
||||
return res.sendFile(filePath);
|
||||
return res.sendFile(filePath, cacheOptions);
|
||||
} catch {}
|
||||
}
|
||||
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/: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(
|
||||
'/',
|
||||
express.static(staticCacheDir),
|
||||
express.static(EDITOR_UI_DIST_DIR, staticOptions),
|
||||
express.static(staticCacheDir, cacheOptions),
|
||||
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 {
|
||||
this.app.use('/', express.static(staticCacheDir));
|
||||
this.app.use('/', express.static(staticCacheDir, cacheOptions));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user