0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

chore(core,cloud): add cloud route security headers (#3674)

* chore(core,cloud): add cloud route security headers

add cloud routes security headers

* chore(core,cloud): only allow ws on dev environment

only allow ws on dev environment

* refactor(core,cloud): update using helmetjs package

update using helmetjs package

* chore(core): fix comment

fix comment
This commit is contained in:
simeng-li 2023-04-10 10:23:07 +08:00 committed by GitHub
parent 7af8e9c9b1
commit 764d0dd5ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 152 additions and 24 deletions

View file

@ -38,6 +38,7 @@
"dotenv": "^16.0.0",
"fetch-retry": "^5.0.4",
"find-up": "^6.3.0",
"helmet": "^6.0.1",
"http-proxy": "^1.18.1",
"jose": "^4.11.0",
"mime-types": "^2.1.35",

View file

@ -17,6 +17,7 @@ const { default: withHttpProxy } = await import('./middleware/with-http-proxy.js
const { default: withPathname } = await import('./middleware/with-pathname.js');
const { default: withSpa } = await import('./middleware/with-spa.js');
const { default: withErrorReport } = await import('./middleware/with-error-report.js');
const { default: withSecurityHeaders } = await import('./middleware/with-security-headers.js');
const { EnvSet } = await import('./env-set/index.js');
const { default: router } = await import('./routes/index.js');
@ -30,6 +31,7 @@ const { listen } = createServer({
.and(withErrorReport())
.and(withRequest())
.and(anonymousRouter.routes())
.and(withSecurityHeaders())
.and(
withPathname(
'/api',

View file

@ -0,0 +1,110 @@
import type { IncomingMessage, ServerResponse } from 'node:http';
import { promisify } from 'node:util';
import type { NextFunction, HttpContext, RequestContext } from '@withtyped/server';
import helmet, { type HelmetOptions } from 'helmet';
import { EnvSet } from '#src/env-set/index.js';
/**
* Apply security headers to the response using helmet
* @see https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html for recommended headers
* @see https://helmetjs.github.io/ for more details
* @returns middleware
*/
const helmetPromise = async (
settings: HelmetOptions,
request: IncomingMessage,
response: ServerResponse
) =>
promisify((callback) => {
helmet(settings)(request, response, (error) => {
// Make TS happy
callback(error, null);
});
})();
export default function withSecurityHeaders<InputContext extends RequestContext>() {
const {
global: { adminUrlSet, cloudUrlSet, urlSet },
isProduction,
} = EnvSet;
const adminOrigins = adminUrlSet.origins;
const cloudOrigins = cloudUrlSet.origins;
const urlSetOrigins = urlSet.origins;
const developmentOrigins = isProduction ? [] : ['ws:'];
return async (
context: InputContext,
next: NextFunction<InputContext>,
{ response, request }: HttpContext
) => {
const requestPath = context.request.url.pathname;
/**
* Default Applied rules:
* - crossOriginOpenerPolicy: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#cross-origin-opener-policy-coop
* - crossOriginResourcePolicy: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#cross-origin-resource-policy-corp
* - crossOriginEmbedderPolicy: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#cross-origin-embedder-policy-coep
* - hidePoweredBy: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-powered-by
* - hsts: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#strict-transport-security-hsts
* - ieNoOpen: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-download-options
* - noSniff: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-content-type-options
* - permittedCrossDomainPolicies: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-permitted-cross-domain-policies
* - referrerPolicy: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#referrer-policy
* - xssFilter: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-xss-protection
* - originAgentCluster: https://whatpr.org/html/6214/origin.html#origin-keyed-agent-clusters
* - frameguard: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-frame-options
*/
const basicSecurityHeaderSettings: HelmetOptions = {
contentSecurityPolicy: false, // Exclusively set for console app only
expectCt: false, // Not recommended, will be deprecated by modern browsers
dnsPrefetchControl: false,
referrerPolicy: {
policy: 'strict-origin-when-cross-origin',
},
};
if (requestPath.startsWith('/api')) {
// FrameOptions: https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html#x-frame-options
await helmetPromise(basicSecurityHeaderSettings, request, response);
return next(context);
}
// For cloud console
// ContentSecurityPolicy: https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html
await helmetPromise(
{
...basicSecurityHeaderSettings,
frameguard: false,
contentSecurityPolicy: {
useDefaults: true,
// Temporary set to report only to avoid breaking the app
reportOnly: true,
directives: {
'upgrade-insecure-requests': null,
imgSrc: ["'self'", 'data:', 'https:'],
scriptSrc: ["'self'", "'unsafe-eval'", "'unsafe-inline'"],
connectSrc: [
"'self'",
...adminOrigins,
...cloudOrigins,
...urlSetOrigins,
...developmentOrigins,
],
frameSrc: ["'self'", ...urlSetOrigins],
},
},
},
request,
response
);
return next(context);
};
}

View file

@ -51,6 +51,7 @@
"find-up": "^6.3.0",
"got": "^12.5.3",
"hash-wasm": "^4.9.0",
"helmet": "^6.0.1",
"i18next": "^21.8.16",
"iconv-lite": "0.6.3",
"jose": "^4.11.0",
@ -59,7 +60,6 @@
"koa-body": "^5.0.0",
"koa-compose": "^4.1.0",
"koa-compress": "^5.1.0",
"koa-helmet": "^7.0.2",
"koa-logger": "^3.2.1",
"koa-mount": "^4.0.0",
"koa-proxies": "^0.12.1",

View file

@ -1,24 +1,35 @@
import { type IncomingMessage, type ServerResponse } from 'node:http';
import { promisify } from 'node:util';
import { defaultTenantId } from '@logto/schemas';
import helmet, { type HelmetOptions } from 'helmet';
import type { MiddlewareType } from 'koa';
import helmet from 'koa-helmet';
import { EnvSet, AdminApps, getTenantEndpoint } from '#src/env-set/index.js';
/**
* Apply security headers to the response using koa-helmet
* Apply security headers to the response using helmet
* @see https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html for recommended headers
* @see https://helmetjs.github.io/ for more details
* @returns koa middleware
*/
const helmetPromise = async (
settings: HelmetOptions,
request: IncomingMessage,
response: ServerResponse
) =>
promisify((callback) => {
helmet(settings)(request, response, (error) => {
// Make TS happy
callback(error, null);
});
})();
export default function koaSecurityHeaders<StateT, ContextT, ResponseBodyT>(
mountedApps: string[],
tenantId: string
): MiddlewareType<StateT, ContextT, ResponseBodyT> {
type Middleware = MiddlewareType<StateT, ContextT, ResponseBodyT>;
type HelmetOptions = Parameters<typeof helmet>[0];
const { isProduction, isCloud, isMultiTenancy, adminUrlSet, cloudUrlSet } = EnvSet.values;
const adminOrigins = adminUrlSet.origins;
@ -66,7 +77,9 @@ export default function koaSecurityHeaders<StateT, ContextT, ResponseBodyT>(
// Temporary set to report only to avoid breaking the app
reportOnly: true,
directives: {
'upgrade-insecure-requests': null,
imgSrc: ["'self'", 'data:', 'https:'],
scriptSrc: ["'self'", "'unsafe-eval'", "'unsafe-inline'"],
connectSrc: ["'self'", ...adminOrigins, ...cloudOrigins, ...developmentOrigins],
// WARNING: high risk Need to allow self hosted terms of use page loaded in an iframe
frameSrc: ["'self'", 'https:'],
@ -85,7 +98,9 @@ export default function koaSecurityHeaders<StateT, ContextT, ResponseBodyT>(
// Temporary set to report only to avoid breaking the app
reportOnly: true,
directives: {
'upgrade-insecure-requests': null,
imgSrc: ["'self'", 'data:', 'https:'],
scriptSrc: ["'self'", "'unsafe-eval'", "'unsafe-inline'"],
connectSrc: [
"'self'",
tenantEndpointOrigin,
@ -99,26 +114,30 @@ export default function koaSecurityHeaders<StateT, ContextT, ResponseBodyT>(
},
};
const buildHelmetMiddleware: (options: HelmetOptions) => Middleware = (options) =>
helmet(options);
return async (ctx, next) => {
const requestPath = ctx.request.path;
const { request, req, res } = ctx;
const requestPath = request.path;
// Admin Console
if (
requestPath.startsWith(`/${AdminApps.Console}`) ||
requestPath.startsWith(`/${AdminApps.Welcome}`)
) {
return buildHelmetMiddleware(consoleSecurityHeaderSettings)(ctx, next);
await helmetPromise(consoleSecurityHeaderSettings, req, res);
return next();
}
// Route has been handled by one of mounted apps
if (mountedApps.some((app) => app !== '' && requestPath.startsWith(`/${app}`))) {
return buildHelmetMiddleware(basicSecurityHeaderSettings)(ctx, next);
await helmetPromise(basicSecurityHeaderSettings, req, res);
return next();
}
// Main flow UI
return buildHelmetMiddleware(mainFlowUiSecurityHeaderSettings)(ctx, next);
await helmetPromise(mainFlowUiSecurityHeaderSettings, req, res);
return next();
};
}

View file

@ -262,6 +262,9 @@ importers:
find-up:
specifier: ^6.3.0
version: 6.3.0
helmet:
specifier: ^6.0.1
version: 6.0.1
http-proxy:
specifier: ^1.18.1
version: 1.18.1
@ -3080,6 +3083,9 @@ importers:
hash-wasm:
specifier: ^4.9.0
version: 4.9.0
helmet:
specifier: ^6.0.1
version: 6.0.1
i18next:
specifier: ^21.8.16
version: 21.8.16
@ -3104,9 +3110,6 @@ importers:
koa-compress:
specifier: ^5.1.0
version: 5.1.0
koa-helmet:
specifier: ^7.0.2
version: 7.0.2
koa-logger:
specifier: ^3.2.1
version: 3.2.1
@ -13730,13 +13733,6 @@ packages:
co: 4.6.0
koa-compose: 4.1.0
/koa-helmet@7.0.2:
resolution: {integrity: sha512-AvzS6VuEfFgbAm0mTUnkk/BpMarMcs5A56g+f0sfrJ6m63wII48d2GDrnUQGp0Nj+RR950vNtgqXm9UJSe7GOg==}
engines: {node: '>= 14.0.0'}
dependencies:
helmet: 6.0.1
dev: false
/koa-is-json@1.0.0:
resolution: {integrity: sha512-+97CtHAlWDx0ndt0J8y3P12EWLwTLMXIfMnYDev3wOTwH/RpBGMlfn4bDXlMEg1u73K6XRE9BbUp+5ZAYoRYWw==}
dev: false