{"record":{"id":"0f0e5df896989751","repo":"hcengineering/platform","slug":"invalid-signature","errorCode":null,"errorMessage":"Invalid signature","messagePattern":"Invalid signature","errorType":"http","errorClass":"WebhookVerificationError","httpStatus":403,"severity":"error","filePath":"services/payment/pod-payment/src/providers/polar/webhook.ts","lineNumber":72,"sourceCode":"      case 'subscription.active':\n      case 'subscription.canceled':\n      case 'subscription.uncanceled':\n      case 'subscription.revoked':\n        void handleSubscriptionUpdated(ctx, accountsUrl, serviceToken, event).catch((err) => {\n          ctx.error('Failed to process Polar webhook event', { event, err })\n        })\n        break\n      default:\n    }\n\n    res.status(202).json({ received: true })\n  } catch (err) {\n    // Check if it's a validation error by class name\n    const isValidationError = err instanceof WebhookVerificationError\n\n    if (isValidationError) {\n      ctx.error('Invalid Polar webhook signature', { err })\n      res.status(403).json({ error: 'Invalid signature' })\n      return\n    }\n\n    ctx.error('Failed to process Polar webhook', { err })\n    res.status(500).json({ error: 'Internal server error' })\n  }\n}\n\n/**\n * Handle subscription.created/updated/active\n */\nasync function handleSubscriptionUpdated (\n  ctx: MeasureContext,\n  accountsUrl: string,\n  serviceToken: string,\n  event: any\n): Promise<void> {\n  const subscription = event.data ?? event","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/services/payment/pod-payment/src/providers/polar/webhook.ts#L54-L90","documentation":"Polar's validateEvent() throws WebhookVerificationError when the webhook signature header does not match the HMAC computed from the raw body and the configured webhook secret. The handler responds 403 'Invalid signature' to reject untrusted requests.","triggerScenarios":"POLAR_WEBHOOK_SECRET env var differs from the secret configured in the Polar dashboard; an attacker or misconfigured client sends a forged request; the body was modified in transit (proxy re-encoding) so the HMAC no longer matches; multiple environments sharing one webhook endpoint with the wrong secret.","commonSituations":"Rotating the secret in Polar's dashboard without updating the service env; running staging against production webhook credentials; proxies that re-sign or mutate payloads.","solutions":["Verify the webhook secret env var matches the secret shown in the Polar dashboard for this endpoint exactly (no quotes/whitespace/newlines)","Re-check that the raw Buffer body is untouched before validateEvent (no re-parsing/re-serialization)","Rotate/recreate the webhook endpoint in Polar and copy the fresh secret into the service","Confirm clock sanity and that the correct endpoint secret is used per environment"],"exampleFix":"// before (stale secret)\nPOLAR_WEBHOOK_SECRET=whsec_old\n// after\nPOLAR_WEBHOOK_SECRET=whsec_current_from_dashboard","handlingStrategy":"try-catch","validationCode":"if (!process.env.POLAR_WEBHOOK_SECRET) {\n  throw new Error('POLAR_WEBHOOK_SECRET is not set')\n}","typeGuard":null,"tryCatchPattern":"try {\n  const event = validateEvent(rawBody, req.headers as Record<string,string>, webhookSecret)\n} catch (err) {\n  if (err instanceof WebhookVerificationError) {\n    res.status(403).json({ error: 'Invalid signature' })\n    return\n  }\n  throw err\n}","preventionTips":["Keep the Polar dashboard secret and the env var in sync per environment","Never log or commit the webhook secret","Verify raw body bytes are unmodified before validateEvent","Alert on 403 spikes (possible forgery or secret drift)"],"tags":["security","webhook","hmac","signature"],"backgroundTag":"webhook-signature-verification-failed","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}