diff --git a/ci/terraform/build.tfvars b/ci/terraform/build.tfvars index a7b02b741..cb438849e 100644 --- a/ci/terraform/build.tfvars +++ b/ci/terraform/build.tfvars @@ -26,3 +26,5 @@ logging_endpoint_arns = [ orch_to_auth_signing_public_key = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENRdvNXHwk1TvrgFUsWXAE5oDTcPr\nCBp6HxbvYDLsqwNHiDFEzCwvbXKY2QQR/Rtel0o156CtU9k1lCZJGAsSIA==\n-----END PUBLIC KEY-----" orch_to_auth_client_id = "orchestrationAuth" orch_to_auth_audience = "https://signin.build.account.gov.uk/" + +frame_ancestors_form_actions_csp_headers = "1" diff --git a/ci/terraform/ecs.tf b/ci/terraform/ecs.tf index 8ab4c640f..8bb493754 100644 --- a/ci/terraform/ecs.tf +++ b/ci/terraform/ecs.tf @@ -168,6 +168,10 @@ locals { name = "CODE_ENTERED_WRONG_BLOCKED_MINUTES" value = var.code_entered_wrong_blocked_minutes }, + { + name = "FRAME_ANCESTORS_FORM_ACTIONS_CSP_HEADERS" + value = var.frame_ancestors_form_actions_csp_headers + }, ] mountPoints = [ diff --git a/ci/terraform/sandpit.tfvars b/ci/terraform/sandpit.tfvars index 3172dc067..6a19286e4 100644 --- a/ci/terraform/sandpit.tfvars +++ b/ci/terraform/sandpit.tfvars @@ -29,3 +29,5 @@ orch_to_auth_audience = "https://signin.sandpit.account.gov.uk/" logging_endpoint_arns = [ "arn:aws:logs:eu-west-2:885513274347:destination:csls_cw_logs_destination_prodpython" ] + +frame_ancestors_form_actions_csp_headers = "1" diff --git a/ci/terraform/variables.tf b/ci/terraform/variables.tf index 0b8fd6fb4..00a681ae1 100644 --- a/ci/terraform/variables.tf +++ b/ci/terraform/variables.tf @@ -251,3 +251,9 @@ variable "orch_to_auth_audience" { type = string default = "" } + +variable "frame_ancestors_form_actions_csp_headers" { + description = "When true, sets frame-ancestors and form-action CSP headers in reponses" + type = string + default = "0" +} diff --git a/src/app.ts b/src/app.ts index 56e0c4609..075cd8936 100644 --- a/src/app.ts +++ b/src/app.ts @@ -164,7 +164,7 @@ async function createApp(): Promise { ); app.use(i18nextMiddleware.handle(i18next)); - app.use(helmet(helmetConfiguration)); + app.use(helmet(helmetConfiguration())); const redisConfig = isProduction ? await getRedisConfig(getAppEnv()) diff --git a/src/config.ts b/src/config.ts index 58951cfa8..511bfef93 100644 --- a/src/config.ts +++ b/src/config.ts @@ -191,3 +191,7 @@ export function getPasswordResetCodeEnteredWrongBlockDurationInMinutes(): number Number(process.env.PASSWORD_RESET_CODE_ENTERED_WRONG_BLOCKED_MINUTES) || 15 ); } + +export function supportFrameAncestorsFormActionsCspHeaders(): boolean { + return process.env.FRAME_ANCESTORS_FORM_ACTIONS_CSP_HEADERS === "1"; +} diff --git a/src/config/helmet.ts b/src/config/helmet.ts index 773423e52..9cb6bbaa2 100644 --- a/src/config/helmet.ts +++ b/src/config/helmet.ts @@ -1,43 +1,59 @@ import helmet from "helmet"; import { Request, Response } from "express"; +import { supportFrameAncestorsFormActionsCspHeaders } from "../config"; // Helmet does not export the config type - This is the way the recommend getting it on GitHub. -export const helmetConfiguration: Parameters[0] = { - contentSecurityPolicy: { - directives: { - defaultSrc: ["'self'"], - styleSrc: ["'self'"], - scriptSrc: [ - "'self'", - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - (req: Request, res: Response): string => - `'nonce-${res.locals.scriptNonce}'`, - "'sha256-+6WnXIl4mbFTCARd8N3COQmT3bJJmo32N8q8ZSQAIcU='", - "https://www.googletagmanager.com", - "https://www.google-analytics.com", - "https://ssl.google-analytics.com", - ], - imgSrc: [ - "'self'", - "data:", - "https://www.googletagmanager.com", - "https://www.google-analytics.com", - ], - objectSrc: ["'none'"], - connectSrc: ["'self'", "https://www.google-analytics.com"], +export function helmetConfiguration(): Parameters[0] { + const helmetConfig = { + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'"], + scriptSrc: [ + "'self'", + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + (req: Request, res: Response): string => + `'nonce-${res.locals.scriptNonce}'`, + "'sha256-+6WnXIl4mbFTCARd8N3COQmT3bJJmo32N8q8ZSQAIcU='", + "https://www.googletagmanager.com", + "https://www.google-analytics.com", + "https://ssl.google-analytics.com", + ], + imgSrc: [ + "'self'", + "data:", + "https://www.googletagmanager.com", + "https://www.google-analytics.com", + ], + objectSrc: ["'none'"], + connectSrc: ["'self'", "https://www.google-analytics.com"], + "frame-ancestors": [""], + "form-action": [""], + }, }, - }, - dnsPrefetchControl: { - allow: false, - }, - frameguard: { - action: "deny", - }, - hsts: { - maxAge: 31536000, // 1 Year - preload: true, - includeSubDomains: true, - }, - referrerPolicy: false, - permittedCrossDomainPolicies: false, - expectCt: false, -}; + dnsPrefetchControl: { + allow: false, + }, + expectCt: false, + frameguard: { + action: "deny", + }, + hsts: { + maxAge: 31536000, // 1 Year + preload: true, + includeSubDomains: true, + }, + permittedCrossDomainPolicies: false, + referrerPolicy: false, + }; + if (supportFrameAncestorsFormActionsCspHeaders()) { + helmetConfig.contentSecurityPolicy.directives["frame-ancestors"] = [ + "'self'", + "https://*.account.gov.uk", + ]; + helmetConfig.contentSecurityPolicy.directives["form-action"] = [ + "'self'", + "https://*.account.gov.uk", + ]; + } + return helmetConfig; +}