The Card Token in the Log File
A payment provider returns a reusable card token in the response to almost every call you make. Logging that response for debugging puts a live payment credential into log files, log shippers and backups — where nothing protects it, because nobody thinks of a log as a place that holds card details.
The safest thing about a stored card is that you never see the card. The awkward thing is what you get instead.
Modern payment integrations are built so that card numbers never touch your servers. The customer types the card into the provider's own form, and what comes back to you is a token — a string standing in for the card, useless to anybody except through that provider's interface.
This is a genuinely good arrangement and it removes most of the risk. What it does not remove is the token, and the token has a property people forget: it is reusable. That is its whole purpose. It exists so you can charge the same customer again without asking for the card. Anybody who can present that token to the provider through your account can charge that card.
So it is a payment credential. Not a card number, and not nothing.
Where it ends up
The token rides along in the response to almost every call. Verifying a transaction returns it. Charging returns it. The webhook announcing a successful charge carries it. You do not ask for it; it is simply part of what the provider sends back.
And what does every team do with a payment provider's response during integration? Logs it. All of it, as one blob, because payment bugs are difficult and the response is what you need to see.
Logging it puts a payment credential into log files, log shippers and backups for no operational benefit.
Consider where a log line actually travels. It is written to a file on the server. A shipper forwards it to wherever logs are aggregated, often a third-party service. Both are backed up. The aggregator is searchable by anybody on the team with access, which is usually a broader group than the one allowed near payment records. Retention on logs is typically longer and less considered than retention on data, because a log is not thought of as data.
Every protection applied to the payment record — restricted access, considered retention, deliberate backup handling — has to be applied again to its copy in a log. In practice it never is, because nobody catalogues a log file as a place that holds payment credentials.
What is actually removed
The response is passed through a scrubber before it reaches a log. Four keys are replaced, at any depth of nesting, with a placeholder.
| What is replaced | What it is | Why it goes |
|---|---|---|
| The card authorisation token | The reusable credential that can charge the card again | A live payment credential. This is the one that matters. |
| The webhook signature | The value proving a webhook is genuine | The thing that authenticates a message is not something to leave lying about in a searchable store. |
| The bank account number | The destination of a payout | A bank identifier belonging to a person, in a file nobody treats as holding them. |
| The leading digits of a card | They identify the issuing bank and card product | More identifying than most people assume. |
What deliberately stays is the last four digits and the card brand. Those are what an engineer actually needs to work out which card a customer is talking about, they are what the customer sees on their own statement, and neither can be used to charge anything.
That is the test worth applying to any redaction decision: what does somebody debugging genuinely need, and does that overlap with what is dangerous? Here it does not overlap at all, which is why the trade is easy. The cases where it does overlap are the hard ones, and they are hard for everyone.
Three details in the scrubber worth copying
-
It recurses
Provider responses are nested — data inside data, an authorisation object inside a transaction inside a response. A scrubber that checks only the top level of a payload removes nothing from a real response while appearing to work perfectly, because the token is three levels down. Top-level-only redaction is the commonest way this is got wrong.
-
It matches keys without regard to case
A provider that changes the casing of a field in a future version of its interface should not silently switch off your redaction. Comparing lowercased keys costs nothing and removes an entire class of quiet regression that would otherwise appear during a routine upgrade.
-
An empty value is left as it was
A key present but null logs as null, not as a placeholder. This is more thoughtful than it looks: writing a placeholder where there was nothing asserts that a secret existed and was removed, which is misleading to the next person reading the log. Redaction should represent what was there, including when nothing was.
The structural weakness, stated plainly
Scrubbing here happens at the point of logging. Eighteen places in the payment controllers wrap a response in the scrubber before passing it to a log statement. Each of those is a call somebody remembered to write.
Which means the nineteenth has to be remembered too. Somebody debugging a payment problem at nine in the evening adds a log line, ships it, and it is still there in six months — logging the whole response, including the token, with nothing to catch it. There is no error, no test failure and no code review signal, because a log statement is the least suspicious line anybody ever writes.
The structurally stronger arrangement is a processor attached to the logger itself, so redaction happens on the way out of the application regardless of what any call site remembered. The per-call-site version is a real protection covering the paths that exist today, and it depends on discipline in a place where discipline is exactly what is missing at nine in the evening.
The general form of this problem
Any protection that has to be invoked will eventually not be invoked. This is not a comment about carelessness — it is a property of the arrangement. When you find one of these in your own code, the useful question is not "who forgot" but "can this be moved somewhere it cannot be forgotten". Sometimes it cannot, and then the answer is a test that fails when a new call site appears without it.
What to do about your own logs
This applies to any system integrating with any payment provider, and to a good deal beyond payments.
- Search your aggregated logs for the token field name your provider uses. Most integrations are logging it somewhere and nobody has looked.
- Check nesting. A redaction that only examines the top level of a payload passes every review and removes nothing from a real response.
- Ask what an engineer actually needs from a logged response. It is almost always an identifier, a status and a reason — very rarely the whole payload, which is what gets logged because it is easier.
- Treat log retention as data retention. A payload logged today is in a backup for as long as your backups last, and nobody revisits that decision for logs.
- Look at who can read the aggregator. It is usually a wider group than the one permitted near payment records, which is the whole point of the copy being a problem.
Redaction that travels with the logger
The scrubber itself is right and its list is right. What can be improved is where it sits, so a new log statement inherits the protection rather than having to ask for it.
A processor on the logger
Redaction applied as records leave the application, so every log statement is covered whether or not its author knew about it.
A test that fails on a new payload log
Catching a log statement that hands a provider response straight to the logger, the way the codebase already gates unescaped output and unknown icons.
One redaction list for every provider
The same treatment for the other payment integrations, driven by a shared list rather than one class per provider.
We publish scope, not dates.
Scope log redactionWhat AWRA OpsHub does today
- A scrubber removing the reusable card token, the webhook signature, bank account numbers and card leading digits from payment provider payloads before they are logged.
- Redaction applied recursively at any depth of nesting, so a token inside a nested object is removed rather than surviving a top-level check.
- Key matching that ignores case, so a provider changing the casing of a field cannot silently disable the redaction during an upgrade.
- An empty or null value left as it was rather than replaced, so a placeholder never asserts that a secret existed where none did.
- The card last four digits and brand deliberately preserved, because they are what an engineer needs to identify a transaction and neither can charge anything.
- Eighteen call sites across the payment controllers passing provider responses through the scrubber before logging them.
More we can add to your workspace
- Redaction attached to the logger itself, so every log statement is covered by default and a new one written in a hurry inherits the protection rather than needing to request it.
- A test that fails when a log statement hands a provider payload straight to the logger, in the shape of the gates this codebase already runs for output escaping and icon coverage.
- One shared redaction list across every payment integration, rather than a class per provider whose lists can drift apart.
- Redaction covering inbound request payloads as well as provider responses, so both directions of a payment conversation get the same treatment.
- A retention policy for logs written deliberately alongside the one for data, so a payload logged today has a stated life rather than the default one.
Where we point you to a specialist
- We would decline to log a complete payment provider payload even into a store we control and consider well protected. The value of a full payload during debugging is real and smaller than it feels, and the copy outlives the debugging session by years.
- Where a card scheme or an acquirer specifies what may be stored, retained or logged, that specification governs and we implement it rather than treating our own judgement as sufficient. Which rules bind you is a question for your acquirer and your own advisers.
- We hold that the last four digits and the card brand should stay in a log rather than being redacted along with everything else. Removing them makes payment problems materially harder to diagnose and protects nothing, and a redaction policy that impedes debugging without reducing risk gets worked around.
Moving redaction onto the logger and adding a test that catches a new unredacted payload log are one contained piece of work between them, and together they convert a protection that depends on memory into one that does not.
Our take
The interesting thing about this defect is that it is invisible from every direction that usually finds defects. The payment works. The customer is charged correctly. No test fails, no monitor fires, no screen looks wrong, and the log line responsible was written by somebody being conscientious about debuggability. It surfaces only when somebody thinks to search the log aggregator for a field name — which is not a thing anybody does spontaneously. The four keys are right and the recursion and case handling are right. The honest weakness is that the protection sits at eighteen call sites rather than on the logger, so it covers what exists and not what gets added next. If you take one action from this article, make it the search: look for your provider token in your own aggregated logs, today, before reading anything else about how it ought to work.
Search your own logs for the token field
Whatever your payment provider calls the reusable authorisation value, search your aggregator for it. Most integrations are logging it and have been since the week they were built. Finding it takes a minute, and the answer determines whether the rest of this article is theoretical for you.
Talk through payment loggingFrequently asked questions
Is a card token really dangerous if it is not a card number?
It is a payment credential rather than a card, and the distinction matters less than it sounds. The token exists specifically so a customer can be charged again without their card being re-entered, so anybody able to present it through your provider account can charge that card. It is not usable elsewhere, which is a genuine limit, and within your own integration it does exactly what a card does.
Why is the webhook signature redacted as well?
Because it is the value proving a message genuinely came from the provider, and the point of a signature is that only the two parties can produce one. Keeping a store of valid signatures alongside the payloads they authenticate is the sort of thing that turns out to matter later. It is also of no debugging value once verification has already passed or failed, so there is nothing to weigh against removing it.
Why keep the last four digits and the brand?
Because they are what makes a logged transaction identifiable to a person. A customer describes a problem as happening on their Visa ending in a particular four digits, and matching that to a log entry is the first step of every payment investigation. Neither value can be used to charge anything, so the trade is one-sided in a way most redaction decisions are not.
Why does redaction need to recurse?
Because provider responses are nested several levels deep, and the token typically lives inside an authorisation object inside a transaction object rather than at the top. A scrubber checking only the outermost level passes review, looks correct in a unit test written against a flat example, and removes nothing at all from a real response. It is the commonest way this protection is written wrongly.
Why is a null value left as null instead of showing as redacted?
Because a placeholder asserts that something was there. Somebody reading the log later, trying to work out whether a token was returned at all, would be misled by a redaction marker standing in for an absent value. Redaction should describe what the payload contained, including when the answer is nothing.
Would a processor on the logger be strictly better?
For coverage, yes, and that is why it is on the list of work we can add. Redaction applied as records leave the application covers every log statement regardless of what its author knew, which removes the dependency on somebody remembering. The current arrangement genuinely protects the paths that exist; what it cannot do is protect the log line added next year by somebody in a hurry.