A capture of a live transaction holds the account number, usually more than once, in more places than you will find by eye. It is in tag 5A, in the Track 2 equivalent in tag 57, sometimes in a Track 1 record, and in the issuer identification number in tag 42. It can also be recovered from the ICC public key certificate, because that certificate contains the account number and the scheme's root keys are published. Redact the obvious copy and keep the certificate, and you leak exactly what you thought you had removed.
Why cleaning it later is not a plan#
It is easy to feel this has been handled when it has not. A commit does not remove an earlier commit; it adds one, and the original object stays reachable in the history, in every clone anybody took, and in whatever forge you pushed to. The same goes for the backup that ran while the raw file sat there, the cloud folder that synced it, the editor's undo file, and the terminal scrollback you have not closed.
So the moment that matters is the first write, not publication. This repository is built around that. The ignore file keeps the originals directory, and anything named for raw captures, out of version control entirely, so the unsanitized file is never something git could be asked to forget. That is a cheaper guarantee than any amount of history rewriting, and it is two lines long.
Replace, do not delete#
When you do sanitize, replace rather than delete. A capture with the account number cut out no longer parses, the lengths no longer match, and it stops being useful for teaching. Substitute a published test number of the same length, rebuild the records around it, and every structure stays intact. That is why the captures on this site can still be decoded byte for byte.
Same length is the rule everything else rests on. If every replacement is exactly as long as the value it replaces, nothing needs recomputing. Hex-dump offsets, byte counts printed in labels, the ASCII gutter down the side of a dump, and the lengths of every enclosing TLV template all stay correct. The tool here refuses to emit a different length rather than fixing up the file around it. That is the right way round, because a length mismatch is a bug in the replacement, not a formatting problem.
The replacement itself should be random, never derived. A hashed mapping feels tidier and is broken. The leading digits are fixed by the brand and the last digit by the checksum, so an attacker holding your salt has only about a billion candidates to try, which is an afternoon. A number drawn at random from a published test range has no relationship to the original at all, and the mapping can live in memory for one run and then be gone.
Two checks, and why they must disagree#
A sanitizer that misses a field and a verifier written from the same assumption will agree with each other perfectly and both be wrong. So the checks here are deliberately built on different knowledge. One is exact and knows the answer: it lifts the real numbers out of the original's PAN-bearing fields and proves that neither they nor any twelve-digit fragment of them survives anywhere in the output. It needs the original, so it can only run on your own machine.
The other knows nothing about the original, which is exactly why it can run in continuous integration where no original exists. It re-reads the finished file and asks a shape question of every field: does this start with something Luhn-valid that is not on a published test range? It also reads the parts that are not fields at all: an ASCII track record anywhere in the text, and plain digits in a comment somebody added by hand.
Naive detection fails in both directions#
The obvious rule, flag every Luhn-valid run of twelve to nineteen digits, cannot be made to work. It fails as a false negative and as a false positive at the same time. The false negative is the certificate: nothing in a random-looking RSA block reads as a card number, and the number is in there all the same. The false positive is stranger, and it is where most homemade sanitizers die.
Read a field's bytes as pairs of hexadecimal nibbles and ordinary text turns into long runs of digits. Most of the letters used in these labels have hexadecimal codes made entirely of decimal digits. Run this corpus through the shape scan with its skip list disabled and three ordinary text values come back looking like account numbers. The application label VISA CREDIT yields a nineteen-digit candidate, the label Debit Mastercard a twelve-digit one, and the preferred name Visa Credit a seventeen-digit one. Flagging those would mean a check that fails on correct output every time.
What survives that trade is a scan that looks only at the leading digit run of a field, because a PAN occupies the start of the field that carries it. In tag 5A the number comes first and padding follows; in tag 57 the number comes first and a separator follows. The scan also throws out runs that cannot be card numbers whatever Luhn says. A run of identical digits, which always passes because it sums to zero, is discarded. So is anything starting with a zero, because ISO/IEC 7812 puts every payment scheme in the first six major industry identifiers.
And it keeps examining the two places where a regression would hide. One is a field that is supposed to carry an account number and might not have been replaced. The other is any tag the tool does not recognize at all. A new capture using a proprietary tag is exactly the case an allowlist cannot reason about, so unknown is treated as suspect rather than as safe.
What the law already says about this#
Capture your own cards, or cards you have written permission to test. That sounds like boilerplate and is not. The data is regulated in most jurisdictions regardless of intent, and "it was for research" describes your motive, not your legal position. Test cards from the schemes exist so that this question never has to come up.
There is also a written rule, and it is unusually blunt for a compliance document. PCI DSS states that "The full contents of any track are not stored upon completion of the authorization process", and its own applicability note then lists the handful of elements that may be kept from a track record. A capture sitting on a laptop is storage. Nobody has to have looked at it for that sentence to apply.
The next ten minutes#
| Do this | Because |
|---|---|
| Move the raw file out of any synced or versioned folder | Every minute it sits there is another backup, another clone, another machine. |
| Add the raw directory to your ignore file first | An ignore rule written before the first commit is a guarantee. Written after, it is a hope. |
| Sanitize into a new file, never in place | In-place editing means a crash halfway leaves you unable to tell which state you are in. |
| Run a shape scan over the output before anything else sees it | It is the only check that still works once the original is gone, which it will be. |
| Check the screenshots too | A sanitized file and an unsanitized image of it in the same report is the most common version of this mistake. |