Belajar Axum - Security Best Practices
Series/Belajar Axum/Episode 18
Episode 18 of 28

Belajar Axum - Security Best Practices

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.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

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.

Prinsip: Defense-in-Depth

Jangan mengandalkan satu lapisan pertahanan. Tiga lapis untuk satu serangan:

100%

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.

SQL Injection: Pertahanan yang Sudah Ada

Kabar baiknya: CRUD di episode 9 sudah kebal terhadap SQL injection karena memakai placeholder $1 + .bind(). Mari pastikan kebiasaan ini terjaga:

Benar vs salah
// 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.

Validasi dan Sanitasi Input

Episode 14 sudah membangun validasi. Security menambahkan perspektif: selalu perlakukan input sebagai tidak tepercaya, termasuk:

  • Header (User-Agent, Referer, X-Forwarded-For) — jangan pernah di-reflect mentah ke response tanpa escaping.
  • Query string dan path segment.
  • Body dari semua format (JSON, form, multipart).

Aturan praktis: jangan pernah memercayai klien untuk sesuatu yang bisa diverifikasi di server. Contoh sanitasi umum:

Sanitasi input
// 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: Ketika Data Menjadi Code

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:

Response 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: Serangan Melalui Browser

CSRF terjadi ketika browser korban (yang sudah login) mengirim request berbahaya ke API kita dari situs lain. Dua pertahanan utama:

  1. Cookie SameSite — cookie session diberi SameSite=Strict/Lax, sehingga browser tidak mengirim cookie pada request lintas-situs.
  2. Token CSRF — untuk form/state-changing request.

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:

Cookie session yang aman
// Contoh atribut cookie session yang benar
set-cookie: session=...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400

Kombinasi 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.

Security Headers: Hardening di Lapisan HTTP

Sebagian besar pertahanan browser datang dari response headers. tower-http menyediakan set-header untuk ini — pasang satu set yang direkomendasikan:

Tambah fitur set-header
cargo add tower-http --features set-header
Security headers
use 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:

HeaderNilaiFungsi
X-Content-Type-OptionsnosniffCegah browser menebak MIME
Referrer-Policyno-referrer / strict-origin-when-cross-originBatasi info referrer
X-Frame-OptionsDENY (atau frame-ancestors CSP)Cegah clickjacking
Strict-Transport-Security (HSTS)max-age=31536000; includeSubDomainsPaksa HTTPS (episode 20)

Untuk header yang nilainya berubah per environment (CSP), gunakan middleware from_fn dengan config (episode 8 dan 16).

Menetapkan Ukuran dan Batas

Serangan tidak selalu cerdas — kadang cukup "banjir". Batasi resource per request:

Batas ukuran body
use axum::extract::DefaultBodyLimit;
 
Router::new()
    .route("/posts", axum::routing::post(create_post))
    .layer(DefaultBodyLimit::max(1_000_000)) // 1 MB

Batas body default Axum adalah 2 MB. Untuk endpoint upload, atur lebih besar di route yang bersangkutan (bukan global) — kita bahas multipart di episode 23.

Hardening Endpoint: Checklist

Terapkan semua prinsip di atas ke satu endpoint sebagai checklist hardening:

Endpoint yang di-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:

  • Body limit sesuai kebutuhan (validasi di episode 14 + limit di sini).
  • Cache policy (no-store untuk endpoint yang menampilkan data privat).
  • Rate limiting untuk endpoint publik — topik episode 19.
  • Auth & RBAC (episode 13) untuk endpoint yang menangani data user.

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.

Penutup

Pada episode 18 ini aplikasi kalian lebih sulit ditembus:

  • SQL injection diblokir oleh placeholder + bind; query_as! menambah lapisan compile-time check.
  • Input selalu tidak tepercaya: validasi, trim, limit.
  • XSS dicegah dengan memisahkan data dari code (JSON API, bukan HTML).
  • CSRF dimitigasi dengan JWT di header + atribut cookie yang aman.
  • Security headers (nosniff, HSTS, dll) via SetResponseHeaderLayer.
  • Body limit per endpoint + cache policy yang sadar keamanan.

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!

Belajar Axum - Security Best Practices | Belajar Axum