A request with a perfectly valid signature can still be captured and resent as-is — a system that only checks the signature processes it again, identically. I reproduced this with an actual capture-and-replay script and verified a nonce (one-time token) defense stops it.
A learning resource for backend developers building APIs that assume signature verification alone guarantees request integrity.
A vulnerable transfer API that checks the signature but never whether the request was already processed.
// Vulnerable transfer API — a valid signature is executed as-is.
// A signature proves WHO authored a request, not whether it has
// already run — replaying a captured request runs it again.
class SignatureOnlyTransfer {
constructor(verifySignature) {
this.verifySignature = verifySignature;
}
transfer(request) {
if (!this.verifySignature(request)) {
return { status: 'bad_signature' };
}
executeTransfer(request.from, request.to, request.amount);
return { status: 'ok' };
}
}
module.exports = { SignatureOnlyTransfer };
The fix: even a validly-signed request is rejected if its nonce has already been used.
// The fix: a nonce (one-time token) rides inside the signed payload,
// and a nonce the server has already seen is rejected even though
// the signature itself is still perfectly valid.
class NonceGuardedTransfer {
constructor(verifySignature) {
this.verifySignature = verifySignature;
this.usedNonces = new Set();
}
transfer(request) {
if (!this.verifySignature(request)) {
return { status: 'bad_signature' };
}
if (this.usedNonces.has(request.nonce)) {
return { status: 'replayed_nonce' }; // signature valid, but reused
}
this.usedNonces.add(request.nonce);
executeTransfer(request.from, request.to, request.amount);
return { status: 'ok' };
}
}
module.exports = { NonceGuardedTransfer };
Questions or feedback about this product? Leave your email if you'd like a reply (optional).