diff --git a/src/line/webhook.test.ts b/src/line/webhook.test.ts index af30040b4b..731653a09c 100644 --- a/src/line/webhook.test.ts +++ b/src/line/webhook.test.ts @@ -70,4 +70,41 @@ describe("createLineWebhookMiddleware", () => { expect(res.status).toHaveBeenCalledWith(400); expect(onEvents).not.toHaveBeenCalled(); }); + + it("rejects webhooks with invalid signatures", async () => { + const onEvents = vi.fn(async () => {}); + const secret = "secret"; + const rawBody = JSON.stringify({ events: [{ type: "message" }] }); + const middleware = createLineWebhookMiddleware({ channelSecret: secret, onEvents }); + + const req = { + headers: { "x-line-signature": "invalid-signature" }, + body: rawBody, + } as any; + const res = createRes(); + + await middleware(req, res, {} as any); + + expect(res.status).toHaveBeenCalledWith(401); + expect(onEvents).not.toHaveBeenCalled(); + }); + + it("rejects webhooks with signatures computed using wrong secret", async () => { + const onEvents = vi.fn(async () => {}); + const correctSecret = "correct-secret"; + const wrongSecret = "wrong-secret"; + const rawBody = JSON.stringify({ events: [{ type: "message" }] }); + const middleware = createLineWebhookMiddleware({ channelSecret: correctSecret, onEvents }); + + const req = { + headers: { "x-line-signature": sign(rawBody, wrongSecret) }, + body: rawBody, + } as any; + const res = createRes(); + + await middleware(req, res, {} as any); + + expect(res.status).toHaveBeenCalledWith(401); + expect(onEvents).not.toHaveBeenCalled(); + }); }); diff --git a/src/line/webhook.ts b/src/line/webhook.ts index 5f5e12441b..846d8d796d 100644 --- a/src/line/webhook.ts +++ b/src/line/webhook.ts @@ -12,7 +12,16 @@ export interface LineWebhookOptions { function validateSignature(body: string, signature: string, channelSecret: string): boolean { const hash = crypto.createHmac("SHA256", channelSecret).update(body).digest("base64"); - return hash === signature; + const hashBuffer = Buffer.from(hash); + const signatureBuffer = Buffer.from(signature); + + // Use constant-time comparison to prevent timing attacks + // Ensure buffers are same length before comparison to prevent timing leak + if (hashBuffer.length !== signatureBuffer.length) { + return false; + } + + return crypto.timingSafeEqual(hashBuffer, signatureBuffer); } function readRawBody(req: Request): string | null {