Security: fix timing attack vulnerability in LINE webhook signature validation
This commit is contained in:
@@ -70,4 +70,41 @@ describe("createLineWebhookMiddleware", () => {
|
|||||||
expect(res.status).toHaveBeenCalledWith(400);
|
expect(res.status).toHaveBeenCalledWith(400);
|
||||||
expect(onEvents).not.toHaveBeenCalled();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,16 @@ export interface LineWebhookOptions {
|
|||||||
|
|
||||||
function validateSignature(body: string, signature: string, channelSecret: string): boolean {
|
function validateSignature(body: string, signature: string, channelSecret: string): boolean {
|
||||||
const hash = crypto.createHmac("SHA256", channelSecret).update(body).digest("base64");
|
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 {
|
function readRawBody(req: Request): string | null {
|
||||||
|
|||||||
Reference in New Issue
Block a user