Skip to content

Commit 7c66709

Browse files
fix: validate OpenNode webhook signature before processing
1 parent 341ddea commit 7c66709

1 file changed

Lines changed: 52 additions & 1 deletion

File tree

src/controllers/callbacks/opennode-callback-controller.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { Request, Response } from 'express'
22

33
import { Invoice, InvoiceStatus } from '../../@types/invoice'
44
import { createLogger } from '../../factories/logger-factory'
5+
import { createSettings } from '../../factories/settings-factory'
56
import { fromOpenNodeInvoice } from '../../utils/transform'
7+
import { getRemoteAddress } from '../../utils/http'
8+
import { hmacSha256 } from '../../utils/secret'
69
import { IController } from '../../@types/controllers'
710
import { IPaymentsService } from '../../@types/services'
811

@@ -13,14 +16,62 @@ export class OpenNodeCallbackController implements IController {
1316
private readonly paymentsService: IPaymentsService,
1417
) {}
1518

16-
// TODO: Validate
1719
public async handleRequest(
1820
request: Request,
1921
response: Response,
2022
) {
2123
debug('request headers: %o', request.headers)
2224
debug('request body: %O', request.body)
2325

26+
const settings = createSettings()
27+
const remoteAddress = getRemoteAddress(request, settings)
28+
const paymentProcessor = settings.payments?.processor
29+
30+
if (paymentProcessor !== 'opennode') {
31+
debug('denied request from %s to /callbacks/opennode which is not the current payment processor', remoteAddress)
32+
response
33+
.status(403)
34+
.send('Forbidden')
35+
return
36+
}
37+
38+
const validStatuses = ['expired', 'refunded', 'unpaid', 'processing', 'underpaid', 'paid']
39+
40+
if (
41+
!request.body
42+
|| typeof request.body.id !== 'string'
43+
|| typeof request.body.hashed_order !== 'string'
44+
|| typeof request.body.status !== 'string'
45+
|| !validStatuses.includes(request.body.status)
46+
) {
47+
response
48+
.status(400)
49+
.setHeader('content-type', 'text/plain; charset=utf8')
50+
.send('Bad Request')
51+
return
52+
}
53+
54+
const openNodeApiKey = process.env.OPENNODE_API_KEY
55+
if (!openNodeApiKey) {
56+
debug('OPENNODE_API_KEY is not configured; unable to verify OpenNode callback from %s', remoteAddress)
57+
response
58+
.status(500)
59+
.setHeader('content-type', 'text/plain; charset=utf8')
60+
.send('Internal Server Error')
61+
return
62+
}
63+
64+
const expected = hmacSha256(openNodeApiKey, request.body.id).toString('hex')
65+
const actual = request.body.hashed_order
66+
67+
if (expected !== actual) {
68+
debug('unauthorized request from %s to /callbacks/opennode: hashed_order mismatch', remoteAddress)
69+
response
70+
.status(403)
71+
.send('Forbidden')
72+
return
73+
}
74+
2475
const invoice = fromOpenNodeInvoice(request.body)
2576

2677
debug('invoice', invoice)

0 commit comments

Comments
 (0)