Bypass autentikasi pada backend monolitik maupun microservices umumnya bersumber dari kegagalan inspeksi runtime, seperti pengembang yang melewatkan middleware guard, kesalahan logika percabangan if session.is_authenticated, atau desinkronisasi flag status. Pendekatan konvensional menyerahkan integritas alur ke kedisiplinan pengembang. Rust memungkinkan jaminan keamanan ini dipindahkan langsung ke compiler.
Melalui Type-State Pattern, status autentikasi dimodelkan sebagai state diskret pada sistem tipe. Handler sensitif hanya menerima session dengan state tipe yang valid. Kesalahan routing atau akses ilegal langsung menggagalkan proses kompilasi.
Prinsip Type-State Pattern dengan Zero-Sized Types
Pola ini memanfaatkan Zero-Sized Types (ZST)—tipe data tanpa representasi memori (0 byte)—untuk menandai state dari suatu entitas. Dikombinasikan dengan std::marker::PhantomData, sebuah struct dapat mengikat parameter generik state tanpa menambah alokasi memori pada runtime.
Unauthenticated: Sesi mentah tanpa verifikasi kredensial.MfaRequired: Lolos validasi kredensial pertama, tertahan sebelum tantangan kedua (TOTP/FIDO2).Authenticated: Verifikasi tuntas. Seluruh handler sensitif terkunci pada state ini.
Implementasi Session dan State Transitions
Kunci keamanan pola ini terletak pada konsumsi kepemilikan (move semantics). Setiap transisi state mengonsumsi self lewat pass-by-value, sehingga instance state lama hancur dan tidak dapat digunakan kembali.
use std::marker::PhantomData;
// Definisi Zero-Sized Types (ZST) sebagai marker
pub struct Unauthenticated;
pub struct MfaRequired;
pub struct Authenticated;
// Struct session generik terhadap State
pub struct Session<State> {
session_id: String,
user_id: Option<u64>,
_state: PhantomData<State>,
}
impl Session<Unauthenticated> {
pub fn new(session_id: String) -> Self {
Session {
session_id,
user_id: None,
_state: PhantomData,
}
}
pub fn authenticate(
self,
user_id: u64,
password_hash: &str,
input_hash: &str,
) -> Result<Session<MfaRequired>, &'static str> {
if password_hash != input_hash {
return Err("Kredensial tidak valid");
}
// Konsumsi instance lama, hasilkan state MfaRequired
Ok(Session {
session_id: self.session_id,
user_id: Some(user_id),
_state: PhantomData,
})
}
}
impl Session<MfaRequired> {
pub fn verify_totp(
self,
valid_totp: bool,
) -> Result<Session<Authenticated>, &'static str> {
if !valid_totp {
return Err("Token MFA salah");
}
// Transisi final ke Authenticated
Ok(Session {
session_id: self.session_id,
user_id: self.user_id,
_state: PhantomData,
})
}
}
impl Session<Authenticated> {
pub fn user_id(&self) -> u64 {
self.user_id.expect("Authenticated session wajib memiliki user_id")
}
}
Membatasi Handler Sensitif di Compile Time
Handler operasi sensitif tidak perlu lagi menyematkan guard assertions berlapis di awal fungsi. Spesifikasikan tipe session yang dibutuhkan secara eksplisit pada signature fungsi.
// Handler sensitif: Menuntut Session<Authenticated>
fn handle_transfer_dana(session: &Session<Authenticated>, amount: u64) {
println!("Eksekusi transfer {} dari user {}", amount, session.user_id());
}
// Simulasi alur request
fn main() {
let raw_session = Session::new("sess_xyz123".to_string());
// Percobaan akses langsung tanpa autentikasi
// handle_transfer_dana(&raw_session, 500000);
// ^ COMPILE ERROR: Mismatched types!
// Alur legal
let mfa_stage = raw_session
.authenticate(101, "hash_valid", "hash_valid")
.expect("Gagal login");
let authed_session = mfa_stage
.verify_totp(true)
.expect("MFA gagal");
// Valid: Tipe cocok dengan Session<Authenticated>
handle_transfer_dana(&authed_session, 500000);
}
Bukti Penolakan Compiler
Jika pengembang mem-bypass verifikasi MFA atau langsung meneruskan raw_session ke handler sensitif, compiler Rust langsung menolak eksekusi:
error[E0308]: mismatched types
--> src/main.rs:60:27
|
60 | handle_transfer_dana(&raw_session, 500000);
| -------------------- ^^^^^^^^^^^^ expected `Session<Authenticated>`, found `Session<Unauthenticated>`
| |
| arguments to this function are incorrect
Celah logic bypass terdeteksi sebelum unit test, pipeline CI, atau kode masuk ke fase deployment.
Zero-Cost Abstraction vs Validasi Runtime
Validasi konvensional berbasis status enum atau boolean field mengorbankan siklus CPU dan rawan regresi.
- Runtime Guard: Memerlukan branching (
if session.status != Status::Authenticated) di setiap entry point request handler. Berpotensi menimbulkan branch misprediction serta rawan bug akibat human error jika pengembang melewatkan pengecekan. - Type-State Pattern: Merupakan zero-cost abstraction. Karena tipe state berupa ZST (
size_of::<Unauthenticated>() == 0), compiler mengoptimalkan dan meng-inline transisi tipe. Tidak ada alokasi heap ekstra, tidak ada runtime type tag, dan footprint memori struct tetap datar setara data primitif yang dibawanya.
Batasan Arsitektural: Type-state pattern beroperasi di dalam domain statis Rust. Ketika session dibaca dari media I/O dinamis (misal Redis atau signed cookie), parsing boundary tetap wajib memetakan payload dinamis ke state konkret melalui pola deserialisasi aman (enum dispatch) di layer middleware sebelum masuk ke core domain.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!