diff --git a/README.md b/README.md index 0a8d789b41..7c88d99919 100644 --- a/README.md +++ b/README.md @@ -87,30 +87,30 @@ Find answers to commonly asked questions about self-hosting Ghostfolio in our [F ### Supported Environment Variables -| Name | Type | Default Value | Description | -| --------------------------------- | --------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `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_PRO` | `string` (optional) | | The _CoinGecko_ Pro API key | -| `DATABASE_URL` | `string` | | The database connection URL. If using a connection pooler, use the pooled connection URL here. e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}` | -| `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_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 | -| `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) | -| `LOG_LEVELS` | `string[]` (optional) | | The logging levels for the Ghostfolio application, e.g. `["debug","error","log","warn"]` | -| `PORT` | `number` (optional) | `3333` | The port where the Ghostfolio application will run on | -| `POSTGRES_DB` | `string` | | The name of the _PostgreSQL_ database | -| `POSTGRES_PASSWORD` | `string` | | The password of the _PostgreSQL_ database | -| `POSTGRES_USER` | `string` | | The user of the _PostgreSQL_ database | -| `REDIS_DB` | `number` (optional) | `0` | The database index of _Redis_ | -| `REDIS_HOST` | `string` | | The host where _Redis_ is running | -| `REDIS_PASSWORD` | `string` | | The password of _Redis_ | -| `REDIS_PORT` | `number` | | The port where _Redis_ is running | -| `REQUEST_TIMEOUT` | `number` (optional) | `2000` | The timeout of network requests to data providers in milliseconds | -| `ROOT_URL` | `string` (optional) | `http://0.0.0.0:3333` | The root URL of the Ghostfolio application, used for generating callback URLs and external links. | -| `TRUST_PROXY` | `string` (optional) | | The [trust proxy](https://expressjs.com/en/guide/behind-proxies.html) setting of _Express.js_ to determine the client IP address for rate limiting, e.g. `1` if the Ghostfolio application runs behind a single reverse proxy | +| Name | Type | Default Value | Description | +| --------------------------------- | --------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `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_PRO` | `string` (optional) | | The _CoinGecko_ Pro API key | +| `DATABASE_URL` | `string` | | The database connection URL. If using a connection pooler, use the pooled connection URL here. e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}` | +| `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_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). 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 | +| `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"]` | +| `PORT` | `number` (optional) | `3333` | The port where the Ghostfolio application will run on | +| `POSTGRES_DB` | `string` | | The name of the _PostgreSQL_ database | +| `POSTGRES_PASSWORD` | `string` | | The password of the _PostgreSQL_ database | +| `POSTGRES_USER` | `string` | | The user of the _PostgreSQL_ database | +| `REDIS_DB` | `number` (optional) | `0` | The database index of _Redis_ | +| `REDIS_HOST` | `string` | | The host where _Redis_ is running | +| `REDIS_PASSWORD` | `string` | | The password of _Redis_ | +| `REDIS_PORT` | `number` | | The port where _Redis_ is running | +| `REQUEST_TIMEOUT` | `number` (optional) | `2000` | The timeout of network requests to data providers in milliseconds | +| `ROOT_URL` | `string` (optional) | `http://0.0.0.0:3333` | The root URL of the Ghostfolio application, used for generating callback URLs and external links. | +| `TRUST_PROXY` | `string` (optional) | | The [trust proxy](https://expressjs.com/en/guide/behind-proxies.html) setting of _Express.js_ to determine the client IP address for rate limiting, e.g. `1` if the Ghostfolio application runs behind a single reverse proxy | #### OpenID Connect OIDC (experimental) diff --git a/apps/api/src/helper/security-headers.helper.spec.ts b/apps/api/src/helper/security-headers.helper.spec.ts index f08c4366e4..1d5f0ac03f 100644 --- a/apps/api/src/helper/security-headers.helper.spec.ts +++ b/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 - * options of the helper. + * options of the helper, and the function which it calls as next middleware. */ function getHeaders({ isSubscriptionEnabled @@ -13,6 +13,7 @@ function getHeaders({ isSubscriptionEnabled: boolean; }) { const headers = new Map(); + const next = jest.fn(); const response = { removeHeader: (name: string) => { @@ -26,15 +27,24 @@ function getHeaders({ helmet(getHelmetOptions({ isSubscriptionEnabled }))( {} as IncomingMessage, response as unknown as ServerResponse, - jest.fn() + next ); - return headers; + return { headers, next }; } describe('getHelmetOptions', () => { describe('without the subscription', () => { - const headers = getHeaders({ isSubscriptionEnabled: false }); + let headers: Map; + 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', () => { expect(headers.get('content-security-policy')).toContain( @@ -52,6 +62,10 @@ describe('getHelmetOptions', () => { 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', () => { expect(headers.get('content-security-policy')).not.toContain( 'https://js.stripe.com' @@ -60,7 +74,16 @@ describe('getHelmetOptions', () => { }); describe('with the subscription', () => { - const headers = getHeaders({ isSubscriptionEnabled: true }); + let headers: Map; + 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', () => { expect(headers.get('content-security-policy')).toContain(