Rate limit berbasis input latency adalah pendekatan untuk menilai seberapa cepat pengguna mengisi form, lalu memakai sinyal itu sebagai faktor tambahan dalam keputusan keamanan backend. Untuk login, OTP, dan form sensitif, pola jeda antarketikan atau waktu dari form tampil sampai submit sering cukup berguna untuk membedakan interaksi manusia normal dari otomasi yang terlalu cepat, terlalu konsisten, atau tidak masuk akal.
Namun sinyal ini tidak boleh dipakai sendirian. Input latency mudah dipalsukan oleh bot yang lebih canggih, dan pengguna asli bisa terlihat “aneh” karena autofill, password manager, aksesibilitas, jaringan lambat, atau JavaScript yang dimatikan. Karena itu, desain yang aman adalah menjadikan latency sebagai risk signal untuk rate limit bertingkat, bukan sebagai satu-satunya kunci blokir.
Mengapa input latency relevan untuk auth hardening
Inspirasi pengukuran latency sering datang dari dunia sistem operasi dan observabilitas, misalnya bagaimana Linux mengamati jeda event input atau penjadwalan. Di level aplikasi web, kita tidak perlu presisi seperti kernel tracing. Yang kita butuhkan adalah pengukuran yang cukup stabil untuk menjawab pertanyaan praktis:
- Apakah form disubmit terlalu cepat untuk ukuran interaksi manusia?
- Apakah pola ketikan terlalu seragam sehingga tampak dihasilkan skrip?
- Apakah banyak percobaan login dari fingerprint yang sama memiliki latency yang identik?
- Apakah alur OTP diisi dalam waktu yang tidak realistis secara berulang?
Sinyal ini berguna terutama saat bot melakukan salah satu hal berikut:
- Mengirim request langsung ke endpoint backend tanpa melewati UI normal.
- Mengotomasi browser tetapi mengisi field hampir seketika.
- Mengulang pola submit yang sangat konsisten di banyak akun.
- Melakukan credential stuffing dengan kecepatan tinggi namun mencoba “terlihat normal” dari sisi IP.
Di sisi lain, latency bisa menyesatkan bila:
- Browser memakai autofill atau password manager.
- Pengguna copy-paste OTP dari SMS manager atau email client.
- Pengguna memakai assistive technology.
- Perangkat lambat membuat event terkirim terlambat.
- JavaScript diblokir atau gagal dimuat.
Prinsip utama: input latency adalah sinyal probabilistik, bukan bukti. Gunakan untuk menaikkan atau menurunkan level pembatasan, bukan untuk memutus akses secara mutlak tanpa konteks lain.
Membedakan latency manusia vs bot
Sinyal yang relatif berguna
Untuk form login atau OTP, beberapa fitur sederhana biasanya cukup membantu:
- Time to first input: waktu dari form siap dipakai sampai input pertama.
- Time to submit: waktu dari form tampil sampai submit.
- Inter-key delay: jeda antarketikan untuk field yang memang diketik.
- Field transition timing: jeda perpindahan antarfield, misalnya dari username ke password.
- Entropy pola: apakah semua jeda terlalu seragam.
- Paste/autofill indicators: apakah field diisi lewat paste atau autofill.
Bot sederhana sering gagal pada kombinasi ini. Misalnya, request login masuk 30 ms setelah halaman dirender, tanpa event input sama sekali, lalu pola yang sama muncul pada ratusan akun. Itu bukan bukti absolut, tetapi cukup kuat untuk menaikkan skor risiko.
Sinyal yang berbahaya jika dipakai mentah
- Login sangat cepat = bot. Ini sering salah karena password manager bisa mengisi dan submit cepat.
- Tidak ada event keydown = bot. Autofill sah sering memang tidak menghasilkan pola ketikan manusia.
- Semua paste = abuse. Banyak pengguna menyalin OTP secara sah.
- Device tertentu selalu mencurigakan. Browser mobile, in-app webview, atau perangkat aksesibilitas bisa punya karakteristik event yang berbeda.
Karena itu, backend sebaiknya menilai latency dalam konteks sinyal lain: reputasi IP, riwayat akun, jumlah percobaan gagal, fingerprint ringan, ASN, negara, umur sesi, dan keberhasilan challenge sebelumnya.
Desain pipeline event untuk rate limit berbasis input latency
Tujuan pipeline
Pipeline yang baik harus:
- Mengumpulkan event input yang minimum namun cukup informatif.
- Menjaga privasi: jangan kirim raw keystroke atau isi field.
- Menghasilkan fitur ringkas yang bisa dipakai backend secara cepat.
- Tetap aman bila data client hilang, rusak, atau dipalsukan.
Data yang dikirim dari client
Alih-alih mengirim semua event keyboard mentah, lebih aman mengirim ringkasan per interaksi. Contoh payload untuk form login:
{
"form_id": "login_v1",
"rendered_at_ms": 1712345678000,
"submitted_at_ms": 1712345684200,
"fields": {
"username": {
"input_method": "typed",
"first_input_delay_ms": 1200,
"input_count": 12,
"paste_count": 0,
"autofill": false,
"key_interval_stats": {
"count": 11,
"min_ms": 70,
"max_ms": 420,
"avg_ms": 165,
"stddev_ms": 85
}
},
"password": {
"input_method": "autofill",
"first_input_delay_ms": 2800,
"input_count": 1,
"paste_count": 0,
"autofill": true
}
},
"page_session_id": "2d3f2f1a-...",
"js_enabled": true,
"client_clock_skew_hint_ms": 0
}Beberapa catatan penting:
- Jangan kirim karakter yang diketik, scan code, atau nilai field.
- Lebih baik kirim statistik agregat daripada urutan event lengkap.
- Bedakan typed, paste, dan autofill agar backend tidak salah menafsirkan kecepatan tinggi.
- Sertakan page_session_id acak agar event form dan submit bisa dikorelasikan.
Arsitektur backend
Di backend, alurnya bisa dibuat seperti ini:
- Client mengambil halaman login dan menerima page session token.
- Client mengumpulkan metrik latency selama user berinteraksi.
- Saat submit, request login menyertakan kredensial + ringkasan metrik.
- Backend memverifikasi token sesi halaman, menormalisasi metrik, lalu menghitung risk score.
- Risk score dipakai untuk memilih level rate limit, challenge tambahan, atau audit logging.
- Hasil login dan sinyal perilaku disimpan untuk kalibrasi threshold berikutnya.
Biasanya Redis cocok untuk rate limiter dan state singkat, sementara database relasional atau warehouse lebih cocok untuk analitik dan tuning threshold.
Threshold adaptif dan rate limit bertingkat
Jangan pakai satu ambang statis
Kesalahan umum adalah menetapkan aturan seperti “submit di bawah 500 ms pasti bot”. Aturan seperti ini rapuh. Threshold sebaiknya adaptif berdasarkan konteks:
- Tipe form: login, OTP, reset password, ubah email, tambah device.
- Metode input: typed, paste, autofill.
- Riwayat perangkat/sesi: baru atau sudah dikenal.
- Risiko lingkungan: IP baru, negara baru, percobaan gagal tinggi.
- Jam dan pola serangan aktif.
Contoh yang lebih aman:
- Jika password di-autofill dan sesi punya cookie yang sudah dikenal, latency cepat tidak otomatis menambah risiko besar.
- Jika tidak ada event UI sama sekali, tetapi login gagal berulang ke banyak akun dari fingerprint yang sama, risiko naik tajam.
- Jika OTP di-submit hampir instan pada banyak nomor tujuan yang berbeda, tambahkan cooldown lebih agresif.
Contoh model skor sederhana
risk = 0
if js_missing:
risk += 15
if no_input_events and no_autofill_signal:
risk += 30
if time_to_submit_ms < suspicious_floor(form_type):
risk += 20
if key_interval_stddev_ms < low_variance_threshold and input_count > min_keys:
risk += 15
if failed_attempts_by_ip_last_10m > 20:
risk += 25
if failed_attempts_by_fingerprint_last_10m > 10:
risk += 20
if known_device_cookie:
risk -= 20
if successful_logins_recently_from_same_device:
risk -= 10
risk = clamp(risk, 0, 100)Skor ini lalu dipetakan ke respons bertingkat:
- 0-29: proses normal, rate limit dasar.
- 30-59: rate limit lebih ketat dan logging lebih detail.
- 60-79: tambahkan challenge, misalnya CAPTCHA atau verifikasi tambahan.
- 80-100: cooldown sementara, blokir percobaan tertentu, atau minta alur verifikasi yang lebih kuat.
Angka di atas hanya contoh struktur, bukan rekomendasi baku. Di produksi, threshold harus dituning dari data Anda sendiri.
Rate limit bertingkat yang disarankan
Jangan hanya batasi per IP. Abuse login modern sering tersebar di banyak IP. Gabungkan beberapa dimensi:
- Per IP / subnet ringan
- Per akun atau identifier login
- Per device cookie atau fingerprint ringan
- Per page session
- Per ASN atau region jika relevan
Contoh kebijakan:
- Level dasar: 5-10 percobaan per akun dalam jendela pendek.
- Level risiko menengah: tambah delay progresif per fingerprint.
- Level risiko tinggi: cooldown per kombinasi akun + fingerprint + IP.
- Level sangat tinggi: blokir endpoint OTP resend atau wajibkan challenge sebelum submit.
Fingerprint ringan tanpa berlebihan
Untuk kasus ini, fingerprint tidak perlu invasif. Tujuannya hanya mengikat serangkaian percobaan ke konteks yang relatif stabil, bukan mengidentifikasi pengguna secara permanen.
Pilihan yang lebih aman:
- Cookie acak berumur pendek untuk browser yang sama.
- Hash ringan dari kombinasi atribut non-sensitif yang berubah secara periodik.
- Page session token yang ditandatangani server.
Hindari bergantung pada teknik fingerprint agresif yang berisiko privasi tinggi atau mudah patah di browser modern. Kalau memakai hash atribut, simpan hasil ter-hash dan beri masa berlaku singkat. Jangan menyimpan data mentah yang tidak dibutuhkan.
Aturan praktis: jika proteksi bisa tercapai dengan cookie acak sesi dan korelasi request, jangan menambah fingerprint yang lebih invasif.
Contoh implementasi frontend dan backend
Frontend: kumpulkan metrik minimum
const state = {
renderedAt: Date.now(),
pageSessionId: window.PAGE_SESSION_ID,
fields: {}
};
function ensureField(name) {
if (!state.fields[name]) {
state.fields[name] = {
firstInputDelayMs: null,
inputCount: 0,
pasteCount: 0,
autofill: false,
intervals: [],
lastInputAt: null
};
}
return state.fields[name];
}
function onFieldInput(name, event) {
const f = ensureField(name);
const now = Date.now();
if (f.firstInputDelayMs === null) {
f.firstInputDelayMs = now - state.renderedAt;
}
if (f.lastInputAt !== null) {
f.intervals.push(now - f.lastInputAt);
}
f.lastInputAt = now;
f.inputCount += 1;
if (event.inputType === 'insertFromPaste') {
f.pasteCount += 1;
}
}
function markAutofill(name) {
const f = ensureField(name);
f.autofill = true;
}
function summarizeIntervals(intervals) {
if (!intervals.length) return null;
const min = Math.min(...intervals);
const max = Math.max(...intervals);
const avg = intervals.reduce((a, b) => a + b, 0) / intervals.length;
const variance = intervals.reduce((a, b) => a + Math.pow(b - avg, 2), 0) / intervals.length;
return {
count: intervals.length,
minMs: Math.round(min),
maxMs: Math.round(max),
avgMs: Math.round(avg),
stddevMs: Math.round(Math.sqrt(variance))
};
}
function buildLatencyPayload() {
const fields = {};
for (const [name, f] of Object.entries(state.fields)) {
fields[name] = {
first_input_delay_ms: f.firstInputDelayMs,
input_count: f.inputCount,
paste_count: f.pasteCount,
autofill: f.autofill,
key_interval_stats: summarizeIntervals(f.intervals)
};
}
return {
form_id: 'login_v1',
rendered_at_ms: state.renderedAt,
submitted_at_ms: Date.now(),
page_session_id: state.pageSessionId,
js_enabled: true,
fields
};
}Catatan implementasi:
- Deteksi autofill berbeda antarbrowser; jika tidak yakin, anggap sinyal ini opsional.
- Jangan menggantungkan keamanan pada integritas payload client. Anggap semua data bisa dimanipulasi.
- Payload sebaiknya dikirim sebagai bagian dari submit utama, bukan request terpisah yang mudah hilang.
Backend: verifikasi, skor risiko, lalu rate limit
function handleLogin(request) {
const pageSession = verifySignedPageSession(request.body.page_session_id);
const latency = normalizeLatencyPayload(request.body.input_latency);
const ctx = {
ip: request.ip,
accountId: normalizeLoginIdentifier(request.body.username),
fingerprint: getLightweightFingerprint(request),
knownDevice: hasKnownDeviceCookie(request),
pageSessionValid: !!pageSession,
recentIpFailures: redisGetCounter(`login:fail:ip:${request.ip}:10m`),
recentFpFailures: redisGetCounter(`login:fail:fp:${getLightweightFingerprint(request)}:10m`),
recentAccountFailures: redisGetCounter(`login:fail:acct:${normalizeLoginIdentifier(request.body.username)}:10m`)
};
const risk = scoreLoginRisk(latency, ctx);
const limiterDecision = decideRateLimitTier(risk, ctx);
if (limiterDecision.block) {
auditLog('login_blocked', { risk, reason: limiterDecision.reason, ctx, latency });
return tooManyRequestsResponse(limiterDecision.retryAfterSec);
}
const authResult = verifyCredentials(request.body.username, request.body.password);
if (!authResult.ok) {
incrementFailureCounters(ctx);
auditLog('login_failed', { risk, ctx, latency });
return unauthorizedResponse();
}
resetRelevantCounters(ctx);
persistKnownDeviceIfAppropriate(request, authResult.userId);
auditLog('login_success', { risk, ctx, latencySummaryOnly(latency) });
return successResponse(authResult.session);
}Dalam implementasi nyata:
- verifySignedPageSession memastikan submit berasal dari sesi halaman yang memang dibuat server.
- normalizeLatencyPayload membatasi nilai aneh, misalnya waktu negatif atau terlalu besar.
- scoreLoginRisk tidak pernah mengasumsikan payload client itu benar.
- decideRateLimitTier menggabungkan skor latency dengan pembatasan klasik.
Contoh logika scoring yang lebih defensif
function scoreLoginRisk(latency, ctx) {
let risk = 0;
if (!ctx.pageSessionValid) risk += 15;
if (!latency || latency.js_enabled !== true) {
risk += 10;
}
const submitMs = latency?.submitted_at_ms && latency?.rendered_at_ms
? latency.submitted_at_ms - latency.rendered_at_ms
: null;
if (submitMs !== null && submitMs >= 0 && submitMs < 400) {
risk += 20;
}
const usernameStats = latency?.fields?.username?.key_interval_stats;
if (usernameStats && usernameStats.count >= 5 && usernameStats.stddev_ms < 20) {
risk += 10;
}
const noFieldSignals = !latency?.fields || Object.keys(latency.fields).length === 0;
if (noFieldSignals) risk += 10;
if (ctx.recentIpFailures > 20) risk += 20;
if (ctx.recentFpFailures > 10) risk += 20;
if (ctx.recentAccountFailures > 5) risk += 15;
if (ctx.knownDevice) risk -= 15;
return Math.max(0, Math.min(risk, 100));
}Skema data yang praktis
Untuk penyimpanan, pisahkan data operasional cepat dan data analitik.
Redis untuk state singkat
login:fail:ip:{ip}:10m - counter + TTL
login:fail:fp:{fingerprint}:10m - counter + TTL
login:fail:acct:{account}:10m - counter + TTL
login:cooldown:{account}:{fp} - flag + TTL
page_session:{id} - metadata sesi halaman + TTLTabel audit di database
auth_risk_events (
id,
event_time,
event_type, -- login_attempt, login_failed, otp_submit, dll
account_ref_hash,
ip_hash,
fingerprint_hash,
risk_score,
risk_tier,
js_enabled,
page_session_valid,
submit_latency_ms,
username_input_count,
username_key_stddev_ms,
password_autofill,
action_taken, -- allow, throttle, challenge, block
reason_codes, -- array/string kode alasan
request_id
)Beberapa praktik baik:
- Hash identifier sensitif bila tidak perlu disimpan mentah.
- Simpan reason code agar tuning threshold lebih mudah.
- Jangan simpan detail berlebihan yang tidak dipakai untuk operasi atau analitik.
Fallback saat JavaScript dimatikan
Jika JavaScript dimatikan, Anda kehilangan sinyal input latency granular. Sistem tetap harus berfungsi. Pendekatan yang aman:
- Turunkan ke proteksi klasik: rate limit per IP, akun, dan device cookie jika ada.
- Gunakan page session token berbasis server agar tetap ada korelasi antara render dan submit.
- Tambahkan challenge lebih cepat hanya jika sinyal risiko lain juga tinggi.
- Jangan langsung menolak semua submit tanpa JS, kecuali memang kebijakan aplikasi mengharuskannya dan dampaknya dapat diterima.
Untuk form server-rendered, Anda masih bisa mengukur render-to-submit time secara kasar dengan token yang diterbitkan saat halaman dibuat. Ini tidak memberi pola antarketikan, tetapi tetap berguna untuk mendeteksi submit yang terlalu cepat atau replay yang tidak wajar.
False positive, privasi, dan batasan pendekatan
Sumber false positive yang paling sering
- Password manager mengisi field sangat cepat.
- OTP di-paste dari clipboard.
- Pengguna dengan kebutuhan aksesibilitas memakai pola input yang berbeda.
- Browser mobile atau webview tidak konsisten dalam event input.
- Jaringan lambat atau tab background membuat timestamp tampak aneh.
Mitigasinya:
- Gunakan latency untuk throttle atau challenge, bukan blokir permanen.
- Masukkan sinyal “autofill/paste/known device” sebagai faktor yang menurunkan risiko.
- Review false positive dari log sebelum menaikkan agresivitas aturan.
- Sediakan jalur pemulihan yang manusiawi jika challenge gagal.
Pertimbangan privasi
Pengukuran input perilaku menyentuh data yang sensitif secara konteks, walaupun bukan isi kredensial. Karena itu:
- Kumpulkan seminimal mungkin.
- Jangan simpan urutan tombol atau teks input.
- Gunakan retensi pendek untuk data mentah operasional.
- Dokumentasikan tujuan pemrosesan dalam kebijakan privasi internal Anda.
- Batasi akses ke log risiko hanya untuk tim yang membutuhkan.
Keterbatasan yang harus diterima
- Bot canggih bisa mensimulasikan delay manusia.
- Latency sangat dipengaruhi konteks perangkat dan browser.
- Sinyal ini lebih efektif untuk menaikkan biaya serangan massal daripada menghentikan aktor canggih secara total.
Dengan kata lain, rate limit berbasis input latency cocok sebagai lapisan tambahan dalam defense in depth.
Logging, observabilitas, dan debugging
Tanpa observabilitas yang baik, Anda tidak akan tahu apakah threshold terlalu longgar atau terlalu agresif. Minimal, log berikut harus tersedia:
- Risk score dan reason codes
- Tier rate limit yang dipilih
- Apakah JS aktif
- Apakah page session valid
- Ringkasan latency utama, bukan detail berlebihan
- Hasil akhir: allow, throttle, challenge, block
Dashboard yang berguna:
- Distribusi submit latency untuk login sukses vs gagal
- Persentase challenge per browser/device class
- False positive rate berdasarkan known device
- Korelasi antara no-JS submit dan keberhasilan login
Debugging tip:
- Jika banyak pengguna sah ter-throttle, cek apakah password manager dianggap “sangat cepat” lalu selalu diberi penalti.
- Jika bot masih lolos, lihat apakah mereka selalu datang tanpa page session valid atau tanpa event input. Aturan Anda mungkin terlalu toleran.
- Jika data latency banyak kosong, pastikan payload dikirim bersama submit utama dan tidak tergantung request async yang bisa balapan.
Checklist rollout aman di produksi
- Mulai dari mode observasi: hitung skor risiko tetapi jangan memblokir dulu.
- Tambahkan reason codes: setiap penalti harus punya kode penyebab yang jelas.
- Pisahkan by form type: login, OTP, dan reset password punya baseline berbeda.
- Kenali autofill dan paste: jangan langsung menyamakan keduanya dengan bot.
- Gabungkan dengan limiter klasik: IP, akun, fingerprint ringan, dan cooldown.
- Buat threshold adaptif: sesuaikan dengan known device, page session, dan riwayat gagal.
- Sediakan fallback no-JS: tetap aman tanpa mematikan akses pengguna sah.
- Audit privasi: pastikan data yang dikirim dan disimpan benar-benar minimum.
- Uji pada traffic nyata bertahap: aktifkan challenge atau throttle lebih dulu sebelum blokir.
- Review false positive mingguan: terutama pada browser mobile, aksesibilitas, dan password manager.
- Siapkan bypass internal terbatas: untuk investigasi insiden dan dukungan pengguna.
- Dokumentasikan respons insiden: kapan aturan diketatkan saat serangan aktif, dan kapan dikembalikan normal.
Kapan pendekatan ini layak dipakai
Rate limit berbasis input latency layak dipakai jika Anda mengelola endpoint auth yang sering menjadi target abuse, terutama login, OTP resend, reset password, atau perubahan faktor autentikasi. Nilainya paling tinggi ketika digunakan sebagai sinyal tambahan untuk memutuskan throttle, challenge, dan prioritas investigasi.
Jika sistem Anda masih belum punya rate limit dasar per akun/IP, deteksi device sederhana, atau logging auth yang layak, perbaiki itu dulu. Input latency bukan pengganti fondasi keamanan. Tetapi bila fondasi tersebut sudah ada, latency bisa menjadi lapisan yang efektif untuk membedakan interaksi manusia normal dari otomasi massal dengan biaya implementasi yang relatif masuk akal.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!