{"record":{"id":"b5e9564cb59b3f05","repo":"yikart/AiToEarn","slug":"channelwebhookinvalidsignature","errorCode":null,"errorMessage":"ChannelWebhookInvalidSignature","messagePattern":"ChannelWebhookInvalidSignature","errorType":"http","errorClass":null,"httpStatus":401,"severity":"warning","filePath":"project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/facebook/facebook-webhook.provider.ts","lineNumber":39,"sourceCode":"@Injectable()\nexport class FacebookWebhookProvider implements PlatformWebhookHandler {\n  private readonly logger = new Logger(FacebookWebhookProvider.name)\n\n  constructor(\n    private readonly config: FacebookConfig,\n    @Optional() private readonly publishRecordRepo?: PublishRecordRepository,\n    @Optional() private readonly stateService?: PublishStateService,\n  ) {}\n\n  async handle(request: Request, response: Response): Promise<void> {\n    if (request.method === 'GET') {\n      this.handleChallenge(request, response)\n      return\n    }\n\n    if (!this.verifyMetaSignature(request)) {\n      this.logger.warn({ platform: AccountType.Facebook }, 'Facebook webhook signature invalid')\n      response.status(401).send(getCodeMessage(ResponseCode.ChannelWebhookInvalidSignature, undefined, getLocale()))\n      return\n    }\n\n    const body = this.parseMetaBody(request)\n    for (const entry of body.entry ?? []) {\n      for (const change of entry.changes ?? []) {\n        await this.applyFacebookChange(change)\n      }\n    }\n    response.status(200).send('EVENT_RECEIVED')\n  }\n\n  private handleChallenge(request: Request, response: Response): void {\n    const {\n      'hub.mode': mode,\n      'hub.verify_token': verifyToken,\n      'hub.challenge': challenge,\n    } = request.query as FacebookWebhookChallengeQuery","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/facebook/facebook-webhook.provider.ts#L21-L57","documentation":"The Facebook webhook provider rejects incoming webhook payloads when verifyMetaSignature(request) fails, responding 401 with the localized ChannelWebhookInvalidSignature message. Meta signs payloads with X-Hub-Signature-256 (HMAC-SHA256 of the raw body using the app secret); a mismatch means the request did not genuinely come from Meta or was altered in transit.","triggerScenarios":"POST to the Facebook webhook endpoint where X-Hub-Signature-256 is absent, computed with a different app secret than the server's FACEBOOK_APP_SECRET, or the raw body bytes differ from what was signed (proxy re-serialization, parsed-then-restringified body).","commonSituations":"Meta app secret rotated/changed per environment without updating server env; reverse proxy (nginx/CDN) modifying the body; multiple Meta apps sharing one webhook URL; forged probing requests; webhook receiving events for a different app than configured.","solutions":["Confirm FACEBOOK_APP_SECRET in the server env matches the Meta app that sends the webhook","Ensure signature verification uses the exact raw body (request.isRaw / raw-body middleware), bypassing body-parser re-serialization","Check that only the intended Meta app is subscribed to this webhook URL and the correct environment is deployed","Inspect the received X-Hub-Signature-256 header in logs and recompute HMAC locally to diagnose the mismatch"],"exampleFix":"// before\napp.use(express.json()) // body parsed before webhook route -> raw bytes lost\n// after\napp.use('/webhooks/facebook', express.raw({ type: 'application/json' })) // verify against raw bytes, then parse","handlingStrategy":"validation","validationCode":"const expected = crypto.createHmac('sha256', appSecret).update(rawBody).digest('hex')\nif (req.headers['x-hub-signature-256'] !== `sha256=${expected}`) return res.status(401).send('invalid signature')","typeGuard":"function hasMetaSignature(req: Request, appSecret: string): boolean {\n  const header = req.headers['x-hub-signature-256'] as string | undefined\n  if (!header?.startsWith('sha256=')) return false\n  const expected = crypto.createHmac('sha256', appSecret).update(req.body as Buffer).digest('hex')\n  return crypto.timingSafeEqual(Buffer.from(header.slice(7)), Buffer.from(expected))\n}","tryCatchPattern":"app.post('/webhooks/facebook', express.raw({ type: 'application/json' }), (req, res) => {\n  if (!hasMetaSignature(req, process.env.FACEBOOK_APP_SECRET!)) {\n    return res.status(401).send('ChannelWebhookInvalidSignature')\n  }\n  const body = JSON.parse((req.body as Buffer).toString())\n  // process entries\n})","preventionTips":["Mount express.raw before JSON body parsing on the Meta webhook route","Keep FACEBOOK_APP_SECRET per environment and aligned with the Meta app sending events","Use one webhook URL per Meta app to avoid cross-app secret mismatches","Log and alert on signature failures to detect config drift quickly"],"tags":["webhook","signature","meta","security"],"backgroundTag":"webhook-signature-verification-failed","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}