Card testing (a BIN attack) brute-forces the digits after a known BIN (bank identification number), using tiny authorization attempts to find valid card numbers. I reproduced this with the real pentesting tool hydra and verified how a velocity check (rate limit) stops it.
A learning resource for backend developers building payment systems whose card-authorization API has no limit on the number of attempts.
A vulnerable payment API that checks card validity with no limit on attempts.
// Vulnerable payment API — checks a card's validity as many times
// as asked. Each individual charge looks small (or $0), but an
// attacker abuses the response itself as a valid-card finder
// (verified directly with hydra).
class UnlimitedChargeAttempts {
constructor(validCards) {
this.validCards = validCards; // Set<cardNumber>
}
charge(cardNumber) {
if (this.validCards.has(cardNumber)) {
return { status: 'card_valid' };
}
return { status: 'card_declined' };
}
}
module.exports = { UnlimitedChargeAttempts };
The fix: once a source racks up enough failures, every further attempt from it is blocked outright.
// The fix: count failures per source (IP, etc.), and once a source
// crosses a threshold, every further attempt is blocked outright —
// without even looking at the card number.
class VelocityCheckGuard {
constructor(validCards, blockAfter = 3) {
this.validCards = validCards;
this.blockAfter = blockAfter;
this.failuresBySource = new Map();
}
charge(cardNumber, source) {
const fails = this.failuresBySource.get(source) || 0;
if (fails >= this.blockAfter) {
return { status: 'rate_limited' }; // won't say even for a real card now
}
if (this.validCards.has(cardNumber)) {
return { status: 'card_valid' };
}
this.failuresBySource.set(source, fails + 1);
return { status: 'card_declined' };
}
}
module.exports = { VelocityCheckGuard };
Questions or feedback about this product? Leave your email if you'd like a reply (optional).