{
  "id": 3829025,
  "title": "The trailing newline that broke every webhook signature.",
  "url": "https://urgent.news/2026/08/27/the-trailing-newline-that-broke-every-webhook-signature",
  "topic": "tech",
  "section": "Tech",
  "published": "2026-08-27T21:31:33.000Z",
  "source": {
    "name": "Dev.to",
    "slug": "dev-to",
    "url": "https://dev.to/frnxcode/the-trailing-newline-that-broke-every-webhook-signature-3pgc"
  },
  "original_language": "en",
  "account": "An evening of frustration led a developer to believe GitHub was sending broken webhook signatures, but the culprit was not GitHub but rather their webhook signature verification mechanism. The setup changed their GitHub Marketplace webhook required verification of every inbound event using HMAC-SHA256, comparing GitHub's X-Hub-Signature-256 header against a signature computed from the raw request body and a shared secret.\n\nThe developer's initial investigation focused on the most obvious suspects: hashing the raw body versus a re-serialized/re-parsed version, using an incorrect hashing algorithm or encoding, or a timing-safe comparison bug. However, each of these checks proved incorrect. Despite all systems appearing to be functioning correctly, the webhook verification continued to fail deterministically.\n\nThe real issue lay in the shared secret itself. The developer had generated the secret and stored it in an environment file using `echo $SECRET`, which appended a trailing newline by default. This newline became part of the stored secret string. GitHub's configuration field had no trailing newline, meaning their computed signature was based on the clean secret, while the developer's server was hashing the secret with an invisible newline at the end. Consequently, every signature computed by the server was simply wrong, irrespective of the incoming payload.\n\nThe developer's fix was to use `printf %s $SECRET` instead of `echo` when storing the secret in the environment file. Unlike `echo`, `printf` does not append any additional characters to the input string. This simple change resolved the issue, as the signature now matched on the server side.\n\nDebugging tips were shared for others facing similar signature mismatches: instead of re-deriving the verification code, first print the secret's length and a repr/dump of it server-side. A trailing newline will be visible in the repr, making it instantly identifiable. This seemingly minor check could have saved the developer hours of debugging.\n\nThe lesson learned extends beyond this particular bug. Secrets should not be typed or piped through a terminal where validation steps are absent. Instead, secrets should be generated, injected via a secrets manager or CI/CD secret store, and verified (byte length or an HMAC against a test vector) before deployment. Appending secrets to a `.env` file from a shell is inherently fragile, as it can lead to duplicate entries and ambiguity in parsing order. This case serves as a reminder of the importance of proper secret management and validation steps in the development process.",
  "summary": "I spent an evening convinced GitHub was sending broken webhook signatures. It wasn't. echo was. The setup Chngd's GitHub Marketplace webhook verifies every inbound event with HMAC-SHA256, comparing GitHub's X-Hub-Signature-256 header against a signature computed from the raw request body and a shared secret. Every single request failed verification. Not intermittently. Every one, from the very…",
  "key_points": [],
  "editors_take": null,
  "illustration": null,
  "coverage": {
    "outlets": 1,
    "also_reported_by": []
  },
  "ai_generated": true,
  "disclaimer": "Summaries, key points and the editor’s take are written by software from other outlets’ reporting and may contain errors — always check the linked original."
}