1. Home
  2. Insights
  3. Security
  4. Replay Attack กับ Duplicate Request ต่างกันยังไง และกันยังไงให้ครบ
Security

Replay Attack กับ Duplicate Request ต่างกันยังไง และกันยังไงให้ครบ

อธิบายความต่างระหว่าง replay attack กับ duplicate request ในระบบจริง ว่าแต่ละแบบเกิดจากอะไร เสี่ยงต่างกันอย่างไร และควรใช้ signature verification, idempotency, rate limiting และ audit trail ร่วมกันแบบไหน

Replay Attack กับ Duplicate Request ต่างกันยังไง และกันยังไงให้ครบ

เวลาระบบเจอ request ซ้ำ หลายทีมมักเรียกรวม ๆ ว่า “โดนยิงซ้ำ” หรือ “request ซ้ำ” แล้วพยายามแก้ด้วยวิธีเดียว แต่ในระบบจริง request ซ้ำมีได้อย่างน้อยสองลักษณะที่หน้าตาคล้ายกันแต่ความหมายต่างกันมาก

แบบแรกคือ Duplicate Request
อีกแบบคือ Replay Attack

ถ้าแยกสองอย่างนี้ไม่ออก เรามักจะป้องกันไม่ครบ เพราะปัญหาหนึ่งเกี่ยวกับความถูกต้องของ state และความทนทานของระบบ distributed ส่วนอีกปัญหาหนึ่งเกี่ยวกับความน่าเชื่อถือของคำขอและความเสี่ยงด้าน security

ลองนึกภาพกรณีต่อไปนี้

  • mobile app timeout แล้ว retry request เดิม
  • user กดปุ่ม submit ซ้ำสองรอบ
  • reverse proxy ส่งคำขอเดิมซ้ำเพราะ upstream fail
  • provider ส่ง webhook event เดิมอีกรอบ
  • attacker ดัก request ที่ถูกต้องไว้แล้วพยายามส่งซ้ำภายหลัง

ทุกอย่างนี้ดูคล้ายกันตรงที่ “ระบบได้รับ request ที่หน้าตาเหมือนเคยเจอมาแล้ว” แต่เหตุผลและวิธีรับมือไม่เหมือนกัน

บทความนี้อธิบายว่า replay attack กับ duplicate request ต่างกันอย่างไร เสี่ยงต่างกันอย่างไร และควรป้องกันอย่างไรให้ครบ

TL;DR

สรุปให้สั้นที่สุดได้แบบนี้

Duplicate request คือคำขอซ้ำที่มักเกิดจากพฤติกรรมปกติของ client, network หรือ distributed system
Replay attack คือการนำคำขอที่เคยถูกต้องกลับมาใช้ซ้ำโดยมีเจตนาหรือในบริบทที่ไม่ควรยอมรับ

ดังนั้น

  • duplicate request ต้องใช้แนวคิดอย่าง idempotency และ state correctness
  • replay attack ต้องใช้แนวคิดอย่าง signature verification, timestamp, nonce, request expiry และ validation ของความน่าเชื่อถือ

ระบบจริงจำนวนมากต้องมีทั้งสองชั้นพร้อมกัน

Duplicate Request คืออะไร

Duplicate request คือคำขอซ้ำที่เกิดขึ้นจากสถานการณ์ปกติของระบบหรือผู้ใช้ โดยไม่ได้แปลว่ามีผู้โจมตีเสมอไป

ตัวอย่างที่เจอบ่อย เช่น

  • user กดปุ่ม submit ซ้ำ
  • mobile app timeout แล้ว retry
  • browser refresh หน้าเดิมแล้วส่ง form ซ้ำ
  • job worker retry หลังเกิด transient failure
  • integration layer ส่ง request ซ้ำเพราะไม่มั่นใจว่ารอบแรกสำเร็จหรือยัง
  • webhook provider ส่ง event เดิมซ้ำเพื่อการันตี delivery

จุดสำคัญคือ duplicate request มักเป็นเรื่องของ reliability และ correctness มากกว่าเรื่อง malicious intent โดยตรง

ปัญหาของมันคือ ถ้าระบบไม่ออกแบบให้ทนต่อ request ซ้ำ มันอาจทำให้

  • charge payment ซ้ำ
  • สร้าง order ซ้ำ
  • refund ซ้ำ
  • ส่ง email ซ้ำ
  • สร้าง workflow ซ้ำ
  • เปลี่ยน state ซ้ำจนเพี้ยน

Replay Attack คืออะไร

Replay attack คือการนำ request หรือ message ที่เคยถูกต้องจริงในอดีต กลับมาส่งซ้ำใหม่ในเวลาหรือบริบทที่ไม่ควรถูกยอมรับ

คำว่า “attack” สำคัญตรงที่มันมีมิติด้านความน่าเชื่อถือและการถูกนำไปใช้ผิดบริบท ไม่ใช่แค่ request มาถึงซ้ำเฉย ๆ

ตัวอย่างเช่น

  • attacker ได้ webhook payload เดิมที่มี signature ถูกต้อง แล้วพยายามส่งซ้ำใหม่
  • attacker จับ signed request ที่เคยใช้ได้ แล้ว replay ภายหลัง
  • request เดิมถูกส่งซ้ำข้ามช่วงเวลาที่ควรหมดอายุไปแล้ว
  • message เดิมถูก replay เพื่อหวังให้ระบบทำ side effect อีกรอบ

สิ่งที่อันตรายคือ request อาจยัง “ดูถูกต้อง” ในหลายมิติ เช่น structure ถูก, signature ถูก, payload ถูก แต่ไม่ควรถูกยอมรับอีกแล้วในบริบทปัจจุบัน

ความต่างหลักระหว่างสองอย่างนี้

ถ้าจะสรุปแบบตรงไปตรงมา ความต่างอยู่ที่ “ต้นเหตุ” และ “มุมที่เราต้องป้องกัน”

Duplicate Request

ต้นเหตุมักมาจาก

  • retry
  • timeout
  • reconnect
  • duplicate submit
  • at-least-once delivery
  • integration behavior ปกติ

มุมที่ต้องป้องกันคือ

  • side effect ซ้ำ
  • state เพี้ยน
  • workflow ถูก trigger ซ้ำ
  • resource ถูกสร้างซ้ำ

Replay Attack

ต้นเหตุมักมาจาก

  • การนำ request เดิมกลับมาใช้ใหม่
  • การใช้ message ที่เคย valid ในเวลาที่ไม่ควร valid แล้ว
  • การอาศัยว่า server ตรวจแค่ “request นี้เคยถูกต้องไหม” แต่ไม่ตรวจว่า “ยังควรถูกยอมรับไหมตอนนี้”

มุมที่ต้องป้องกันคือ

  • ความน่าเชื่อถือของ request
  • การหมดอายุของคำขอ
  • uniqueness ของ message
  • การใช้ signed request ซ้ำ
  • การนำ event หรือ callback ที่เคยถูกต้องกลับมา invoke ใหม่

ทำไมแค่ idempotency อย่างเดียวไม่พอ

หลายทีมพอเจอ request ซ้ำก็มักนึกถึง idempotency ก่อน ซึ่งถูกต้องในหลายกรณี แต่ถ้าระบบต้องรับคำขอจาก external source หรือ signed callback คุณต้องระวังให้มากขึ้น

idempotency ช่วยตอบคำถามว่า

  • request นี้เคยทำไปแล้วหรือยัง
  • ถ้าคำขอเดิมเข้ามาอีกควรคืนผลเดิมไหม
  • จะกัน side effect ซ้ำยังไง

แต่ idempotency ไม่ได้ตอบคำถามว่า

  • request นี้มาจากแหล่งที่เชื่อถือได้จริงไหม
  • request นี้ยังอยู่ในช่วงเวลาที่ควรยอมรับไหม
  • มีคนกำลังนำ signed message เดิมกลับมาใช้ซ้ำหรือไม่

ดังนั้นสำหรับ replay attack คุณยังต้องมีชั้นอื่นเพิ่ม เช่น

  • signature verification
  • timestamp validation
  • nonce
  • expiry window
  • event/message deduplication
  • scope validation

ทำไมแค่ signature verification อย่างเดียวก็ไม่พอ

ในอีกด้านหนึ่ง หลายระบบตรวจ signature แล้วคิดว่าปลอดภัยแล้ว แต่ replay attack เกิดได้แม้ signature ถูกต้อง

เพราะ signature บอกได้แค่ว่า payload นี้มาจากผู้ส่งที่ถือ secret หรือ private key ที่ถูกต้องในตอนที่เซ็น มันไม่ได้บอกว่า request นี้ “ใหม่” หรือ “ยังควรถูกยอมรับ” เสมอไป

สมมติ webhook provider ส่ง event มาและมี signature ถูกต้องจริง ถ้ามีคน replay request เดิมอีกรอบ แล้วระบบตรวจแค่ signature อย่างเดียว ระบบอาจยอมรับ request เดิมซ้ำอีกครั้ง

ดังนั้น signature verification ต้องทำงานร่วมกับกลไกอย่างน้อยหนึ่งอย่างต่อไปนี้

  • ตรวจ timestamp ว่าไม่เก่าเกิน window ที่ยอมรับ
  • ตรวจ event id หรือ message id ว่าเคยประมวลผลไปแล้วหรือยัง
  • ตรวจ nonce ว่าเคยใช้แล้วหรือไม่
  • ตรวจ business state ว่ายังทำ action นี้ซ้ำได้หรือไม่

ตัวอย่างให้เห็นภาพ

กรณี Duplicate Request

user กด POST /payments แล้ว network ช้า
client timeout แต่ server จริง ๆ ทำ payment สำเร็จแล้ว
client retry request เดิมอีกครั้ง

นี่คือ duplicate request

สิ่งที่ระบบควรทำคือ

  • รู้ว่า request นี้คือความพยายามครั้งเดิม
  • ไม่ charge ซ้ำ
  • คืนผลลัพธ์เดิมหรือ reject อย่างมีความหมาย

เครื่องมือหลักคือ idempotency, unique constraints และ state checks

กรณี Replay Attack

provider ส่ง webhook payment.succeeded มาที่ระบบของคุณพร้อม signature ถูกต้อง
ต่อมามีผู้ไม่หวังดีหรือมีระบบกลางบางจุด replay request เดิมอีกรอบ

ถ้าระบบของคุณตรวจแค่ signature แต่ไม่ตรวจ event id หรือเวลาหมดอายุ ระบบอาจ process event เดิมอีกครั้ง

นี่คือ replay attack ในเชิงผลลัพธ์ แม้ payload เดิมจะเคย valid จริง

เครื่องมือหลักคือ signature verification, timestamp window, event deduplication และ state validation

จะป้องกัน Duplicate Request ยังไง

การป้องกัน duplicate request ควรเริ่มจากการยอมรับก่อนว่าระบบ distributed มี request ซ้ำเป็นเรื่องปกติ ไม่ใช่เรื่องผิดปกติเสมอไป

แนวทางหลักที่ใช้จริงคือ

1) Idempotency Key

เหมาะกับ request ที่ client ส่งเข้ามา โดยเฉพาะ endpoint ที่มี side effect เช่น

  • POST /payments
  • POST /orders
  • POST /refunds
  • POST /subscriptions

เป้าหมายคือให้ request เดิมไม่สร้างผลกระทบซ้ำ

2) Unique Constraints และ Transactional Guarantees

บางกรณี idempotency อย่างเดียวไม่พอ ถ้า storage ด้านในไม่มี uniqueness ที่สอดคล้องกับ business rule ก็ยังมี race condition ได้

3) State Validation

ถึง request จะมาอีกครั้ง ระบบก็ควรตรวจว่าขั้นตอนนั้นยังทำได้อยู่ไหม เช่น order นี้ถูกจ่ายไปแล้วหรือยัง, refund นี้ถูก approve แล้วหรือยัง

4) Event Deduplication

ในกรณี webhook หรือ message ingestion ควรเก็บ event id หรือ delivery id เพื่อกัน processing ซ้ำ

จะป้องกัน Replay Attack ยังไง

Replay attack ต้องมองว่าปัญหาไม่ได้อยู่แค่ “คำขอซ้ำ” แต่อยู่ที่ “คำขอนี้ยังควรถูกยอมรับไหม”

แนวทางหลักมีดังนี้

1) Signature Verification

ตรวจว่าข้อความมาจาก sender ที่เชื่อถือได้จริง

2) Timestamp Validation

คำขอที่ signed มาแล้วควรมีเวลาประกอบ และ server ควรปฏิเสธ request ที่เก่าเกิน acceptable window เช่น 5 นาที หรือ 10 นาที ตามลักษณะระบบ

3) Nonce หรือ Message ID Uniqueness

ถ้ามี nonce, request id หรือ event id ที่ไม่ควรถูกใช้ซ้ำ ก็ควรเก็บและตรวจว่าค่านี้เคยใช้แล้วหรือยัง

4) Request Expiry

คำขอบางประเภทควรมีอายุสั้นชัดเจน เช่น signed upload URL, signed callback, one-time action link

5) Business State Check

แม้ request จะผ่าน verification แล้ว แต่ถ้า state ตอนนี้ไม่สอดคล้องกับ action ก็ควร reject เช่น event เดิมถูก apply ไปแล้ว หรือ resource ไม่อยู่ในสถานะที่ action นี้ควรเกิดได้อีก

Rate Limiting ช่วยตรงไหน และช่วยไม่ถึงไหน

rate limiting ช่วยลดปริมาณ request ที่ถี่เกินไปและช่วยลด abuse ได้ แต่ไม่ได้แก้ replay หรือ duplicate ให้จบด้วยตัวเอง

ตัวอย่างเช่น

  • duplicate request ที่มาไม่ถี่มากนักอาจยังผ่าน rate limit ได้
  • replay attack ที่ทำอย่างพอดี ๆ ก็อาจไม่ชน limit
  • webhook replay หนึ่งครั้งก็อาจสร้างปัญหาได้แม้ไม่ถึงเพดาน rate

ดังนั้น rate limiting ควรถือเป็นชั้นเสริม ไม่ใช่กลไกหลักในการแยก replay กับ duplicate

Webhook คือพื้นที่ที่สองปัญหานี้เจอกันบ่อยที่สุด

Webhook เป็นตัวอย่างที่ดีมาก เพราะมันเจอทั้ง duplicate request และ replay risk พร้อมกัน

ในโลกจริง provider จำนวนมากส่ง event แบบ at-least-once delivery ซึ่งแปลว่า event เดิมอาจถูกส่งซ้ำเป็นเรื่องปกติ นี่คือ duplicate behavior ที่ระบบต้องทนได้

พร้อมกันนั้น ถ้า request ถูก signed และมีคน replay payload เดิม ระบบก็ต้องกัน replay ด้วย

ดังนั้น webhook ที่ดีควรมีอย่างน้อย

  • signature verification
  • timestamp window
  • event id deduplication
  • state-aware processing
  • audit trail ว่ารับ event ไหนและตัดสินใจอย่างไร

ตัวอย่าง Express/Node.js แบบเริ่มต้น

ตัวอย่างนี้ตั้งใจสาธิตแนวคิด ไม่ใช่ implementation production ครบทุกมิติ โดยแยกให้เห็นว่าเราตรวจทั้ง signature, timestamp และ event id เพื่อกัน replay และ processing ซ้ำ

const express = require("express");
const crypto = require("crypto");

const app = express();

/**
 * demo only:
 * production ควรใช้ raw body จริง, durable store และ secret management ที่เหมาะสม
 */
app.use(express.json());

const processedEvents = new Map();
const WEBHOOK_SECRET = "replace-this-secret";
const MAX_AGE_SECONDS = 300;

app.post("/webhooks/provider", (req, res) => {
  try {
    const signature = req.get("X-Signature");
    const timestamp = req.get("X-Timestamp");
    const eventId = req.get("X-Event-Id");

    if (!signature || !timestamp || !eventId) {
      return res.status(400).json({
        error: "Missing required webhook headers"
      });
    }

    const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));

    if (!Number.isFinite(Number(timestamp)) || ageSeconds > MAX_AGE_SECONDS) {
      return res.status(400).json({
        error: "Webhook timestamp expired"
      });
    }

    const rawPayload = JSON.stringify(req.body);
    const expectedSignature = crypto
      .createHmac("sha256", WEBHOOK_SECRET)
      .update(`${timestamp}.${rawPayload}`)
      .digest("hex");

    if (!safeEqual(signature, expectedSignature)) {
      return res.status(401).json({
        error: "Invalid webhook signature"
      });
    }

    const existing = processedEvents.get(eventId);

    if (existing) {
      return res.status(200).json({
        success: true,
        duplicate: true,
        message: "Event already processed"
      });
    }

    processWebhookEvent(req.body);

    processedEvents.set(eventId, {
      processedAt: new Date().toISOString()
    });

    return res.status(200).json({
      success: true
    });
  } catch (error) {
    console.error("Webhook handling failed:", error);
    return res.status(500).json({
      error: "Webhook handling failed"
    });
  }
});

function safeEqual(a, b) {
  const left = Buffer.from(a);
  const right = Buffer.from(b);

  if (left.length !== right.length) {
    return false;
  }

  return crypto.timingSafeEqual(left, right);
}

function processWebhookEvent(payload) {
  console.log("Processing webhook event:", payload.type);
}

app.listen(3000, () => {
  console.log("Server listening on port 3000");
});

โค้ดชุดนี้กำลังช่วยอะไร

จุดแรกคือมันไม่ได้เชื่อ request เพียงเพราะหน้าตาถูก แต่ตรวจ signature ก่อนว่ามาจาก source ที่ถูกต้องจริง

จุดที่สองคือมันมี timestamp window ทำให้ request เก่าที่ถูกนำกลับมาส่งใหม่ไม่ผ่านง่าย ๆ

จุดที่สามคือมันเก็บ eventId เพื่อกัน event เดิมถูก process ซ้ำ แม้จะเป็น duplicate delivery หรือ replay ก็ตาม

สิ่งสำคัญคือสามชั้นนี้ทำงานคนละหน้าที่

  • signature ตอบว่าใครส่งมา
  • timestamp ตอบว่ายังสดพอที่จะยอมรับไหม
  • event dedup ตอบว่าเคยประมวลผลไปแล้วหรือยัง

จุดที่ต้องระวังใน production

1) อย่าใช้ JSON.stringify(req.body) แทน raw body ในระบบที่ signature พึ่งพา raw payload จริง

provider หลายรายเซ็นบน raw request body ถ้า parse JSON ก่อนแล้วค่อย stringify กลับ อาจได้ payload ที่ไม่ตรง original bytes ทำให้ตรวจ signature ผิด

2) อย่าใช้ in-memory store จริงสำหรับ dedup

ถ้ามีหลาย instance หรือ restart แล้วข้อมูลหาย การกัน replay และ duplicate จะไม่เสถียร ควรใช้ Redis, database หรือ durable shared store

3) อย่าคิดว่า event id เพียงอย่างเดียวพอในทุกกรณี

บางระบบต้องเช็ก business state ด้วย เช่น event เดิมเคย apply ไปแล้วหรือ resource อยู่ในสถานะที่ไม่ควรเปลี่ยนอีก

4) อย่าลืม retention policy

ทั้ง nonce, event id, replay window และ dedup records ต้องมีระยะเก็บที่เหมาะสม ไม่ใช่เก็บตลอดไปแบบไม่มีเหตุผล

Audit Trail ช่วยยังไงในเรื่องนี้

เวลาเกิดปัญหา request ซ้ำหรือ event ซ้ำ ทีมมักอยากรู้ว่า

  • request นี้เข้ามากี่ครั้ง
  • ตัดสินว่าเป็น duplicate หรือ replay เพราะอะไร
  • ถูก reject เพราะ signature ผิด, timestamp หมดอายุ หรือ event เคยประมวลผลแล้ว
  • side effect ถูกทำไปก่อนหรือไม่
  • request นี้ผูกกับ resource ไหน

ถ้ามี audit trail หรืออย่างน้อย structured event log ที่ดี การไล่ย้อนหลังจะง่ายขึ้นมาก โดยเฉพาะเวลาต้องตอบคำถามกับทีม ops, security, product หรือ provider ภายนอก

รีวิวแนวทางนี้แบบ production-minded

Correctness

ระบบที่แยก duplicate request ออกจาก replay attack ได้ จะออกแบบ control ได้แม่นกว่า เพราะรู้ว่ากำลังแก้ปัญหา reliability หรือ security อยู่กันแน่

Security

signature verification, timestamp validation และ nonce หรือ event dedup เป็นหัวใจของการลด replay risk โดยเฉพาะใน webhook หรือ signed callback flows

Efficiency

ถ้าปล่อยให้ request ซ้ำเข้ามาโดยไม่ควบคุม ระบบอาจเสียทรัพยากรไปกับการประมวลผลซ้ำ แม้สุดท้าย business state จะไม่เปลี่ยน การมี dedup และ early rejection จึงช่วยทั้งความปลอดภัยและประสิทธิภาพ

Error handling

เวลาปฏิเสธ request ควรแยกเหตุผลให้ชัด เช่น invalid signature, expired timestamp, duplicate event หรือ conflict ทาง business state เพื่อให้ debug ได้ตรงจุด

Checklist สั้น ๆ ก่อนปล่อย endpoint หรือ webhook สำคัญขึ้น production

  • แยกให้ออกว่ากำลังป้องกัน duplicate request หรือ replay attack
  • endpoint ที่มี side effect ใช้ idempotency เมื่อเหมาะสม
  • webhook หรือ signed callback มี signature verification
  • มี timestamp หรือ expiry window
  • มี nonce, event id หรือ message id deduplication
  • มี state validation ภายใน business logic
  • มี rate limiting เป็นชั้นเสริมเมื่อเหมาะสม
  • ใช้ durable shared store สำหรับ dedup
  • มี structured logging หรือ audit trail สำหรับการตัดสินใจแต่ละครั้ง
  • รู้ policy ชัดว่า request แบบไหนควร replay ได้และแบบไหนไม่ควร

บทความที่ควรอ่านต่อ

สรุป

Replay attack กับ duplicate request หน้าตาคล้ายกันตรงที่ request เดิมกลับมาอีกครั้ง แต่สิ่งที่เราต้องระวังต่างกัน

duplicate request เป็นปัญหาที่มักเกิดจากพฤติกรรมปกติของระบบ distributed และต้องออกแบบให้ทนต่อ side effect ซ้ำ
replay attack เป็นปัญหาที่เกี่ยวกับความน่าเชื่อถือของ request และต้องป้องกันไม่ให้คำขอเดิมถูกนำกลับมาใช้ในบริบทที่ไม่ควรยอมรับอีก

สรุปสั้นที่สุดคือ

duplicate request ต้องกันไม่ให้ state เพี้ยน
replay attack ต้องกันไม่ให้คำขอเดิมถูกใช้ซ้ำอย่างไม่ควร

💬 Chat (ตอบเร็ว)