From c268ab3198f8fd6b42004b5de28393b04b9dc5f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:23:13 +0000 Subject: [PATCH 1/3] Initial plan From 777bb3b33de31c669571a096036411d940dce376 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:43:46 +0000 Subject: [PATCH 2/3] security: remove overly broad Lambda invoke permission, use timing-safe API key comparison, move esbuild to devDependencies Agent-Logs-Url: https://github.com/NHSDigital/nhs-notify-client-callbacks/sessions/da7486ea-77e8-4923-a92a-13e662923c0a Co-authored-by: RossBugginsNHS <78215796+RossBugginsNHS@users.noreply.github.com> --- .../callbacks/module_mock_webhook_lambda.tf | 7 ------- lambdas/client-transform-filter-lambda/package.json | 2 +- lambdas/mock-webhook-lambda/package.json | 4 ++-- lambdas/mock-webhook-lambda/src/index.ts | 11 ++++++++++- pnpm-lock.yaml | 12 ++++++------ 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/infrastructure/terraform/components/callbacks/module_mock_webhook_lambda.tf b/infrastructure/terraform/components/callbacks/module_mock_webhook_lambda.tf index b951351e..9557bb69 100644 --- a/infrastructure/terraform/components/callbacks/module_mock_webhook_lambda.tf +++ b/infrastructure/terraform/components/callbacks/module_mock_webhook_lambda.tf @@ -88,10 +88,3 @@ resource "aws_lambda_permission" "mock_webhook_function_url" { function_url_auth_type = "NONE" } -resource "aws_lambda_permission" "mock_webhook_function_invoke" { - count = var.deploy_mock_clients ? 1 : 0 - statement_id_prefix = "FunctionURLAllowInvokeAction" - action = "lambda:InvokeFunction" - function_name = module.mock_webhook_lambda[0].function_name - principal = "*" -} diff --git a/lambdas/client-transform-filter-lambda/package.json b/lambdas/client-transform-filter-lambda/package.json index 266911da..e3a2c7ef 100644 --- a/lambdas/client-transform-filter-lambda/package.json +++ b/lambdas/client-transform-filter-lambda/package.json @@ -6,7 +6,6 @@ "@nhs-notify-client-callbacks/models": "workspace:*", "aws-embedded-metrics": "catalog:app", "cloudevents": "catalog:app", - "esbuild": "catalog:tools", "p-map": "catalog:app", "zod": "catalog:app" }, @@ -15,6 +14,7 @@ "@types/aws-lambda": "catalog:tools", "@types/jest": "catalog:test", "@types/node": "catalog:tools", + "esbuild": "catalog:tools", "eslint": "catalog:lint", "jest": "catalog:test", "typescript": "catalog:tools" diff --git a/lambdas/mock-webhook-lambda/package.json b/lambdas/mock-webhook-lambda/package.json index 18d4b84f..00a0a1bd 100644 --- a/lambdas/mock-webhook-lambda/package.json +++ b/lambdas/mock-webhook-lambda/package.json @@ -1,14 +1,14 @@ { "dependencies": { "@nhs-notify-client-callbacks/logger": "workspace:*", - "@nhs-notify-client-callbacks/models": "workspace:*", - "esbuild": "catalog:tools" + "@nhs-notify-client-callbacks/models": "workspace:*" }, "devDependencies": { "@tsconfig/node22": "catalog:tools", "@types/aws-lambda": "catalog:tools", "@types/jest": "catalog:test", "@types/node": "catalog:tools", + "esbuild": "catalog:tools", "eslint": "catalog:lint", "jest": "catalog:test", "jest-html-reporter": "catalog:test", diff --git a/lambdas/mock-webhook-lambda/src/index.ts b/lambdas/mock-webhook-lambda/src/index.ts index 081ef3b9..15ed7264 100644 --- a/lambdas/mock-webhook-lambda/src/index.ts +++ b/lambdas/mock-webhook-lambda/src/index.ts @@ -1,3 +1,4 @@ +import { timingSafeEqual } from "node:crypto"; import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; import { Logger } from "@nhs-notify-client-callbacks/logger"; import type { ClientCallbackPayload } from "@nhs-notify-client-callbacks/models"; @@ -58,7 +59,15 @@ async function buildResponse( const expectedApiKey = process.env.API_KEY; const providedApiKey = headers["x-api-key"]; - if (!expectedApiKey || providedApiKey !== expectedApiKey) { + if ( + !expectedApiKey || + !providedApiKey || + expectedApiKey.length !== providedApiKey.length || + !timingSafeEqual( + Buffer.from(expectedApiKey), + Buffer.from(providedApiKey), + ) + ) { logger.error("Unauthorized: invalid or missing x-api-key"); return { statusCode: 401, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 753d95da..7ebf8557 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -279,9 +279,6 @@ importers: cloudevents: specifier: catalog:app version: 10.0.0 - esbuild: - specifier: catalog:tools - version: 0.28.0 p-map: specifier: catalog:app version: 4.0.0 @@ -301,6 +298,9 @@ importers: '@types/node': specifier: catalog:tools version: 24.12.0 + esbuild: + specifier: catalog:tools + version: 0.28.0 eslint: specifier: catalog:lint version: 9.39.4(jiti@2.6.1) @@ -319,9 +319,6 @@ importers: '@nhs-notify-client-callbacks/models': specifier: workspace:* version: link:../../src/models - esbuild: - specifier: catalog:tools - version: 0.28.0 devDependencies: '@tsconfig/node22': specifier: catalog:tools @@ -335,6 +332,9 @@ importers: '@types/node': specifier: catalog:tools version: 24.12.0 + esbuild: + specifier: catalog:tools + version: 0.28.0 eslint: specifier: catalog:lint version: 9.39.4(jiti@2.6.1) From 25217d903d12ae7c4ed18732db44948127bc5bf6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:45:42 +0000 Subject: [PATCH 3/3] security: improve timing-safe comparison to use hash-based approach for constant-time comparison regardless of key length Agent-Logs-Url: https://github.com/NHSDigital/nhs-notify-client-callbacks/sessions/da7486ea-77e8-4923-a92a-13e662923c0a Co-authored-by: RossBugginsNHS <78215796+RossBugginsNHS@users.noreply.github.com> --- lambdas/mock-webhook-lambda/src/index.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lambdas/mock-webhook-lambda/src/index.ts b/lambdas/mock-webhook-lambda/src/index.ts index 15ed7264..68d41231 100644 --- a/lambdas/mock-webhook-lambda/src/index.ts +++ b/lambdas/mock-webhook-lambda/src/index.ts @@ -1,4 +1,4 @@ -import { timingSafeEqual } from "node:crypto"; +import { createHash, timingSafeEqual } from "node:crypto"; import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; import { Logger } from "@nhs-notify-client-callbacks/logger"; import type { ClientCallbackPayload } from "@nhs-notify-client-callbacks/models"; @@ -62,10 +62,9 @@ async function buildResponse( if ( !expectedApiKey || !providedApiKey || - expectedApiKey.length !== providedApiKey.length || !timingSafeEqual( - Buffer.from(expectedApiKey), - Buffer.from(providedApiKey), + createHash("sha256").update(expectedApiKey).digest(), + createHash("sha256").update(providedApiKey).digest(), ) ) { logger.error("Unauthorized: invalid or missing x-api-key");