fix(backend): make csp configurable to unbreak app-backend served apps not being able to fetch
This commit is contained in:
@@ -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:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user