Server dapat membuktikan dengan benar bahwa sebuah request berasal dari client tepercaya tetapi tetap memprosesnya lebih banyak dari yang dimaksudkan. Jika request terautentikasi mengatakan “setujui pembayaran ini” atau “ubah alamat pemulihan ini”, menerima request valid yang sama dua kali dapat menimbulkan masalah keamanan walaupun tidak ada salinan yang dipalsukan.

Ini adalah masalah replay. Replay terjadi ketika pesan yang sebelumnya valid disajikan kembali dan penerima tidak dapat mengetahui bahwa otoritasnya sudah digunakan, atau bahwa pesan tersebut terlalu lama untuk dipercaya.

Tujuan pertahanan bukan sekadar mengautentikasi request sensitif. Sistem harus menentukan kapan setiap request terautentikasi valid dan, jika operasi mengharuskannya, memastikan otorisasi yang sama tidak dapat dikonsumsi dua kali. Freshness dan uniqueness bekerja bersama untuk memberikan properti tersebut.

Autentikasi dan replay resistance menjawab pertanyaan berbeda

Autentikasi menjawab pertanyaan seperti:

Did a trusted principal authorize these request bytes?

Replay resistance menjawab pertanyaan lain:

Is this authenticated request still acceptable now,
and has this particular authorization already been used?

Perbedaan ini penting karena message authentication code, digital signature, atau authenticator kriptografis lain biasanya tetap berhasil diverifikasi ketika pesan terautentikasi yang sama persis disalin. Itu memang diinginkan: verifikasi harus deterministik untuk pesan valid yang sama. Namun, autentisitas saja tidak memberi tahu penerima apakah pesan tersebut baru.

Pertimbangkan command bertanda tangan yang disederhanakan:

operation = "approve-transfer"
transfer_id = "T-4815"
amount = "250.00"
signature = Sign(operation || transfer_id || amount)

Jika signature valid, penerima memiliki bukti bahwa field yang ditandatangani telah diotorisasi oleh signer terkait berdasarkan asumsi key sistem. Tidak ada field yang menyatakan kapan command kedaluwarsa atau apakah otorisasi persis ini sudah pernah diterima.

Penyerang dalam threat model ini tidak perlu membuat signature valid baru. Mereka hanya perlu memperoleh request yang sebelumnya valid melalui channel yang tersedia di lingkungan sistem dan membuatnya dikirim kembali. Replay protection mengurangi nilai request yang disalin tersebut.

Replay protection tidak melindungi signing key yang dicuri, akun yang memang berwenang melakukan tindakan, atau server dengan logika otorisasi yang memberi kewenangan terlalu luas. Masalah tersebut memerlukan kontrol terpisah.

Berikan informasi yang dapat dinilai penerima

Penerima tidak dapat mendeteksi replay dari pesan yang identik jika protocol tidak menyediakan informasi yang membedakan penggunaan yang dapat diterima dari penggunaan berulang atau stale.

Dua properti umum menyediakan informasi tersebut:

  • Freshness membatasi berapa lama request boleh diterima.
  • Uniqueness memberi identitas pada request atau otorisasi sehingga penerima dapat mengenalinya sebagai sudah dikonsumsi.

Keduanya menyelesaikan masalah yang berkaitan tetapi berbeda.

Timestamp dapat menetapkan freshness:

request_id = "7f2c..."
created_at = "2026-09-09T05:30:00Z"
operation = "approve-transfer"
transfer_id = "T-4815"

Authenticator harus mencakup request_id, created_at, operasi, dan setiap parameter yang relevan bagi keamanan. Jika tidak, pihak yang tidak tepercaya mungkin dapat mengubah field yang tidak diperiksa tanpa membatalkan bukti autentikasi.

Penerima dapat menolak request yang timestamp terautentikasinya berada di luar time window yang diizinkan. Ini membatasi berapa lama request yang tertangkap tetap dapat diterima, dengan asumsi clock dan aturan validasi bekerja sebagaimana mestinya.

Namun, freshness window lima menit tetap memungkinkan request valid yang sama tiba dua kali dalam lima menit tersebut. Freshness mempersempit replay window; freshness sendiri tidak menegakkan single use.

Gunakan identifier unik ketika penerimaan duplikat penting

Untuk operasi yang seharusnya mengonsumsi satu otorisasi hanya sekali, berikan identifier unik yang cukup tidak dapat diprediksi atau collision-resistant sesuai desain protocol. Penerima mencatat konsumsi yang berhasil dan menolak upaya berikutnya untuk mengonsumsi identifier yang sama.

Alur konseptualnya:

receive request
    |
    v
verify authentication evidence
    |
    v
check freshness and request fields
    |
    v
atomically mark request_id as consumed
    |
    v
perform the protected state transition

Kata atomically penting. Pemeriksaan dan perubahan state yang mereservasi atau mengonsumsi identifier harus bertindak sebagai satu keputusan yang tidak dapat dipisahkan dari sudut pandang aplikasi.

Implementasi rapuh terlihat seperti ini:

if request_id is not in used_requests:
    perform_sensitive_action()
    add request_id to used_requests

Ini hanya pseudocode pembelajaran. Masalahnya adalah urutan: dua worker dapat sama-sama melihat identifier belum ada sebelum salah satunya mencatatnya. Keduanya kemudian dapat melakukan tindakan.

Desain produksi sebaiknya menggunakan primitive storage yang dapat menegakkan uniqueness atau melakukan conditional state transition secara atomik. Mekanisme tepat bergantung pada datastore, misalnya unique constraint, compare-and-set, atau transaction dengan isolation dan constraint yang benar-benar mencegah dua consumer menang.

Properti keamanan berasal dari atomic state transition, bukan dari nama fitur database.

Ikat metadata replay ke pesan terautentikasi

Request identifier atau timestamp hanya membantu jika penyerang tidak dapat menggantinya sambil mempertahankan bukti autentikasi yang valid.

Misalkan protocol hanya menandatangani operasi dan jumlah:

Sign(operation || amount)

namun mengirim request_id di samping field tersebut tanpa mengautentikasinya. Penerima yang mempercayai identifier unsigned sebagai replay key telah memisahkan keputusan replay dari pesan terautentikasi. Request yang disalin dapat disajikan dengan identifier berbeda sambil mempertahankan signature asli yang valid.

Sebaliknya, definisikan pesan terautentikasi kanonis yang mencakup metadata terkait replay dan data operasi yang dilindungi:

Sign(request_id || created_at || operation || resource_id || parameters)

Ini contoh konseptual, bukan format serialisasi portabel. Protocol nyata harus mendefinisikan encoding, urutan field, tipe, dan aturan canonicalization yang tidak ambigu agar sender dan receiver mengautentikasi byte yang persis sama.

Prinsipnya portabel: jika sebuah field mengubah apakah request diterima, ikat field tersebut ke bukti autentikasi.

Tentukan apakah Anda memerlukan deduplication atau otorisasi single-use

Tidak setiap request duplikat merupakan replay keamanan. Network melakukan retry. Client dapat timeout setelah server melakukan commit tetapi sebelum response tiba. Queue dapat mengirim pesan lebih dari sekali. Sistem yang robust harus membedakan penanganan retry yang sah dari pemberian otoritas dua kali.

Untuk beberapa operasi, idempotency sudah cukup. Operasi idempotent dirancang agar pengulangan logical request yang sama menghasilkan state yang dimaksudkan, bukan mengulangi side effect. Menetapkan status resource ke nilai tertentu, misalnya, biasanya lebih mudah dibuat idempotent daripada instruksi “increment balance by 10”.

Idempotency key juga dapat mengaitkan retry dengan hasil operasi sebelumnya. Namun, fitur idempotency hanya menjadi replay defense jika semantik keamanannya cukup kuat untuk tindakan yang dilindungi: key harus terikat ke principal dan makna request yang relevan, disimpan selama periode yang diperlukan, dan ditangani secara atomik.

Untuk otorisasi single-use berdampak tinggi, perlakukan konsumsi sebagai bagian dari authorization state, bukan sekadar kemudahan retry. Penerima harus dapat menyatakan bahwa otoritas tersebut valid dan sudah dikonsumsi.

Perbedaan ini mencegah kesalahan umum berupa penggunaan retry cache berumur pendek lalu menganggapnya memberikan semantik single-use permanen.

Pilih retention period dari kebutuhan keamanan

Mengingat setiap request identifier selamanya biasanya tidak perlu dan dapat membuat storage tanpa batas. Namun, menghapus identifier terlalu dini dapat membuat request terautentikasi lama tampak baru kembali.

Aturan retention yang aman bergantung pada protocol.

Jika request hanya diterima dalam freshness window terbatas, replay record umumnya perlu tetap efektif cukup lama agar request yang sebelumnya diterima tidak dapat menjadi dapat diterima kembali setelah record hilang. Pertimbangkan clock tolerance dan processing delay.

Jika authorization token tetap valid sampai digunakan atau dicabut secara eksplisit, cache deduplication berbasis waktu yang pendek mungkin tidak cukup. Consumed state mungkin harus hidup selama otorisasi tersebut masih dapat diterima.

Nyatakan invariant secara langsung:

A consumed authorization must remain recognizable as consumed
for every period in which that authorization could otherwise be accepted.

Dengan demikian, cleanup storage menjadi keputusan keamanan, bukan setting cache arbitrer.

Tangani retry tanpa membuka kembali otoritas

Client dapat melakukan retry secara sah karena tidak menerima response pertama. Hanya mengembalikan “already used” dapat membuat client tidak mengetahui apakah operasi awal berhasil.

Jika sesuai, simpan metadata hasil non-sensitif secukupnya bersama identifier yang dikonsumsi untuk menjawab retry secara konsisten. Server, misalnya, dapat mengembalikan operation identifier dan final status yang sama tanpa melakukan protected transition lagi.

Desain ini memungkinkan client pulih dari hasil network yang tidak pasti sekaligus mempertahankan semantik single-use.

Perhatikan isi replay record. Record tersebut tidak boleh menjadi penyimpanan baru untuk secret, payload sensitif lengkap, atau personal data yang tidak perlu. Simpan hanya yang diperlukan untuk menegakkan invariant dan mendukung perilaku retry yang dimaksud.

Penanganan kegagalan adalah bagian dari kontrol

Replay state menciptakan dependency operasional. Jika store untuk memeriksa konsumsi tidak tersedia, aplikasi harus menentukan apa yang terjadi pada request sensitif.

Untuk tindakan berdampak tinggi dengan kebutuhan keamanan “accept this authorization at most once”, melewati replay check saat outage melanggar kebutuhan tersebut. Fail closed dapat sesuai walaupun mengurangi availability.

Untuk operasi berdampak lebih rendah, trade-off lain mungkin masuk akal. Yang penting adalah membuat degraded behavior eksplisit dan mengujinya. Outage tidak boleh secara tidak sengaja mengubah “single use” menjadi “unlimited use”.

Pertimbangkan juga partial failure. Jika server mencatat request sebagai consumed lalu gagal sebelum business state transition di-commit, client mungkin tidak dapat retry. Jika business transition di-commit lebih dulu dan consumption dicatat kemudian, crash di antaranya dapat memungkinkan eksekusi duplikat.

Jika model storage memungkinkan, tempatkan consumption dan protected state transition dalam atomic transaction yang sama. Jika keduanya harus melintasi sistem, mungkin tidak ada transaction sederhana yang mencakup keduanya. Dalam kasus tersebut, desain workflow menggunakan durable operation state, idempotent processing, atau mekanisme lain yang membuat recovery eksplisit. Jangan menyembunyikan consistency problem di balik replay-cache lookup.

Verifikasi properti dengan pengujian concurrency dan expiry

Happy-path test yang mengirim satu request valid hanya membuktikan sedikit tentang perilaku replay. Uji boundary yang dapat merusak jaminan.

Kirim request terautentikasi yang sama dua kali dan pastikan protected effect terjadi hanya sesuai semantik yang dimaksud. Kemudian kirim duplikat secara concurrent agar dua worker berlomba mengonsumsi identifier yang sama. Storage layer harus hanya mengizinkan satu winning state transition ketika single use diwajibkan.

Uji timestamp tepat di dalam dan di luar freshness boundary. Uji penanganan clock skew dengan tolerance yang benar-benar diizinkan protocol. Uji apa yang terjadi ketika replay-state storage tidak tersedia, ketika aplikasi restart, dan ketika replay record lama dibersihkan.

Terakhir, pastikan perubahan request_id, timestamp, operation, resource identifier, atau protected parameters membatalkan bukti autentikasi. Ini memeriksa bahwa replay metadata dan makna request benar-benar terikat bersama.

Pahami apa yang tidak diselesaikan replay resistance

Replay resistance membatasi penggunaan ulang bukti autentikasi yang valid. Ia tidak menentukan apakah signer seharusnya diizinkan melakukan tindakan. Receiver tetap memerlukan authorization check normal untuk principal, resource, dan operation.

Replay resistance juga tidak menggantikan transport protection. Encryption in transit dapat mengurangi peluang pesan diamati di network, sedangkan replay resistance membatasi apa yang dapat dilakukan pesan valid yang disalin jika diperoleh melalui channel dalam threat model.

Request identifier juga tidak membuat authenticator yang predictable atau forgeable menjadi lebih kuat. Request tetap membutuhkan autentikasi yang sound. Replay protection menambahkan properti terpisah di atas fondasi tersebut.

Untuk operasi sederhana berdampak rendah yang secara alami idempotent dan sudah dilindungi protocol dengan replay semantics yang sesuai, menambahkan custom single-use store dapat menciptakan kompleksitas tanpa pengurangan risiko yang berarti. Untuk sensitive state change, operasi finansial, perubahan credential, atau tindakan lain dengan konsekuensi nyata dari otoritas duplikat, replay semantics eksplisit lebih mudah dianalisis daripada berharap retry tidak pernah terjadi.

Kesimpulan

Signature atau authenticator yang valid membuktikan sesuatu tentang siapa yang mengotorisasi pesan dan apa yang diautentikasi. Itu tidak otomatis membuktikan bahwa pesan tersebut baru.

Rancang replay resistance berdasarkan properti yang benar-benar dibutuhkan operasi. Gunakan freshness data terautentikasi untuk membatasi berapa lama request dapat diterima. Gunakan unique identifier terautentikasi dan atomic consumption ketika otorisasi harus single-use. Pertahankan consumption state selama otorisasi lama masih dapat diterima, serta definisikan perilaku outage dan retry tanpa melemahkan invariant tersebut.

Pertanyaan praktis pada boundary request sensitif adalah: jika request valid yang persis sama tiba lagi, apa yang membuat penggunaan kedua tidak berbahaya atau dapat ditolak? Sistem yang dapat menjawabnya secara presisi telah menjadikan replay handling sebagai kontrol keamanan eksplisit, bukan kebetulan implementasi.