Fix webhook signature verification — raw body capture + Forgejo header support
- fastify-raw-body was never registered, so HMAC ran over re-serialised JSON and every Forgejo delivery failed with 401 - accept X-Forgejo-Signature / X-Gitea-Signature (bare hex) as well as the GitHub-style sha256= prefix, and reject length mismatches instead of crashing timingSafeEqual with a 500 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2bc57ee89d
commit
568cd8d299
1 changed files with 14 additions and 2 deletions
|
|
@ -6,6 +6,13 @@ import { promisify } from 'util'
|
||||||
const execAsync = promisify(exec)
|
const execAsync = promisify(exec)
|
||||||
|
|
||||||
const app = Fastify({ logger: true })
|
const app = Fastify({ logger: true })
|
||||||
|
|
||||||
|
// Capture the exact request bytes for webhook HMAC verification —
|
||||||
|
// re-serialising the parsed body does not round-trip Forgejo's payload.
|
||||||
|
app.addContentTypeParser('application/json', { parseAs: 'string' }, (req, body, done) => {
|
||||||
|
req.rawBody = body
|
||||||
|
try { done(null, JSON.parse(body)) } catch (err) { done(err) }
|
||||||
|
})
|
||||||
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || ''
|
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || ''
|
||||||
const SSH_KEY = process.env.SSH_KEY_PATH || '/root/.ssh/id_ed25519'
|
const SSH_KEY = process.env.SSH_KEY_PATH || '/root/.ssh/id_ed25519'
|
||||||
const FORGEJO_URL = process.env.FORGEJO_URL || 'https://git.pterois.co.uk'
|
const FORGEJO_URL = process.env.FORGEJO_URL || 'https://git.pterois.co.uk'
|
||||||
|
|
@ -75,8 +82,11 @@ const CACHE_TTL = 5 * 60 * 1000
|
||||||
|
|
||||||
function verifySignature(body, signature) {
|
function verifySignature(body, signature) {
|
||||||
if (!WEBHOOK_SECRET) return true
|
if (!WEBHOOK_SECRET) return true
|
||||||
const expected = `sha256=${crypto.createHmac('sha256', WEBHOOK_SECRET).update(body).digest('hex')}`
|
// Forgejo/Gitea send a bare hex digest; GitHub prefixes it with "sha256="
|
||||||
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature || ''))
|
const provided = (signature || '').replace(/^sha256=/, '')
|
||||||
|
const expected = crypto.createHmac('sha256', WEBHOOK_SECRET).update(body).digest('hex')
|
||||||
|
if (provided.length !== expected.length) return false
|
||||||
|
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sshGet(host, cmd) {
|
async function sshGet(host, cmd) {
|
||||||
|
|
@ -203,6 +213,8 @@ app.post('/deploy/:repo', async (request, reply) => {
|
||||||
|
|
||||||
app.post('/webhook', { config: { rawBody: true } }, async (request, reply) => {
|
app.post('/webhook', { config: { rawBody: true } }, async (request, reply) => {
|
||||||
const sig = request.headers['x-hub-signature-256']
|
const sig = request.headers['x-hub-signature-256']
|
||||||
|
|| request.headers['x-forgejo-signature']
|
||||||
|
|| request.headers['x-gitea-signature']
|
||||||
const rawBody = request.rawBody || JSON.stringify(request.body)
|
const rawBody = request.rawBody || JSON.stringify(request.body)
|
||||||
if (!verifySignature(rawBody, sig)) return reply.status(401).send({ error: 'Invalid signature' })
|
if (!verifySignature(rawBody, sig)) return reply.status(401).send({ error: 'Invalid signature' })
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue