Mengeraskan API dari serangan umum: sanitasi input, SQL injection, XSS, CSRF, security headers, dan prinsip defense-in-depth — lalu menerapkannya ke endpoint yang sudah dibangun di episode sebelumnya.

Sejauh ini kita fokus membangun fitur. Episode ini bersikap defensif: mengeraskan aplikasi dari serangan yang paling umum di API — SQL injection, XSS, CSRF, dan header yang longgar. Ini bukan episode "menambah fitur", melainkan "menutup pintu" pada yang sudah ada.
Mengapa security didiskusikan sebagai episode tersendiri dan bukan tersebar? Karena banyak pertahanan bersifat lintas-fitur: satu header, satu kebijakan, satu pola di semua endpoint. Memahaminya sebagai satu kesatuan — dengan prinsip defense-in-depth — lebih efektif daripada menambal per kasus.
Jangan mengandalkan satu lapisan pertahanan. Tiga lapis untuk satu serangan:
Contoh nyata SQL injection: lapisan 1 mencegat payload berbahaya; lapisan 2 memastikan payload yang lolos tetap diperlakukan sebagai data; lapisan 3 membatasi kerusakan jika dua lapis pertama gagal. Kita bangun ketiganya di bawah.
Kabar baiknya: CRUD di episode 9 sudah kebal terhadap SQL injection karena memakai placeholder $1 + .bind(). Mari pastikan kebiasaan ini terjaga:
// BENAR — placeholder, data tidak pernah jadi bagian SQL
let post = sqlx::query_as::<_, Post>(
"SELECT * FROM posts WHERE title = $1",
)
.bind(&title)
.fetch_one(&state.db)
.await?;
// SALAH — string concatenation, RAWAN injection
// let query = format!("SELECT * FROM posts WHERE title = '{}'", title);Aturan yang wajib diingat: query apa pun yang menyentuh input user harus lewat placeholder. query_as! (macro compile-time) bahkan menolak query yang bisa diinject karena seluruh query divalidasi terhadap skema — keunggulan ekstra dibanding query_as.
Warning
Hati-hati dengan "escape the string" sebagai solusi — pendekatan ini klasik dan rapuh. Satu-satunya cara yang benar adalah parameterized query (placeholder + bind). Jangan pernah menyusun SQL dari string input dalam bentuk apa pun.
Episode 14 sudah membangun validasi. Security menambahkan perspektif: selalu perlakukan input sebagai tidak tepercaya, termasuk:
Aturan praktis: jangan pernah memercayai klien untuk sesuatu yang bisa diverifikasi di server. Contoh sanitasi umum:
// Trim & limit panjang selalu
let title = body.title.trim();
if title.chars().count() > 120 {
return Err(AppError::BadRequest("judul terlalu panjang".into()));
}
// Jangan pernah mengembalikan input mentah sebagai HTML
// (jika kalian merender HTML server-side — lihat XSS di bawah)XSS terjadi saat input user dirender sebagai HTML tanpa escaping. Untuk API yang melayani frontend SPA (seperti aplikasi kita), pertahanan utama adalah jangan pernah merender data user sebagai HTML di sisi server, dan pastikan API mengembalikan data sebagai data (Content-Type: application/json), bukan HTML:
// AMAN: JSON — data tidak akan dieksekusi browser
async fn search(q: axum::extract::Query<Search>) -> AppResult<Json<Vec<Post>>> {
// q.q tidak dirender sebagai HTML di sini
}
// BERBAHAYA (jika dipakai): mengembalikan input mentah di HTML
// response dengan body format!("<div>{}</div>", user_input)Prinsipnya: separation of data and code. Data user tetap data; browser frontend bertanggung jawab men-escape saat render (React otomatis men-escape teks). API yang benar tidak mencampur input ke dalam markup.
CSRF terjadi ketika browser korban (yang sudah login) mengirim request berbahaya ke API kita dari situs lain. Dua pertahanan utama:
SameSite=Strict/Lax, sehingga browser tidak mengirim cookie pada request lintas-situs.Karena aplikasi kita memakai JWT di header Authorization (episode 13), CSRF sudah lebih tahan: browser tidak otomatis menambahkan header Authorization pada request lintas-situs — hanya cookie yang otomatis. Jika nanti beralih ke cookie-based session, wajib menambahkan perlindungan CSRF:
// Contoh atribut cookie session yang benar
set-cookie: session=...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400Kombinasi HttpOnly (tidak bisa dibaca JS), Secure (hanya HTTPS), dan SameSite=Lax adalah minimum untuk session cookie. Topik session vs JWT dibahas di episode 13; pertegas dengan atribut cookie di atas.
Sebagian besar pertahanan browser datang dari response headers. tower-http menyediakan set-header untuk ini — pasang satu set yang direkomendasikan:
cargo add tower-http --features set-headeruse axum::http::{header, HeaderValue};
use tower_http::set_header::SetResponseHeaderLayer;
fn security_headers() -> SetResponseHeaderLayer<&'static str> {
SetResponseHeaderLayer::overriding(
header::X_CONTENT_TYPE_OPTIONS,
HeaderValue::from_static("nosniff"),
)
}
fn app() -> Router {
Router::new()
.route("/api/health", axum::routing::get(|| async { "ok" }))
.layer(security_headers())
}Header yang sebaiknya selalu ada di aplikasi web:
| Header | Nilai | Fungsi |
|---|---|---|
X-Content-Type-Options | nosniff | Cegah browser menebak MIME |
Referrer-Policy | no-referrer / strict-origin-when-cross-origin | Batasi info referrer |
X-Frame-Options | DENY (atau frame-ancestors CSP) | Cegah clickjacking |
Strict-Transport-Security (HSTS) | max-age=31536000; includeSubDomains | Paksa HTTPS (episode 20) |
Untuk header yang nilainya berubah per environment (CSP), gunakan middleware from_fn dengan config (episode 8 dan 16).
Serangan tidak selalu cerdas — kadang cukup "banjir". Batasi resource per request:
use axum::extract::DefaultBodyLimit;
Router::new()
.route("/posts", axum::routing::post(create_post))
.layer(DefaultBodyLimit::max(1_000_000)) // 1 MBBatas body default Axum adalah 2 MB. Untuk endpoint upload, atur lebih besar di route yang bersangkutan (bukan global) — kita bahas multipart di episode 23.
Terapkan semua prinsip di atas ke satu endpoint sebagai checklist hardening:
use axum::extract::DefaultBodyLimit;
use tower_http::set_header::SetResponseHeaderLayer;
use axum::http::header::{CACHE_CONTROL, HeaderValue};
fn hardened_routes() -> Router {
Router::new()
.route(
"/posts",
axum::routing::post(create_post)
.layer(DefaultBodyLimit::max(1_000_000)),
)
.layer(
SetResponseHeaderLayer::overriding(
CACHE_CONTROL,
HeaderValue::from_static("no-store"),
),
)
}Keputusan per endpoint yang perlu dipertimbangkan:
no-store untuk endpoint yang menampilkan data privat).Note
Security bukan checklist satu kali, melainkan kebiasaan. Untuk setiap fitur baru, tanya: input apa yang masuk? di mana data disimpan? siapa yang boleh akses? dan apa yang bisa dipakai attacker dari ini? Menjawab empat pertanyaan itu per fitur jauh lebih efektif daripada audit besar-besaran setahun sekali.
Pada episode 18 ini aplikasi kalian lebih sulit ditembus:
query_as! menambah lapisan compile-time check.nosniff, HSTS, dll) via SetResponseHeaderLayer.Di episode 19 selanjutnya kita tangani banjir request: rate limiting & DoS protection — token bucket dengan tower-governor, throttling per-IP, dan strategi bertahan dari denial of service. Sampai jumpa di episode 19!