Verify Sanity webhook signatures in the revalidate route

The secret was passed in the query string, which ends up in access logs
and doesn't protect the request body. Verify the signed header with
@sanity/webhook instead, and return 400 for invalid JSON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
leanderandClaude Opus 5.5 committed 2026-10-30 21:48:49 +01:00
1 parent c5c366884a
commit c997b74d01
4 files changed
+41 -4

No files matched your search

+2
View File
@@ -47,6 +47,8 @@ MEN
Du må spesifisere URL for webhook i Sanity sitt dashbord. URLen dette webhooket skal peke på er `https://uiogaming.no/api/revalidate` med _POST_ forespørselstype.
Sett feltet _Secret_ på webhooket til samme verdi som `SANITY_REVALIDATE_SECRET` i `.env`. Sanity signerer da hver forespørsel, og APIet avviser forespørsler uten gyldig signatur.
Du kan lese mer om det [her](https://www.sanity.io/docs/webhooks).
## Vil du bidra?
+10
View File
@@ -11,6 +11,7 @@
"@portabletext/react": "^3.2.1",
"@sanity/client": "^7.8.1",
"@sanity/image-url": "^1.1.0",
"@sanity/webhook": "^4.0.4",
"country-flag-icons": "^1.5.19",
"install": "^0.13.0",
"next": "^16.3.3",
@@ -1433,6 +1434,15 @@
"node": ">=10.0.0"
}
},
"node_modules/@sanity/webhook": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@sanity/webhook/-/webhook-4.0.4.tgz",
"integrity": "sha512-c8LmzR5Wx93zJ10ohhp0XlmPrTNSFyvPcLbNY/sN45Wj5VOngz4FbBMbX9j64sSQLt+676U6OhiVfjs1yw3YCw==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@schummar/icu-type-parser": {
"version": "1.21.5",
"resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz",
+1
View File
@@ -15,6 +15,7 @@
"@portabletext/react": "^3.2.1",
"@sanity/client": "^7.8.1",
"@sanity/image-url": "^1.1.0",
"@sanity/webhook": "^4.0.4",
"country-flag-icons": "^1.5.19",
"install": "^0.13.0",
"next": "^16.3.3",
+28 -4
View File
@@ -1,3 +1,4 @@
import { isValidSignature, SIGNATURE_HEADER_NAME } from "@sanity/webhook"
import { revalidateTag } from "next/cache"
import { NextRequest, NextResponse } from "next/server"
@@ -6,14 +7,37 @@ import { NextRequest, NextResponse } from "next/server"
const expireTag = (tag: string) => revalidateTag(tag, { expire: 0 })
export async function POST(request: NextRequest) {
const secret = request.nextUrl.searchParams.get("secret")
const secret = process.env.SANITY_REVALIDATE_SECRET
if (!secret) {
console.error("SANITY_REVALIDATE_SECRET is not set")
return NextResponse.json(
{ message: "Webhook is not configured" },
{ status: 500 }
)
}
if (secret !== process.env.SANITY_REVALIDATE_SECRET) {
return NextResponse.json({ message: "Invalid secret" }, { status: 401 })
// Sanity signs the raw body with the webhook secret, so verify it before
// parsing
const rawBody = await request.text()
const signature = request.headers.get(SIGNATURE_HEADER_NAME) ?? ""
// Throws on a malformed signature header instead of returning false
const isValid = await isValidSignature(rawBody, signature, secret).catch(
() => false
)
if (!isValid) {
return NextResponse.json({ message: "Invalid signature" }, { status: 401 })
}
let body
try {
body = JSON.parse(rawBody)
} catch {
return NextResponse.json({ message: "Invalid JSON body" }, { status: 400 })
}
try {
const body = await request.json()
const { _type, slug } = body
console.log(