Skip to content

Commit 3865559

Browse files
committed
feat(webhook): enforce size cap, replay dedupe, installation allowlist
1 parent f1ad47e commit 3865559

2 files changed

Lines changed: 31 additions & 6 deletions

File tree

apps/dashboard/server/api/integrations/github/webhook.post.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@ import { and, eq } from "drizzle-orm"
44
import { verifyWebhookSignature } from "@reprojs/integrations-github"
55
import { db } from "../../../db"
66
import { githubIntegrations, reportEvents, reports } from "../../../db/schema"
7-
import { env } from "../../../lib/env"
87
import { getWebhookSecret } from "../../../lib/github"
98
import { invalidateInstallationRepos } from "../../../lib/github-repo-cache"
109
import { parseGithubLabels } from "../../../lib/github-helpers"
10+
import {
11+
checkBodySize,
12+
MAX_WEBHOOK_BODY_BYTES,
13+
recordDelivery,
14+
isKnownInstallation,
15+
} from "../../../lib/github-webhook-auth"
1116

1217
interface IssuesPayload {
1318
action: "opened" | "closed" | "reopened" | "edited" | "labeled" | "unlabeled" | "deleted" | string
@@ -32,17 +37,17 @@ interface InstallationReposPayload {
3237
}
3338

3439
export default defineEventHandler(async (event) => {
35-
const contentLength = Number(getHeader(event, "content-length") ?? 0)
36-
if (contentLength > env.GITHUB_WEBHOOK_MAX_BYTES) {
37-
throw createError({ statusCode: 413, statusMessage: "Payload too large" })
40+
const contentLength = Number(getHeader(event, "content-length") ?? NaN)
41+
if (!checkBodySize(Number.isNaN(contentLength) ? undefined : contentLength)) {
42+
throw createError({ statusCode: 413, statusMessage: "Payload Too Large" })
3843
}
3944
const raw = await readRawBody(event)
4045
if (!raw || typeof raw !== "string") {
4146
throw createError({ statusCode: 400, statusMessage: "invalid body" })
4247
}
4348
// Fallback guard when Content-Length is missing or understated (chunked).
44-
if (Buffer.byteLength(raw, "utf8") > env.GITHUB_WEBHOOK_MAX_BYTES) {
45-
throw createError({ statusCode: 413, statusMessage: "Payload too large" })
49+
if (Buffer.byteLength(raw, "utf8") > MAX_WEBHOOK_BODY_BYTES) {
50+
throw createError({ statusCode: 413, statusMessage: "Payload Too Large" })
4651
}
4752
const sig = getHeader(event, "x-hub-signature-256")
4853
if (
@@ -56,9 +61,27 @@ export default defineEventHandler(async (event) => {
5661
throw createError({ statusCode: 401, statusMessage: "invalid signature" })
5762
}
5863

64+
const deliveryId = getHeader(event, "x-github-delivery")
65+
if (!deliveryId) {
66+
throw createError({ statusCode: 400, statusMessage: "Missing X-GitHub-Delivery" })
67+
}
68+
if ((await recordDelivery(deliveryId)) === "replay") {
69+
setResponseStatus(event, 202)
70+
return { status: "replay" }
71+
}
72+
5973
const kind = getHeader(event, "x-github-event")
6074
const payload: unknown = JSON.parse(raw)
6175

76+
const installationId = (payload as { installation?: { id?: unknown } })?.installation?.id
77+
if (typeof installationId === "number" && !(await isKnownInstallation(installationId))) {
78+
console.warn(
79+
`[github-webhook] unknown installation id: ${installationId}, delivery=${deliveryId}`,
80+
)
81+
setResponseStatus(event, 202)
82+
return { status: "unknown-installation" }
83+
}
84+
6285
if (kind === "installation") {
6386
const p = payload as InstallationPayload
6487
if (p.action === "deleted" || p.action === "suspend") {

apps/dashboard/tests/api/github-sync.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ describe("webhook", () => {
423423
headers: {
424424
"content-type": "application/json",
425425
"x-github-event": "issues",
426+
"x-github-delivery": crypto.randomUUID(),
426427
"x-hub-signature-256": sign(
427428
process.env.GITHUB_APP_WEBHOOK_SECRET ?? "test-webhook-secret",
428429
body,
@@ -459,6 +460,7 @@ describe("webhook", () => {
459460
headers: {
460461
"content-type": "application/json",
461462
"x-github-event": "installation",
463+
"x-github-delivery": crypto.randomUUID(),
462464
"x-hub-signature-256": sign(
463465
process.env.GITHUB_APP_WEBHOOK_SECRET ?? "test-webhook-secret",
464466
body,

0 commit comments

Comments
 (0)