fix(backend): make csp configurable to unbreak app-backend served apps not being able to fetch

This commit is contained in:
Fredrik Adelöw
2020-09-25 12:14:52 +02:00
parent d7c2adae86
commit ad90193d2a
6 changed files with 88 additions and 9 deletions
+2
View File
@@ -9,3 +9,5 @@ backend:
origin: http://localhost:3000
methods: [GET, POST, PUT, DELETE]
credentials: true
csp:
connect-src: ["'self'", 'http:', 'https:']
+2
View File
@@ -9,6 +9,8 @@ backend:
database:
client: sqlite3
connection: ':memory:'
csp:
connect-src: ["'self'", 'https:']
# See README.md in the proxy-backend plugin for information on the configuration format
proxy:
@@ -31,10 +31,12 @@ import {
} from '../../middleware';
import { ServiceBuilder } from '../types';
import {
CspOptions,
HttpsSettings,
readBaseOptions,
readCorsOptions,
readCspOptions,
readHttpsSettings,
HttpsSettings,
} from './config';
import { createHttpServer, createHttpsServer } from './hostFactory';
import { metricsHandler } from './metrics';
@@ -42,12 +44,27 @@ import { metricsHandler } from './metrics';
const DEFAULT_PORT = 7000;
// '' is express default, which listens to all interfaces
const DEFAULT_HOST = '';
// taken from the helmet source code - don't seem to be exported
const DEFAULT_CSP = {
'default-src': ["'self'"],
'base-uri': ["'self'"],
'block-all-mixed-content': [],
'font-src': ["'self'", 'https:', 'data:'],
'frame-ancestors': ["'self'"],
'img-src': ["'self'", 'data:'],
'object-src': ["'none'"],
'script-src': ["'self'"],
'script-src-attr': ["'none'"],
'style-src': ["'self'", 'https:', "'unsafe-inline'"],
'upgrade-insecure-requests': [],
};
export class ServiceBuilderImpl implements ServiceBuilder {
private port: number | undefined;
private host: string | undefined;
private logger: Logger | undefined;
private corsOptions: cors.CorsOptions | undefined;
private cspOptions: CspOptions | undefined;
private httpsSettings: HttpsSettings | undefined;
private enableMetrics: boolean = true;
private routers: [string, Router][];
@@ -79,6 +96,11 @@ export class ServiceBuilderImpl implements ServiceBuilder {
this.corsOptions = corsOptions;
}
const cspOptions = readCspOptions(backendConfig);
if (cspOptions) {
this.cspOptions = cspOptions;
}
const httpsSettings = readHttpsSettings(backendConfig);
if (httpsSettings) {
this.httpsSettings = httpsSettings;
@@ -115,6 +137,11 @@ export class ServiceBuilderImpl implements ServiceBuilder {
return this;
}
setCsp(options: CspOptions): ServiceBuilder {
this.cspOptions = options;
return this;
}
addRouter(root: string, router: Router): ServiceBuilder {
this.routers.push([root, router]);
return this;
@@ -127,10 +154,20 @@ export class ServiceBuilderImpl implements ServiceBuilder {
host,
logger,
corsOptions,
cspOptions,
httpsSettings,
} = this.getOptions();
app.use(helmet());
app.use(
helmet({
contentSecurityPolicy: {
directives: {
...DEFAULT_CSP,
...cspOptions,
},
},
}),
);
if (corsOptions) {
app.use(cors(corsOptions));
}
@@ -177,6 +214,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
host: string;
logger: Logger;
corsOptions?: cors.CorsOptions;
cspOptions?: CspOptions;
httpsSettings?: HttpsSettings;
} {
return {
@@ -184,6 +222,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
host: this.host ?? DEFAULT_HOST,
logger: this.logger ?? getRootLogger(),
corsOptions: this.corsOptions,
cspOptions: this.cspOptions,
httpsSettings: this.httpsSettings,
};
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { Config } from '@backstage/config';
import { CorsOptions } from 'cors';
export type BaseOptions = {
@@ -57,6 +57,13 @@ export type CertificateAttributes = {
commonName?: string;
};
/**
* A map from CSP directive names to their values.
*
* Added here since helmet doesn't export this type publicly.
*/
export type CspOptions = Record<string, string[]>;
/**
* Reads some base options out of a config object.
*
@@ -71,7 +78,7 @@ export type CertificateAttributes = {
* }
* ```
*/
export function readBaseOptions(config: ConfigReader): BaseOptions {
export function readBaseOptions(config: Config): BaseOptions {
if (typeof config.get('listen') === 'string') {
// TODO(freben): Expand this to support more addresses and perhaps optional
const { host, port } = parseListenAddress(config.getString('listen'));
@@ -105,7 +112,7 @@ export function readBaseOptions(config: ConfigReader): BaseOptions {
* }
* ```
*/
export function readCorsOptions(config: ConfigReader): CorsOptions | undefined {
export function readCorsOptions(config: Config): CorsOptions | undefined {
const cc = config.getOptionalConfig('cors');
if (!cc) {
return undefined;
@@ -123,6 +130,33 @@ export function readCorsOptions(config: ConfigReader): CorsOptions | undefined {
});
}
/**
* Attempts to read a CSP options object from the root of a config object.
*
* @param config The root of a backend config object
* @returns A CSP options object, or undefined if not specified
*
* @example
* ```yaml
* backend:
* csp:
* connect-src: ["'self'", 'http:', 'https:']
* ```
*/
export function readCspOptions(config: Config): CspOptions | undefined {
const cc = config.getOptionalConfig('csp');
if (!cc) {
return undefined;
}
const result: CspOptions = {};
for (const key of cc.keys()) {
result[key] = cc.getStringArray(key);
}
return result;
}
/**
* Attempts to read a https settings object from the root of a config object.
*
@@ -138,9 +172,7 @@ export function readCorsOptions(config: ConfigReader): CorsOptions | undefined {
* }
* ```
*/
export function readHttpsSettings(
config: ConfigReader,
): HttpsSettings | undefined {
export function readHttpsSettings(config: Config): HttpsSettings | undefined {
const cc = config.getOptionalConfig('https');
if (!cc) {
@@ -157,7 +189,7 @@ export function readHttpsSettings(
}
function getOptionalStringOrStrings(
config: ConfigReader,
config: Config,
key: string,
): string | string[] | undefined {
const value = config.getOptional(key);
@@ -9,3 +9,5 @@ backend:
origin: http://localhost:3000
methods: [GET, POST, PUT, DELETE]
credentials: true
csp:
connect-src: ["'self'", 'http:', 'https:']
@@ -9,6 +9,8 @@ backend:
baseUrl: http://localhost:7000
listen:
port: 7000
csp:
connect-src: ["'self'", 'https:']
{{#if dbTypeSqlite}}
database:
client: sqlite3