Download Cheat sheet PDF 12 pages · syntax, editors, patterns, Unicode, performance, debugging
Blog

Mask all but the last 4 digits of a credit card number

Logging credit card numbers is a PCI compliance violation. Here's a regex one-liner to mask them properly.

The pattern

Card numbers are 13–19 digits, sometimes formatted with spaces or hyphens. We want to keep the last 4 and replace the rest with asterisks.

function maskCard(s) {
  return s.replace(
    /\b((?:\d[\s-]?){9,15})(\d{4})\b/g,
    (_, prefix, last4) => prefix.replace(/\d/g, "*") + last4
  );
}

maskCard("Card: 4111 1111 1111 1234, expires 12/25");
// "Card: **** **** **** 1234, expires 12/25"

Without preserving spaces

function maskCardSimple(s) {
  return s.replace(/\b\d{12,15}(\d{4})\b/g, "************$1");
}

Works for the 16-digit case. For variable lengths, the first version is more reliable.

Python

import re

CARD_RE = re.compile(r"\b((?:\d[\s-]?){9,15})(\d{4})\b")

def mask_card(s):
    def replacer(m):
        return re.sub(r"\d", "*", m.group(1)) + m.group(2)
    return CARD_RE.sub(replacer, s)

Be careful with logs

This works after the fact for old logs, but the right answer is to never log the raw card number in the first place. Most payment libraries (Stripe, Adyen, PayU) tokenize the card and you only ever see the token plus the last 4 digits.

If you do need to handle raw card numbers in your service, ensure: (1) they're never logged, (2) they're passed only over TLS, (3) they're never persisted unencrypted, (4) your service is in PCI-DSS scope.


← Back to blog