Pentest findings — payments service
What Northgate Security found in August 2026, worst first, and the fix for each.
Northgate Security tested payments-api, payments-worker, and the ledger-db listener from 2026-08-24 to 2026-09-04. They had a merchant sandbox account and no source access. Two findings let an attacker move money or mark orders paid, so both are the first work in every sprint until they close.
Assumptions
Findings, worst first
Severity follows the Northgate rating. A row is fixing when a pull request exists and fixed only when the fix is in production and Northgate confirmed it.
| ID | Severity | Finding | Evidence | Fix | Owner | Status |
|---|---|---|---|---|---|---|
| P-01 | critical | Refund endpoint trusts the client amount and skips the merchant checkAPI | POST /v1/refunds with payment_id pay_9f2 (merchant B, captured 40.00) and amount 250.00 returned 201 from merchant A's key; the PSP paid the refund | Load the captured amount from the ledger, reject a payment outside the caller's merchant, cap the refund at the remaining balance, and add the three cases as contract tests | payments-core | fixing |
| P-02 | critical | Webhook signature is not checked when the X-Signature header is absentWebhooks | A forged payment.succeeded event with no signature header set order ord_31c to paid | Reject every webhook without a valid signature; rotate the PSP webhook secret; alert on any unsigned webhook | payments-core | fixed |
| P-03 | high | Full card number is logged on the 3DS fallback pathLogging | worker.log 2026-08-27 14:02 holds 16-digit PANs for 212 payments | Mask to the first 6 and last 4 digits, add the log-scrub test to CI, purge the affected log range from the log store | payments-worker | fixing |
| P-04 | high | Idempotency keys are global, not scoped to the merchantAPI | Replay of merchant B's idempotency key from merchant A's key returned merchant B's charge response | Key the idempotency store on merchant_id plus key; return 404 on a cross-merchant hit | payments-core | open |
| P-05 | medium | No rate limit on POST /v1/payment-methods enables card testingAPI | 1,200 card adds per minute from one IP were accepted; 38 percent hit valid BINs | Apply the shared limiter at 10 per minute per IP and merchant; add a velocity rule that blocks after 5 declines in 10 minutes | platform | open |
| P-06 | medium | The ledger-db listener still accepts TLS 1.0 and 1.1Transport | openssl s_client -tls1 connected to ledger-db:5432 | Set ssl_min_protocol_version to TLSv1.2 and verify in the rescan | platform | fixed |
| P-07 | low | Stack traces are returned in 500 responsesAPI | A malformed JSON body returned the Go stack and internal hostnames | Return the error id only; keep the trace in the log | payments-core | fixing |
| P-08 | info | The health endpoint leaks the build SHA and the pod nameAPI | GET /healthz returned sha and hostname fields | platform | accepted |
How the refund over-payment works
P-01 is the finding that moves money. The endpoint takes the amount from the request body and never reads the captured amount or the owning merchant from the ledger. After the fix the API answers 422 before it calls the PSP.
When each fix lands
The two critical findings gate the next release. Medium and low rows go in the following two sprints, and Northgate retests every row that is not accepted.