Availability: Taking on new projects

2 min read

Verify the gateway’s response, not the string it hands you

Payment gateways sign their callbacks. Validate against the concatenation the gateway supplies and you have written a function that always returns true.

Bank gateways sign the response they send back so you can prove it came from them. Most of them also supply a ready-made concatenation of the values that went into that signature, which is a convenience and must never be what you check.

The mistake

Take the supplied concatenation, append your key, hash it, compare to the supplied hash. It looks correct and it passes in testing. What it actually does is check the gateway’s own string against the gateway’s own hash, both taken from the same request. Anyone who can alter the request can alter both, and your check agrees with itself. You have written a function that returns true.

What to do instead

The response also carries a list of the field names that went into the signature. Walk that list, look each name up in the data as it actually arrived, join the values with the documented separator, append your key, and hash that. Now you are verifying the payload rather than a summary of it, and tampering with any signed field breaks the comparison.

Three things that will waste a day each

  1. The documented approach may not work on your instance. Vendor documentation describes the product. Banks deploy it with options disabled. If the recommended signature version fails on the very first request, stop debugging your implementation and test an older one.
  2. Keys change without notice. Nothing in the response says so, and every symptom points at your code. When you have eliminated everything you control, ask the provider to confirm the key rather than rewriting the hashing again.
  3. Your base address must be natively correct. The signature covers the values you submitted, including the return addresses. Forcing those to secure at the last moment in code changes what you send without changing what you hashed.

Not every anomaly is a fault

One field on this integration carried a zero where a currency code belonged, which looked exactly like an uninitialised variable. It was correct: in that system it means use the merchant’s configured default. Changing an anomaly before you understand it is how a problem with three causes acquires a fourth.

The failure path deserves the same care

Most gateway work is tested on payments that succeed. The declines are where the money is lost:

  • Do not empty the basket when a payment fails, or the customer has nothing left to retry with.
  • Do not write payment metadata in a mode that refuses to overwrite, or the retry can never replace the failed attempt’s data.
  • A guard against reprocessing a paid order has to return, not merely notice.
  • Send a declined customer to the page that lets them try again, not to a confirmation page for an order that was never paid.
  • Read the failure notification email. A copied template that still carries the success subject line tells customers their declined payment went through.

Describe the symptom. I will tell you what it usually means.

Every enquiry gets a reply within one working day.