mirror of https://github.com/ghostfolio/ghostfolio
6 changed files with 161 additions and 38 deletions
@ -0,0 +1,81 @@ |
|||||
|
import helmet from 'helmet'; |
||||
|
import { IncomingMessage, ServerResponse } from 'node:http'; |
||||
|
|
||||
|
import { getHelmetOptions } from './security-headers.helper'; |
||||
|
|
||||
|
/** |
||||
|
* Gives the headers which the helmet middleware sets on a response with the |
||||
|
* options of the helper. |
||||
|
*/ |
||||
|
function getHeaders({ |
||||
|
isSubscriptionEnabled |
||||
|
}: { |
||||
|
isSubscriptionEnabled: boolean; |
||||
|
}) { |
||||
|
const headers = new Map<string, string>(); |
||||
|
|
||||
|
const response = { |
||||
|
removeHeader: (name: string) => { |
||||
|
headers.delete(name.toLowerCase()); |
||||
|
}, |
||||
|
setHeader: (name: string, value: string) => { |
||||
|
headers.set(name.toLowerCase(), value); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
helmet(getHelmetOptions({ isSubscriptionEnabled }))( |
||||
|
{} as IncomingMessage, |
||||
|
response as unknown as ServerResponse, |
||||
|
jest.fn() |
||||
|
); |
||||
|
|
||||
|
return headers; |
||||
|
} |
||||
|
|
||||
|
describe('getHelmetOptions', () => { |
||||
|
describe('without the subscription', () => { |
||||
|
const headers = getHeaders({ isSubscriptionEnabled: false }); |
||||
|
|
||||
|
it('should set the Content-Security-Policy header', () => { |
||||
|
expect(headers.get('content-security-policy')).toContain( |
||||
|
"default-src 'self'" |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
it('should not upgrade insecure requests', () => { |
||||
|
expect(headers.get('content-security-policy')).not.toContain( |
||||
|
'upgrade-insecure-requests' |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
it('should not set the Strict-Transport-Security header', () => { |
||||
|
expect(headers.has('strict-transport-security')).toBe(false); |
||||
|
}); |
||||
|
|
||||
|
it('should not allow resources of Stripe', () => { |
||||
|
expect(headers.get('content-security-policy')).not.toContain( |
||||
|
'https://js.stripe.com' |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('with the subscription', () => { |
||||
|
const headers = getHeaders({ isSubscriptionEnabled: true }); |
||||
|
|
||||
|
it('should upgrade insecure requests', () => { |
||||
|
expect(headers.get('content-security-policy')).toContain( |
||||
|
'upgrade-insecure-requests' |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
it('should set the Strict-Transport-Security header', () => { |
||||
|
expect(headers.has('strict-transport-security')).toBe(true); |
||||
|
}); |
||||
|
|
||||
|
it('should allow resources of Stripe', () => { |
||||
|
expect(headers.get('content-security-policy')).toContain( |
||||
|
'https://js.stripe.com' |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,37 @@ |
|||||
|
import { HelmetOptions } from 'helmet'; |
||||
|
|
||||
|
export function getHelmetOptions({ |
||||
|
isSubscriptionEnabled |
||||
|
}: { |
||||
|
isSubscriptionEnabled: boolean; |
||||
|
}): HelmetOptions { |
||||
|
if (isSubscriptionEnabled) { |
||||
|
return { |
||||
|
contentSecurityPolicy: { |
||||
|
directives: { |
||||
|
connectSrc: ["'self'", 'https://js.stripe.com'], // Allow connections to Stripe
|
||||
|
frameSrc: ["'self'", 'https://js.stripe.com'], // Allow loading frames from Stripe
|
||||
|
scriptSrc: ["'self'", "'unsafe-inline'", 'https://js.stripe.com'], // Allow inline scripts and scripts from Stripe
|
||||
|
scriptSrcAttr: ["'self'", "'unsafe-inline'"], // Allow inline event handlers
|
||||
|
styleSrc: ["'self'", "'unsafe-inline'"] // Allow inline styles
|
||||
|
} |
||||
|
}, |
||||
|
crossOriginOpenerPolicy: false // Disable Cross-Origin-Opener-Policy header (for Internet Identity)
|
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
// The self-hosted setup can run via HTTP, hence the headers which need HTTPS
|
||||
|
// are disabled (see https://github.com/ghostfolio/ghostfolio/issues/2102)
|
||||
|
return { |
||||
|
contentSecurityPolicy: { |
||||
|
directives: { |
||||
|
scriptSrc: ["'self'", "'unsafe-inline'"], // Allow inline scripts
|
||||
|
scriptSrcAttr: ["'self'", "'unsafe-inline'"], // Allow inline event handlers
|
||||
|
styleSrc: ["'self'", "'unsafe-inline'"], // Allow inline styles
|
||||
|
upgradeInsecureRequests: null // Disable upgrade-insecure-requests directive (the browser changes each request to HTTPS, which gives a blank page via HTTP)
|
||||
|
} |
||||
|
}, |
||||
|
crossOriginOpenerPolicy: false, // Disable Cross-Origin-Opener-Policy header (the browser ignores it via HTTP)
|
||||
|
strictTransportSecurity: false // Disable Strict-Transport-Security header (the reverse proxy sets it, if HTTPS is required)
|
||||
|
}; |
||||
|
} |
||||
Loading…
Reference in new issue