From 06bd593b60646f78b6f2fad8a9e71ddaa1cea02e Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 3 Jul 2025 23:10:32 -0400 Subject: [PATCH] Verify requests are from Bitbucket using signing --- .../bitbucket-secret-scanning-factory.ts | 13 ++++++- .../src/server/plugins/secret-scanner-v2.ts | 37 +++++++++++++++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts index 8055cf5a0..598eda18b 100644 --- a/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts +++ b/backend/src/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory.ts @@ -1,3 +1,4 @@ +import crypto from "crypto"; import { join } from "path"; import { scanContentAndGetFindings } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns"; @@ -38,6 +39,13 @@ import { TQueueBitbucketResourceDiffScan } from "./bitbucket-secret-scanning-types"; +export function generateBitbucketWebhookSecret(serverSecret: string, dataSourceId: string) { + return crypto + .createHash("sha256") + .update(serverSecret + dataSourceId) + .digest("hex"); +} + export const BitbucketSecretScanningFactory = () => { const initialize: TSecretScanningFactoryInitialize< TBitbucketDataSourceInput, @@ -54,7 +62,7 @@ export const BitbucketSecretScanningFactory = () => { { description: "Infisical webhook for push events", url: `${cfg.SITE_URL}/secret-scanning/webhooks/bitbucket`, - active: true, + active: false, events: ["repo:push"] }, { @@ -89,7 +97,8 @@ export const BitbucketSecretScanningFactory = () => { description: "Infisical webhook for push events", url: newWebhookUrl, active: true, - events: ["repo:push"] + events: ["repo:push"], + secret: generateBitbucketWebhookSecret(cfg.AUTH_SECRET, dataSourceId) }, { headers: { diff --git a/backend/src/server/plugins/secret-scanner-v2.ts b/backend/src/server/plugins/secret-scanner-v2.ts index f1d034f30..047a23373 100644 --- a/backend/src/server/plugins/secret-scanner-v2.ts +++ b/backend/src/server/plugins/secret-scanner-v2.ts @@ -7,6 +7,8 @@ import { TBitbucketPushEvent } from "@app/ee/services/secret-scanning-v2/bitbuck import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; import { writeLimit } from "@app/server/config/rateLimiter"; +import crypto from "crypto"; +import { generateBitbucketWebhookSecret } from "@app/ee/services/secret-scanning-v2/bitbucket/bitbucket-secret-scanning-factory"; export const registerSecretScanningV2Webhooks = async (server: FastifyZodProvider) => { const probotApp = (app: Probot) => { @@ -73,16 +75,45 @@ export const registerSecretScanningV2Webhooks = async (server: FastifyZodProvide schema: { querystring: z.object({ dataSourceId: z.string().min(1, { message: "Data Source ID is required" }) - }) + }), + headers: z + .object({ + "x-hub-signature": z.string().min(1, { message: "X-Hub-Signature header is required" }) + }) + .passthrough() }, config: { rateLimit: writeLimit }, handler: async (req, res) => { - // TODO(andrey): Verify request is from bitbucket - const { dataSourceId } = req.query; + // Verify signature + const signature = req.headers["x-hub-signature"]; + if (!signature) { + logger.error("Missing X-Hub-Signature header for Bitbucket webhook"); + return res.status(401).send({ message: "Unauthorized: Missing signature" }); + } + + const expectedSignaturePrefix = "sha256="; + if (!signature.startsWith(expectedSignaturePrefix)) { + logger.error({ signature }, "Invalid X-Hub-Signature format for Bitbucket webhook"); + return res.status(401).send({ message: "Unauthorized: Invalid signature format" }); + } + + const cfg = getConfig(); + + const hmac = crypto.createHmac("sha256", generateBitbucketWebhookSecret(cfg.AUTH_SECRET, dataSourceId)); + hmac.update(JSON.stringify(req.body)); + const calculatedSignature = hmac.digest("hex"); + + const receivedSignature = signature.substring(expectedSignaturePrefix.length); + + if (calculatedSignature !== receivedSignature) { + logger.error("Invalid signature for Bitbucket webhook"); + return res.status(401).send({ message: "Unauthorized: Invalid signature" }); + } + if (!dataSourceId) return res.status(400).send({ message: "Data Source ID is required" }); await server.services.secretScanningV2.bitbucket.handlePushEvent({