Browse Source

Expose ENABLE_FEATURE_SECURITY_HEADERS

pull/8041/head
Thomas Kaul 2 weeks ago
parent
commit
6c0f18c3eb
  1. 4
      README.md
  2. 33
      apps/api/src/helper/security-headers.helper.spec.ts

4
README.md

@ -88,7 +88,7 @@ Find answers to commonly asked questions about self-hosting Ghostfolio in our [F
### Supported Environment Variables ### Supported Environment Variables
| Name | Type | Default Value | Description | | Name | Type | Default Value | Description |
| --------------------------------- | --------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | --------------------------------- | --------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ACCESS_TOKEN_SALT` | `string` | | A random string used as salt for access tokens | | `ACCESS_TOKEN_SALT` | `string` | | A random string used as salt for access tokens |
| `API_KEY_COINGECKO_DEMO` | `string` (optional) |   | The _CoinGecko_ Demo API key | | `API_KEY_COINGECKO_DEMO` | `string` (optional) |   | The _CoinGecko_ Demo API key |
| `API_KEY_COINGECKO_PRO` | `string` (optional) | | The _CoinGecko_ Pro API key | | `API_KEY_COINGECKO_PRO` | `string` (optional) | | The _CoinGecko_ Pro API key |
@ -96,7 +96,7 @@ Find answers to commonly asked questions about self-hosting Ghostfolio in our [F
| `DIRECT_URL` | `string` (optional) | | The direct database connection URL used by the _Prisma CLI_ (e.g. for schema migrations) and seeding, bypassing any connection poolers (falls back to `DATABASE_URL`) | | `DIRECT_URL` | `string` (optional) | | The direct database connection URL used by the _Prisma CLI_ (e.g. for schema migrations) and seeding, bypassing any connection poolers (falls back to `DATABASE_URL`) |
| `ENABLE_FEATURE_AUTH_TOKEN` | `boolean` (optional) | `true` | Enables authentication via security token | | `ENABLE_FEATURE_AUTH_TOKEN` | `boolean` (optional) | `true` | Enables authentication via security token |
| `ENABLE_FEATURE_MCP` | `boolean` (optional) | `false` | Enables the server of the _Model Context Protocol_ (MCP) at `/mcp` (experimental) | | `ENABLE_FEATURE_MCP` | `boolean` (optional) | `false` | Enables the server of the _Model Context Protocol_ (MCP) at `/mcp` (experimental) |
| `ENABLE_FEATURE_SECURITY_HEADERS` | `boolean` (optional) | `false` | Enables HTTP security headers, e.g. `Content-Security-Policy` and `X-Frame-Options` (experimental). `Strict-Transport-Security` is not set, because it requires HTTPS: set it in the reverse proxy | | `ENABLE_FEATURE_SECURITY_HEADERS` | `boolean` (optional) | `false` | Enables HTTP security headers, e.g. `Content-Security-Policy` and `X-Frame-Options` (experimental). Other origins cannot embed pages (e.g. via `iframe`) or resources of Ghostfolio. `Strict-Transport-Security` is not set, because it requires HTTPS: set it in the reverse proxy |
| `HOST` | `string` (optional) | `0.0.0.0` | The host where the Ghostfolio application will run on | | `HOST` | `string` (optional) | `0.0.0.0` | The host where the Ghostfolio application will run on |
| `JWT_SECRET_KEY` | `string` | | A random string used for _JSON Web Tokens_ (JWT) | | `JWT_SECRET_KEY` | `string` | | A random string used for _JSON Web Tokens_ (JWT) |
| `LOG_LEVELS` | `string[]` (optional) | | The logging levels for the Ghostfolio application, e.g. `["debug","error","log","warn"]` | | `LOG_LEVELS` | `string[]` (optional) | | The logging levels for the Ghostfolio application, e.g. `["debug","error","log","warn"]` |

33
apps/api/src/helper/security-headers.helper.spec.ts

@ -5,7 +5,7 @@ import { getHelmetOptions } from './security-headers.helper';
/** /**
* Gives the headers which the helmet middleware sets on a response with the * Gives the headers which the helmet middleware sets on a response with the
* options of the helper. * options of the helper, and the function which it calls as next middleware.
*/ */
function getHeaders({ function getHeaders({
isSubscriptionEnabled isSubscriptionEnabled
@ -13,6 +13,7 @@ function getHeaders({
isSubscriptionEnabled: boolean; isSubscriptionEnabled: boolean;
}) { }) {
const headers = new Map<string, string>(); const headers = new Map<string, string>();
const next = jest.fn();
const response = { const response = {
removeHeader: (name: string) => { removeHeader: (name: string) => {
@ -26,15 +27,24 @@ function getHeaders({
helmet(getHelmetOptions({ isSubscriptionEnabled }))( helmet(getHelmetOptions({ isSubscriptionEnabled }))(
{} as IncomingMessage, {} as IncomingMessage,
response as unknown as ServerResponse, response as unknown as ServerResponse,
jest.fn() next
); );
return headers; return { headers, next };
} }
describe('getHelmetOptions', () => { describe('getHelmetOptions', () => {
describe('without the subscription', () => { describe('without the subscription', () => {
const headers = getHeaders({ isSubscriptionEnabled: false }); let headers: Map<string, string>;
let next: jest.Mock;
beforeAll(() => {
({ headers, next } = getHeaders({ isSubscriptionEnabled: false }));
});
it('should call the next middleware without an error', () => {
expect(next).toHaveBeenCalledWith();
});
it('should set the Content-Security-Policy header', () => { it('should set the Content-Security-Policy header', () => {
expect(headers.get('content-security-policy')).toContain( expect(headers.get('content-security-policy')).toContain(
@ -52,6 +62,10 @@ describe('getHelmetOptions', () => {
expect(headers.has('strict-transport-security')).toBe(false); expect(headers.has('strict-transport-security')).toBe(false);
}); });
it('should not set the Cross-Origin-Opener-Policy header', () => {
expect(headers.has('cross-origin-opener-policy')).toBe(false);
});
it('should not allow resources of Stripe', () => { it('should not allow resources of Stripe', () => {
expect(headers.get('content-security-policy')).not.toContain( expect(headers.get('content-security-policy')).not.toContain(
'https://js.stripe.com' 'https://js.stripe.com'
@ -60,7 +74,16 @@ describe('getHelmetOptions', () => {
}); });
describe('with the subscription', () => { describe('with the subscription', () => {
const headers = getHeaders({ isSubscriptionEnabled: true }); let headers: Map<string, string>;
let next: jest.Mock;
beforeAll(() => {
({ headers, next } = getHeaders({ isSubscriptionEnabled: true }));
});
it('should call the next middleware without an error', () => {
expect(next).toHaveBeenCalledWith();
});
it('should upgrade insecure requests', () => { it('should upgrade insecure requests', () => {
expect(headers.get('content-security-policy')).toContain( expect(headers.get('content-security-policy')).toContain(

Loading…
Cancel
Save