fix(line): return 200 for webhook verification requests without signature

LINE Platform sends POST {"events":[]} without an X-Line-Signature
header when the user clicks 'Verify' in the LINE Developers Console.
Both webhook.ts and monitor.ts rejected this with 400 'Missing
X-Line-Signature header', causing verification to fail.

Now detect the verification pattern (no signature + empty events array)
and return 200 OK immediately, while still requiring valid signatures
for all real webhook deliveries with non-empty events.

Fixes #16425
This commit is contained in:
artale
2026-02-14 23:10:13 +01:00
committed by Peter Steinberger
parent 3189430ad0
commit 4f2f641950
3 changed files with 71 additions and 2 deletions

View File

@@ -322,8 +322,24 @@ export async function monitorLineProvider(
const rawBody = await readLineWebhookRequestBody(req, LINE_WEBHOOK_MAX_BODY_BYTES);
const signature = req.headers["x-line-signature"];
// Validate signature
// LINE webhook verification sends POST {"events":[]} without a
// signature header. Return 200 so the LINE Developers Console
// "Verify" button succeeds.
if (!signature || typeof signature !== "string") {
try {
const verifyBody = JSON.parse(rawBody) as WebhookRequestBody;
if (Array.isArray(verifyBody.events) && verifyBody.events.length === 0) {
logVerbose(
"line: webhook verification request (empty events, no signature) — 200 OK",
);
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ status: "ok" }));
return;
}
} catch {
// Not valid JSON — fall through to the error below.
}
logVerbose("line: webhook missing X-Line-Signature header");
res.statusCode = 400;
res.setHeader("Content-Type", "application/json");

View File

@@ -98,6 +98,48 @@ describe("createLineWebhookMiddleware", () => {
expect(onEvents).not.toHaveBeenCalled();
});
it("returns 200 for verification request (empty events, no signature)", async () => {
const onEvents = vi.fn(async () => {});
const secret = "secret";
const rawBody = JSON.stringify({ events: [] });
const middleware = createLineWebhookMiddleware({ channelSecret: secret, onEvents });
const req = {
headers: {},
body: rawBody,
// oxlint-disable-next-line typescript/no-explicit-any
} as any;
const res = createRes();
// oxlint-disable-next-line typescript/no-explicit-any
await middleware(req, res, {} as any);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({ status: "ok" });
expect(onEvents).not.toHaveBeenCalled();
});
it("rejects missing signature when events are non-empty", 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: {},
body: rawBody,
// oxlint-disable-next-line typescript/no-explicit-any
} as any;
const res = createRes();
// oxlint-disable-next-line typescript/no-explicit-any
await middleware(req, res, {} as any);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error: "Missing X-Line-Signature header" });
expect(onEvents).not.toHaveBeenCalled();
});
it("rejects webhooks with signatures computed using wrong secret", async () => {
const onEvents = vi.fn(async () => {});
const correctSecret = "correct-secret";

View File

@@ -39,13 +39,24 @@ export function createLineWebhookMiddleware(
return async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
try {
const signature = req.headers["x-line-signature"];
const rawBody = readRawBody(req);
// LINE webhook verification sends POST {"events":[]} without a
// signature header. Return 200 immediately so the LINE Developers
// Console "Verify" button succeeds.
if (!signature || typeof signature !== "string") {
if (rawBody) {
const body = parseWebhookBody(req, rawBody);
if (body && Array.isArray(body.events) && body.events.length === 0) {
logVerbose("line: webhook verification request (empty events, no signature) — 200 OK");
res.status(200).json({ status: "ok" });
return;
}
}
res.status(400).json({ error: "Missing X-Line-Signature header" });
return;
}
const rawBody = readRawBody(req);
if (!rawBody) {
res.status(400).json({ error: "Missing raw request body for signature verification" });
return;