Skip to main content

Forwarding the Raw Body & x-line-signature

Overview

This is a commonly misunderstood point: webhook-go does not verify the x-line-signature header itself.

A review of both internal/ and cmd/ turns up no HMAC-SHA256 comparison against a channel secret anywhere in this project. What does exist is internal/util/hash.go (HMACShort, a general-purpose short hash helper) and internal/util/base64.go (fileSignature, which reads a file's magic bytes) — neither is related to LINE signature verification.

What the service does instead is preserve the raw request body bytes untouched, so that downstream consumers — whether a customer endpoint receiving a forward, or worker-go consuming the queue — can verify the signature themselves. This is a transfer of responsibility, not an oversight.

The technical reason: x-line-signature is an HMAC-SHA256 over the entire raw body. If the payload is passed through json.Unmarshal and then json.Marshal, Go sorts map keys alphabetically. The bytes change and the signature breaks immediately, even though the JSON content is semantically identical.

Business Flow

  1. processLine reads the body with io.ReadAll(c.Request.Body), producing raw []byte.
  2. The bytes are copied into BodyRaw, typed as json.RawMessage. This is a real copy, not an alias to a buffer that gin will later reuse.
  3. raw is parsed into a map[string]interface{} stored in the Body field, used only for internal routing logic (reading events[].type, source.userId, message.text, postback.data). It is never serialised back out.
  4. When publishing to a queue, LineWebhookPayload.MarshalJSON is overridden to emit BodyRaw verbatim as the body field. It only falls back to marshalling the Body map when BodyRaw is empty, i.e. when the body was not a JSON object.
  5. When forwarding over HTTP, BodyRaw is sent as the request body and the x-line-signature header is copied along with it, so the receiving system can match the signature against exactly the bytes it received.
  6. All header keys are lowercased at the handler layer, because Go canonicalises them to X-Line-Signature while downstream systems ported from Express look them up as x-line-signature.

Security implications worth recording

  • POST /api/line/:id is public and unauthenticated. Anyone who knows a webhookId can post a forged payload. The only real defences are per-IP rate limiting and the fact that webhookId is hard to guess.
  • Real verification has to happen in worker-go, the consumer of line_webhook. If the worker does not verify, then effectively nothing in the pipeline does.
  • The queued payload carries the complete headers map including the signature, so downstream consumers have everything they need to verify.

Key Files & Functions

FileRelevant part
internal/line/handler.goHandler.processLine performs io.ReadAll and copies into bodyRaw; toLowerHeader
internal/line/service.goLineWebhookPayload.MarshalJSON, webhookPayload.MarshalJSON, and marshalWebhookBody, the shared helper that prefers raw over the map
internal/line/service.goService.forwardWebhook sends the raw bytes and re-sets the signature header on the outgoing request
internal/line/rawbody_test.goTests asserting the emitted body is byte-for-byte identical to the original

Connections to Other Services

  • worker-go — responsible for actual verification, since it receives both the headers map and the raw body.
  • Customer endpoints (forwardWebhookUrl) — can verify independently because they receive both the raw body and the signature header (see Forwarding Webhooks to Customer Systems).
  • Maintenance warning: never mutate the Body map and re-marshal it on the publish or forward path — doing so breaks this parity immediately. The code carries warning comments at all three locations.
  • Known parity gap: in mbox_handoff, the exit and mbox_team branches send a webhookPayload without setting BodyRaw, so the outgoing body is re-marshalled from the map with reordered keys. This differs from the handoff and department_picker branches, which send the raw bytes (see Handoff to Human Agents).