Skip to content
chiltepin

Generated from: “Write up what the pentest found on the payments service, worst first, with what we do about each.

Pentest findings — payments service

Written by an agent from the skill, validated by chiltepin check, rendered by the renderer — shown as generated, 13 September 2026.

DOCUMENTSECURITY

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.

SECTION 01 · Note

Assumptions

Warning
The finding list, severities, owners, and dates below are taken from the Northgate report dated 2026-09-04. The report did not name a retest date; we assume 2026-10-17, two weeks after the last planned fix. P-08 is accepted because the build SHA is public on the status page already.

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.

SECTION 02 · Audit findings
scopepayments-api, payments-worker, ledger-dbdate2026-09-04auditorNorthgate Security
8 findings2 critical2 high2 medium1 low1 info
IDSeverityFindingEvidenceFixOwnerStatus
P-01criticalRefund endpoint trusts the client amount and skips the merchant checkAPIPOST /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 refundLoad 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 testspayments-corefixing
P-02criticalWebhook signature is not checked when the X-Signature header is absentWebhooksA forged payment.succeeded event with no signature header set order ord_31c to paidReject every webhook without a valid signature; rotate the PSP webhook secret; alert on any unsigned webhookpayments-corefixed
P-03highFull card number is logged on the 3DS fallback pathLoggingworker.log 2026-08-27 14:02 holds 16-digit PANs for 212 paymentsMask to the first 6 and last 4 digits, add the log-scrub test to CI, purge the affected log range from the log storepayments-workerfixing
P-04highIdempotency keys are global, not scoped to the merchantAPIReplay of merchant B's idempotency key from merchant A's key returned merchant B's charge responseKey the idempotency store on merchant_id plus key; return 404 on a cross-merchant hitpayments-coreopen
P-05mediumNo rate limit on POST /v1/payment-methods enables card testingAPI1,200 card adds per minute from one IP were accepted; 38 percent hit valid BINsApply the shared limiter at 10 per minute per IP and merchant; add a velocity rule that blocks after 5 declines in 10 minutesplatformopen
P-06mediumThe ledger-db listener still accepts TLS 1.0 and 1.1Transportopenssl s_client -tls1 connected to ledger-db:5432Set ssl_min_protocol_version to TLSv1.2 and verify in the rescanplatformfixed
P-07lowStack traces are returned in 500 responsesAPIA malformed JSON body returned the Go stack and internal hostnamesReturn the error id only; keep the trace in the logpayments-corefixing
P-08infoThe health endpoint leaks the build SHA and the pod nameAPIGET /healthz returned sha and hostname fieldsplatformaccepted

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.

SECTION 03 · Sequence
SEQUENCE
Sequence diagram: 7 messages between 4 actorsMerchant A keypayments-apiledger-dbEXTPSPALT[as found][after the P-01 fix]1POST /v1/refunds pay_9f2 amount 250.002read payment pay_9f23captured 40.00 for merchant B4refund 250.00 on pay_9f25refund accepted6201 Created7422 refund_exceeds_capture
Legendcallresponsethe answer the caller getsEXTexternal actorfragment (alt / opt / loop)active

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.

SECTION 04 · Roadmap
2026-09-05
done
Hotfix P-02
Unsigned webhooks rejected in production; PSP secret rotated
2026-09-19
current
Critical and high closed
P-01, P-03, P-04 deployed; affected log range purged
2026-10-03
next
Medium and low closed
P-05 limiter live; P-06 rescanned; P-07 error ids only
2026-10-17
future
Retest
Northgate retests every row; report to the risk committee
Legenddonecurrentnextfuture
View the Markdown
```meta
title: Pentest findings — payments service
subtitle: What Northgate Security found in August 2026, worst first, and the fix for each.
tag: SECURITY
```

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.

```callout
tone: warn
title: Assumptions
body: The finding list, severities, owners, and dates below are taken from the Northgate report dated 2026-09-04. The report did not name a retest date; we assume 2026-10-17, two weeks after the last planned fix. P-08 is accepted because the build SHA is public on the status page already.
```

## 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.

```audit
id: findings
scope: payments-api, payments-worker, ledger-db
date: 2026-09-04
auditor: Northgate Security
findings:
  - id: P-01
    title: Refund endpoint trusts the client amount and skips the merchant check
    severity: critical
    area: API
    evidence: "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"
    fix: "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"
    owner: payments-core
    status: fixing
  - id: P-02
    title: Webhook signature is not checked when the X-Signature header is absent
    severity: critical
    area: Webhooks
    evidence: "A forged payment.succeeded event with no signature header set order ord_31c to paid"
    fix: "Reject every webhook without a valid signature; rotate the PSP webhook secret; alert on any unsigned webhook"
    owner: payments-core
    status: fixed
  - id: P-03
    title: Full card number is logged on the 3DS fallback path
    severity: high
    area: Logging
    evidence: "worker.log 2026-08-27 14:02 holds 16-digit PANs for 212 payments"
    fix: "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"
    owner: payments-worker
    status: fixing
  - id: P-04
    title: Idempotency keys are global, not scoped to the merchant
    severity: high
    area: API
    evidence: "Replay of merchant B's idempotency key from merchant A's key returned merchant B's charge response"
    fix: "Key the idempotency store on merchant_id plus key; return 404 on a cross-merchant hit"
    owner: payments-core
    status: open
  - id: P-05
    title: No rate limit on POST /v1/payment-methods enables card testing
    severity: medium
    area: API
    evidence: "1,200 card adds per minute from one IP were accepted; 38 percent hit valid BINs"
    fix: "Apply the shared limiter at 10 per minute per IP and merchant; add a velocity rule that blocks after 5 declines in 10 minutes"
    owner: platform
    status: open
  - id: P-06
    title: The ledger-db listener still accepts TLS 1.0 and 1.1
    severity: medium
    area: Transport
    evidence: "openssl s_client -tls1 connected to ledger-db:5432"
    fix: "Set ssl_min_protocol_version to TLSv1.2 and verify in the rescan"
    owner: platform
    status: fixed
  - id: P-07
    title: Stack traces are returned in 500 responses
    severity: low
    area: API
    evidence: "A malformed JSON body returned the Go stack and internal hostnames"
    fix: "Return the error id only; keep the trace in the log"
    owner: payments-core
    status: fixing
  - id: P-08
    title: The health endpoint leaks the build SHA and the pod name
    severity: info
    area: API
    evidence: "GET /healthz returned sha and hostname fields"
    owner: platform
    status: 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.

```sequence
id: refund-exploit
actors:
  - { id: Attacker, name: Merchant A key }
  - { id: API, name: payments-api }
  - { id: Ledger, name: ledger-db }
  - { id: PSP, name: PSP, external: true }
messages:
  - Attacker -> +API: POST /v1/refunds pay_9f2 amount 250.00
  - API -> Ledger: read payment pay_9f2
  - Ledger --> API: captured 40.00 for merchant B
  - alt: as found
  - API -> PSP: refund 250.00 on pay_9f2
  - PSP --> API: refund accepted
  - API --> -Attacker: 201 Created
  - else: after the P-01 fix
  - API --> -Attacker: 422 refund_exceeds_capture
  - end
```

## 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.

```timeline
id: remediation
items:
  - "[done] 2026-09-05 · Hotfix P-02 · Unsigned webhooks rejected in production; PSP secret rotated"
  - "[current] 2026-09-19 · Critical and high closed · P-01, P-03, P-04 deployed; affected log range purged"
  - "[next] 2026-10-03 · Medium and low closed · P-05 limiter live; P-06 rescanned; P-07 error ids only"
  - "[future] 2026-10-17 · Retest · Northgate retests every row; report to the risk committee"
```