{"record":{"id":"b944a19572b4d657","repo":"bytedance/deer-flow","slug":"invalid-or-missing-x-hub-signature-256","errorCode":null,"errorMessage":"Invalid or missing X-Hub-Signature-256","messagePattern":"Invalid or missing X-Hub-Signature-256","errorType":"http","errorClass":"HTTPException","httpStatus":401,"severity":"error","filePath":"backend/app/gateway/routers/github_webhooks.py","lineNumber":252,"sourceCode":"            )\n            raise HTTPException(\n                status_code=503,\n                detail=f\"Webhook signature verification not configured. Set {_SECRET_ENV_VAR} or {_ALLOW_UNVERIFIED_ENV_VAR}=1 for unverified dev mode.\",\n            )\n        logger.warning(\n            \"github_webhook: accepting UNVERIFIED delivery (event=%s delivery=%s). %s=1 is set — dev/loopback mode ONLY. Do not use in production.\",\n            x_github_event,\n            x_github_delivery,\n            _ALLOW_UNVERIFIED_ENV_VAR,\n        )\n    else:\n        if not _verify_signature(secret, body, x_hub_signature_256):\n            logger.warning(\n                \"github_webhook: signature verification FAILED (event=%s delivery=%s)\",\n                x_github_event,\n                x_github_delivery,\n            )\n            raise HTTPException(status_code=401, detail=\"Invalid or missing X-Hub-Signature-256\")\n\n    if not x_github_event:\n        raise HTTPException(status_code=400, detail=\"Missing X-GitHub-Event header\")\n\n    # Parse JSON payload after signature is verified (verify-then-parse).\n    try:\n        payload: dict[str, Any] = json.loads(body) if body else {}\n    except json.JSONDecodeError as exc:\n        logger.warning(\n            \"github_webhook: invalid JSON body (event=%s delivery=%s): %s\",\n            x_github_event,\n            x_github_delivery,\n            exc,\n        )\n        raise HTTPException(status_code=400, detail=\"Invalid JSON body\") from exc\n\n    if x_github_event in _KNOWN_EVENTS:\n        logger.info(","sourceCodeStart":234,"sourceCodeEnd":270,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/app/gateway/routers/github_webhooks.py#L234-L270","documentation":"Raised by the GitHub webhook receiver with status 401 when a secret IS configured but the X-Hub-Signature-256 header is missing or does not match HMAC-SHA256(secret, raw_body). The receiver verifies the signature over the exact raw request bytes before parsing JSON (verify-then-parse), so any body mutation by a proxy breaks it.","triggerScenarios":"GitHub webhook secret in repo settings differs from the Gateway's configured secret; an intermediate proxy (nginx with body rewrite, request modification, or re-encoding) altering the raw body; replaying a captured request with a modified payload; missing signature header from a non-Github sender.","commonSituations":"Secret regenerated on one side only; nginx lua/scripts rewriting request bodies; line-ending or encoding transformations; load balancer appending trailers; using a test client that doesn't compute the HMAC.","solutions":["Re-sync the secret: set the identical value in the GitHub App/webhook settings and the Gateway env var, then redeliver from GitHub's webhook UI.","Ensure the reverse proxy passes the request body through byte-for-byte untouched (no body filters/rewrites) and forwards the X-Hub-Signature-256 header.","When testing manually, compute the header: sha256 hex of HMAC over the exact body bytes you send."],"exampleFix":"# before (test client sends no/incorrect signature)\ncurl -X POST https://host/api/webhooks/github -H 'X-GitHub-Event: push' -d @payload.json\n# -> 401 Invalid or missing X-Hub-Signature-256\n\n# after\nSIG=$(openssl dgst -sha256 -hmac \"$GITHUB_WEBHOOK_SECRET\" -hex < payload.json | awk '{print $2}')\ncurl -X POST https://host/api/webhooks/github \\\n  -H \"X-GitHub-Event: push\" \\\n  -H \"X-Hub-Signature-256: sha256=$SIG\" \\\n  --data-binary @payload.json","handlingStrategy":"validation","validationCode":"# verify locally before sending: HMAC over exact body bytes\nimport hmac, hashlib\nsig = 'sha256=' + hmac.new(secret.encode(), body_bytes, hashlib.sha256).hexdigest()\nassert sig == sent_header, 'signature mismatch — check secret and body bytes'","typeGuard":null,"tryCatchPattern":"try { await deliver(); } catch (e) { if (e.status === 401) { await resyncWebhookSecret(); await redeliver(); } throw e; }","preventionTips":["Keep the GitHub-side and Gateway-side secret identical","Never let proxies rewrite the request body on the webhook route","Use --data-binary (no re-encoding) when testing"],"tags":["http-401","webhooks","hmac","signature","github"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}