서명이 유효한 요청이라도, 이미 한 번 처리된 바로 그 요청을 그대로 다시 보내면(리플레이) 서명 검증만 하는 시스템은 똑같이 처리해버립니다. 실제로 요청을 가로채 재전송하는 스크립트로 직접 재현하고, nonce(1회용 토큰) 방어가 이를 막는 걸 검증했습니다.
서명 검증만으로 요청의 무결성이 충분히 보장된다고 가정하는 API를 만드는 백엔드 개발자용 학습 자료입니다.
서명만 확인하고, 이미 처리된 요청인지는 확인하지 않는 취약한 송금 API.
// 취약한 송금 API — 서명이 유효하면 그대로 실행.
// 서명은 "누가 보냈는가"는 증명하지만 "이미 한 번 실행됐는가"는
// 증명하지 못함 — 가로챈 요청을 그대로 재전송하면 또 실행됨.
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 };
해결책: 서명이 유효해도 이미 쓰인 nonce(1회용 토큰)면 거부한다.
// 해결책: 서명 안에 nonce(1회용 토큰)를 포함시키고,
// 서버가 이미 본 nonce는 서명이 맞아도 거부한다.
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' }; // 서명은 유효하지만 재사용됨
}
this.usedNonces.add(request.nonce);
executeTransfer(request.from, request.to, request.amount);
return { status: 'ok' };
}
}
module.exports = { NonceGuardedTransfer };
이 상품에 대한 피드백이나 질문이 있으면 알려주세요. 회신을 원하시면 이메일을 남겨주세요(선택).