API Security #
API adalah permukaan serangan yang paling menarik di aplikasi modern — ia bisa diakses dari mana saja, mudah diotomatisasi, dan kesalahan kecil dalam desainnya bisa mengekspos data jutaan user. Yang membuatnya lebih mengkhawatirkan: sebagian besar insiden keamanan API bukan disebabkan oleh serangan yang canggih, melainkan oleh pelanggaran prinsip dasar yang seharusnya sudah menjadi standar. IDOR karena tidak ada ownership validation. Privilege escalation karena mass assignment yang tidak di-filter. Data sensitif bocor di error response. Ini bukan kegagalan teknologi — ini kegagalan disiplin. Artikel ini membahas tujuh prinsip fundamental API security, cara menerapkannya dalam layer yang saling melindungi, pola serangan yang paling umum dengan contoh konkret, dan checklist yang bisa digunakan tim untuk audit secara mandiri.
Tujuh Prinsip Fundamental API Security #
Prinsip-prinsip ini bukan teori abstrak — setiap insiden API security yang serius bisa dilacak kembali ke salah satu (atau beberapa) prinsip berikut yang dilanggar.
1. Authentication Berbeda dari Authorization #
Ini adalah kebingungan yang paling sering menyebabkan kerentanan serius. Authentication menjawab “siapa kamu?”, authorization menjawab “kamu boleh melakukan apa?”. Berhasil melewati authentication tidak secara otomatis berarti boleh mengakses semua resource.
// ANTI-PATTERN: Hanya cek authentication
func GetOrder(w http.ResponseWriter, r *http.Request) {
user := getAuthenticatedUser(r) // cek token valid
if user == nil {
http.Error(w, "Unauthorized", 401)
return
}
orderId := r.PathValue("id")
order, _ := db.GetOrder(orderId) // langsung fetch tanpa cek ownership!
json.NewEncoder(w).Encode(order)
}
// User A yang sudah login bisa melihat order milik User B
// BENAR: Cek authentication DAN authorization (ownership)
func GetOrder(w http.ResponseWriter, r *http.Request) {
user := getAuthenticatedUser(r)
if user == nil {
http.Error(w, "Unauthorized", 401)
return
}
orderId := r.PathValue("id")
order, _ := db.GetOrderByIdAndUserId(orderId, user.ID) // filter by user
if order == nil {
http.Error(w, "Not Found", 404) // 404, bukan 403 (jangan konfirmasi resource ada)
return
}
json.NewEncoder(w).Encode(order)
}
2. Least Privilege — Akses Sekecil Mungkin #
Setiap token, API key, service account, dan user harus hanya memiliki akses minimum yang dibutuhkan untuk tugasnya. Memberikan akses berlebihan adalah hutang keamanan yang akan jatuh tempo saat terjadi breach.
// ANTI-PATTERN: Token dengan akses terlalu luas
JWT payload: {
"sub": "usr_123",
"role": "admin", // semua permission admin
"scope": "all" // akses ke semua API
}
// Jika token ini bocor: attacker dapat akses ke seluruh sistem
// BENAR: Token dengan scope yang spesifik
Mobile app token: {
"sub": "usr_123",
"scope": "read:orders write:cart read:profile",
"context": "mobile" // tidak bisa digunakan di admin panel
}
Admin panel token: {
"sub": "usr_123",
"scope": "admin:users admin:orders",
"context": "admin",
"ip_restriction": "office_subnet"
}
3. Default Deny — Tolak Dulu, Izinkan Secara Eksplisit #
Semua akses ditolak secara default. Endpoint baru tidak otomatis terbuka — harus ada deklarasi eksplisit bahwa endpoint ini boleh diakses, oleh siapa, dan dalam kondisi apa.
// ANTI-PATTERN: Default allow, exceptions are blocked
router.Use(authMiddleware) // tapi developer lupa tambahkan di route baru
// BENAR: Semua route harus eksplisit declare accessibility-nya
// Gunakan annotation atau explicit middleware:
// Public endpoint (explicitly declared as public)
router.GET("/health", publicHandler)
router.GET("/v1/products", publicHandler)
// Authenticated endpoints
authRouter := router.Group("/v1", authMiddleware)
authRouter.GET("/orders", getOrders)
authRouter.POST("/orders", createOrder)
// Admin-only endpoints
adminRouter := router.Group("/v1/admin", authMiddleware, adminOnlyMiddleware)
adminRouter.GET("/users", listUsers)
4. Never Trust Client Input #
Semua input dari client — URL parameter, query string, request body, bahkan header — dianggap tidak tepercaya sampai divalidasi. Client tidak boleh menentukan sendiri role, permission, user_id, atau data sensitif lainnya.
// ANTI-PATTERN: Percaya data dari client
func CreateOrder(r *http.Request) {
var order Order
json.NewDecoder(r.Body).Decode(&order)
// order.UserID bisa diisi bebas oleh client!
db.CreateOrder(order)
}
// BENAR: Ambil data sensitif dari konteks autentikasi, bukan dari input
func CreateOrder(r *http.Request) {
user := getAuthenticatedUser(r) // dari token, bukan dari body
var input CreateOrderInput
json.NewDecoder(r.Body).Decode(&input)
// Validasi input: hanya field yang diizinkan
order := Order{
UserID: user.ID, // dari token autentikasi, bukan dari request body
Items: input.Items,
// user tidak bisa set UserID, status, dll
}
db.CreateOrder(order)
}
5. Explicit Ownership Validation #
Setiap akses ke resource harus divalidasi kepemilikannya. Tidak cukup hanya cek bahwa resource ada — harus dipastikan bahwa resource tersebut memang milik user yang sedang mengaksesnya.
// ANTI-PATTERN: Tidak ada ownership validation (IDOR)
GET /api/invoices/12345
// Server hanya cek apakah invoice 12345 ada — tidak cek siapa pemiliknya
// BENAR: Ownership validation
SELECT * FROM invoices WHERE id = ? AND user_id = ?
// ^^^^^^^^^^^ wajib
// Atau menggunakan row-level security di database
// Atau menggunakan scope-based query di service layer
6. Defense in Depth — Lapisan Pertahanan Berlapis #
Jangan bergantung pada satu lapisan keamanan. Jika satu lapisan berhasil dibobol, lapisan berikutnya harus masih bisa mencegah damage yang lebih besar.
flowchart TD
Internet["Internet / Attacker"]
L1["Layer 1: Transport\nHTTPS / TLS\nHSTS Header"]
L2["Layer 2: Rate Limiting & WAF\nBlokir brute force\nDDoS mitigation"]
L3["Layer 3: Authentication\nVerifikasi token / API key"]
L4["Layer 4: Authorization\nValidasi permission & ownership"]
L5["Layer 5: Input Validation\nSchema enforcement\nSanitization"]
L6["Layer 6: Business Logic\nAnti-fraud rules\nRisk scoring"]
L7["Layer 7: Data Layer\nRow-level security\nEncryption at rest"]
L8["Layer 8: Monitoring & Alerting\nAnomaly detection\nAudit logging"]
Internet --> L1
L1 --> L2
L2 --> L3
L3 --> L4
L4 --> L5
L5 --> L6
L6 --> L7
L7 --> L8
style L1 fill:#27AE60,color:#fff
style L2 fill:#2ECC71,color:#fff
style L3 fill:#F39C12,color:#fff
style L4 fill:#E67E22,color:#fff
style L5 fill:#E74C3C,color:#fff
style L6 fill:#C0392B,color:#fff
style L7 fill:#8E44AD,color:#fff
style L8 fill:#2C3E50,color:#fff7. Fail Securely #
Saat terjadi error, sistem harus gagal dengan cara yang tidak mengekspos informasi sensitif. Error message untuk client harus generik — detail teknis hanya untuk log internal.
// ANTI-PATTERN: Error yang mengekspos detail internal
HTTP 500 Internal Server Error
{
"error": "SQLSTATE[42S02]: Table 'user_sessions_backup' doesn't exist",
"query": "SELECT * FROM user_sessions_backup WHERE token = 'abc123'",
"stack": "at database.go:145 in executeQuery..."
}
// Attacker tahu: nama tabel, struktur query, file dan baris kode
// BENAR: Error generik ke client, detail ke log internal
HTTP 500 Internal Server Error
{ "error": { "code": "INTERNAL_ERROR", "message": "Terjadi kesalahan internal" } }
// Log internal (tidak dikirim ke client):
ERROR [2024-01-27 14:32:01] SQLSTATE[42S02]: Table 'user_sessions_backup' doesn't exist
query: SELECT * FROM user_sessions_backup WHERE token = ?
request_id: req_abc123
user_id: usr_456
OWASP API Security Top 10 #
OWASP merilis daftar 10 risiko keamanan API yang paling kritis. Memahami dan mengetes terhadap daftar ini adalah baseline keamanan yang perlu dimiliki setiap API.
flowchart LR
subgraph OWASP["OWASP API Security Top 10 (2023)"]
direction TB
A1["API1: Broken Object\nLevel Authorization\n(IDOR)"]
A2["API2: Broken\nAuthentication"]
A3["API3: Broken Object\nProperty Level\nAuthorization"]
A4["API4: Unrestricted\nResource Consumption"]
A5["API5: Broken Function\nLevel Authorization"]
A6["API6: Unrestricted\nAccess to Sensitive\nBusiness Flows"]
A7["API7: Server-Side\nRequest Forgery"]
A8["API8: Security\nMisconfiguration"]
A9["API9: Improper\nInventory Management"]
A10["API10: Unsafe\nConsumption of APIs"]
endTiga yang paling sering ditemukan di production:
API1 — Broken Object Level Authorization (BOLA/IDOR): Endpoint yang tidak memvalidasi apakah object yang diminta memang milik user yang melakukan request. Sudah dibahas di prinsip Explicit Ownership Validation di atas.
API3 — Broken Object Property Level Authorization (Mass Assignment): Endpoint yang menerima update dari client tanpa memfilter field mana yang boleh diubah. User bisa mengubah field yang seharusnya tidak bisa diubah seperti role, status, atau is_admin.
API4 — Unrestricted Resource Consumption: Tidak ada rate limiting, size limit, atau pagination limit yang cukup. Memungkinkan attacker melakukan brute force, data scraping masif, atau exhausting resource server.
Pola Serangan Umum dengan Contoh Konkret #
IDOR — Insecure Direct Object Reference #
Skenario:
User A: ID usr_001
User B: ID usr_002
User A login dan request:
GET /api/invoices/inv_456
Authorization: Bearer <token_user_a>
Server hanya cek: apakah token valid? Ya → return invoice inv_456
Server TIDAK cek: apakah inv_456 milik user_a?
User A mendapat invoice milik User B.
Dampak:
Jika ada 100.000 invoice dengan ID sequential atau predictable,
satu script bisa dump seluruh data invoice semua user dalam hitungan menit.
Pencegahan:
SELECT * FROM invoices WHERE id = ? AND user_id = ?
— selalu include ownership filter
Mass Assignment / Over-posting #
// Endpoint update profil user:
PATCH /api/users/me
Authorization: Bearer <token_user_a>
Body: {
"name": "Hacker",
"email": "[email protected]",
"role": "admin", ← seharusnya tidak boleh diubah user
"is_verified": true, ← seharusnya tidak boleh diubah user
"subscription_plan": "enterprise" ← seharusnya tidak boleh diubah user
}
// Jika server langsung mapping body ke model tanpa whitelist:
user.UpdateFromJSON(body) // semua field ikut ter-update!
// Pencegahan: whitelist field yang boleh diubah
type UpdateProfileInput struct {
Name string `json:"name"`
Email string `json:"email"`
// role, is_verified, dll TIDAK ada di struct ini
}
Broken Function Level Authorization #
// ANTI-PATTERN: Endpoint admin tidak dilindungi dengan benar
GET /api/admin/users → 403 Forbidden (benar, ada middleware)
GET /api/admin/users/export → 200 OK (SALAH — lupa tambahkan middleware)
// ANTI-PATTERN: HTTP method yang berbeda tidak dilindungi sama
GET /api/orders/123 → auth check ada
DELETE /api/orders/123 → auth check lupa (route baru, lupa ditambahkan)
// Pencegahan: gunakan route group dengan middleware
adminGroup := router.Group("/admin", adminAuthMiddleware)
adminGroup.GET("/users", listUsers)
adminGroup.GET("/users/export", exportUsers) // otomatis protected
// Atau: test setiap kombinasi endpoint + HTTP method dalam security test suite
Excessive Data Exposure #
// ANTI-PATTERN: Expose semua field dari database
GET /api/users/me
Response:
{
"id": "usr_123",
"name": "Budi",
"email": "[email protected]",
"password_hash": "$2b$10$...", ← BAHAYA
"api_secret_key": "sk_live_...", ← BAHAYA
"internal_notes": "...", ← tidak perlu
"stripe_customer_id": "cus_...", ← tidak perlu
"is_banned": false, ← tidak perlu
"ban_reason": null ← tidak perlu
}
// BENAR: Hanya expose yang dibutuhkan
{
"id": "usr_123",
"name": "Budi",
"email": "[email protected]",
"avatar_url": "...",
"created_at": "2024-01-15T10:00:00Z"
}
Rate Limiting — Lebih dari Sekadar Anti-DDoS #
Rate limiting sering dianggap hanya untuk mencegah DDoS, padahal ia juga melindungi dari brute force, credential stuffing, dan data scraping.
Jenis rate limiting yang perlu ada:
1. Per IP — mencegah serangan dari satu sumber
Limit: 100 request/menit per IP
Action: Return 429 dengan Retry-After header
2. Per user/token — mencegah penyalahgunaan oleh akun yang valid
Limit: 1000 request/jam per user
Action: Return 429
3. Per endpoint yang sensitif — lebih ketat untuk operasi berisiko tinggi
POST /auth/login: 5 request/menit per IP
POST /auth/forgot-password: 3 request/menit per email
Action: Exponential backoff atau CAPTCHA
4. Per API key — untuk B2B API
Setiap API key punya quota yang berbeda sesuai tier
Expose X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset header
Response yang benar saat rate limited:
HTTP 429 Too Many Requests
{
"error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Terlalu banyak request" }
}
Headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706356000
Retry-After: 47
Jangan hanya rate limit di level IP — IP mudah di-rotate menggunakan proxy atau VPN. Rate limiting paling efektif ketika diterapkan secara berlapis: per IP, per user, per endpoint, dan kombinasinya. Attacker dengan IP berbeda tapi token yang sama tetap bisa dideteksi dengan per-token rate limiting.
Input Validation dan Schema Enforcement #
Validasi input adalah pertahanan terakhir sebelum data menyentuh business logic dan database.
Level validasi yang harus ada:
1. Format validasi — tipe data, panjang, pattern
email harus valid email format
phone harus numeric, panjang 10-15 digit
date harus valid ISO 8601
ID harus UUID atau format yang didefinisikan
2. Business validasi — aturan domain
quantity tidak boleh negatif
start_date harus sebelum end_date
discount percentage harus antara 0-100
3. Security validasi
Reject field yang tidak dikenal (strict mode)
Sanitize input untuk mencegah injection
Limit request body size
Contoh implementasi di Go dengan struct validation:
type CreateOrderInput struct {
Items []OrderItem `json:"items" validate:"required,min=1,max=100"`
Note string `json:"note" validate:"max=500"`
// UserID tidak ada di sini — diambil dari token autentikasi
// Status tidak ada di sini — set otomatis ke "pending"
}
// Reject unknown fields di JSON decoder
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() // extra fields = error
Logging dan Audit Trail #
Logging adalah komponen API security yang paling sering diabaikan sampai terjadi insiden.
Yang harus di-log untuk API security:
AUTHENTICATION EVENTS:
✓ Login berhasil: timestamp, user_id, IP, user-agent, device
✓ Login gagal: timestamp, email yang dicoba, IP, alasan gagal
✓ Token refresh: timestamp, user_id, IP
✓ Logout: timestamp, user_id
✓ Password reset request: timestamp, email, IP
AUTHORIZATION EVENTS:
✓ Access denied (403): timestamp, user_id, endpoint, method
✓ Access sensitive resource: timestamp, user_id, resource_id
DATA ACCESS EVENTS:
✓ Bulk data export atau download
✓ Akses data user lain (harusnya dicegah, tapi jika lolos perlu tercatat)
✓ Perubahan data sensitif (role, permission, payment info)
Yang TIDAK boleh di-log:
✗ Password atau password hash
✗ Token JWT atau API key (log hanya prefix-nya jika perlu)
✗ Credit card number (log hanya 4 digit terakhir)
✗ PII yang tidak diperlukan untuk audit
Log tanpa alerting adalah log yang hanya berguna setelah incident terjadi. Tambahkan alerting untuk event yang mencurigakan: banyak failed login dari satu IP, access denied yang tinggi dari satu user, atau traffic yang tidak biasa di jam-jam tertentu. Alert yang proaktif memungkinkan response sebelum damage besar terjadi.
Security Headers untuk API #
HTTP security headers adalah lapisan tambahan yang mudah diterapkan tapi sering dilewatkan untuk API.
Headers yang direkomendasikan untuk API:
Strict-Transport-Security: max-age=31536000; includeSubDomains
→ Paksa HTTPS, mencegah downgrade attack
X-Content-Type-Options: nosniff
→ Mencegah MIME type sniffing
X-Frame-Options: DENY
→ Mencegah clickjacking
Content-Security-Policy: default-src 'none'
→ Untuk API response yang tidak butuh resource loading
Referrer-Policy: no-referrer
→ Tidak kirim referrer header
Cache-Control: no-store
→ Untuk response yang mengandung data sensitif
X-Request-ID: <uuid>
→ Untuk tracing dan debugging (kembalikan ke client agar bisa dilaporkan)
Checklist API Security #
AUTHENTICATION:
□ Semua endpoint yang butuh auth sudah menggunakan auth middleware
□ Token divalidasi: signature, exp, iss, aud
□ Token berumur pendek (≤60 menit untuk access token)
□ Tidak ada API key statis tanpa expiry di production
□ Logout endpoint mencabut token/session
AUTHORIZATION:
□ Setiap resource access memvalidasi ownership (bukan hanya authentication)
□ Admin-only endpoint dilindungi dengan permission check terpisah
□ Authorization dilakukan di server, tidak di client
□ Tidak ada endpoint yang "lupa" ditambahkan middleware
INPUT VALIDATION:
□ Semua input divalidasi format, tipe, dan panjangnya
□ Unknown fields di-reject (strict schema validation)
□ Field sensitif (role, user_id, is_admin) tidak bisa diubah dari body
□ Request body size di-limit
RATE LIMITING:
□ Rate limiting ada di endpoint login dan auth-related
□ Rate limiting ada di endpoint yang mahal atau data-heavy
□ Response 429 sudah include Retry-After header
DATA EXPOSURE:
□ Response hanya berisi field yang dibutuhkan client
□ Tidak ada password hash, token, atau internal ID di response
□ Error response generik — tidak ada detail teknis atau stack trace
□ Pagination di-enforce — tidak ada endpoint yang bisa return semua data
TRANSPORT SECURITY:
□ HTTPS di semua environment (bukan hanya production)
□ Security headers sudah dikonfigurasi
□ TLS version minimal 1.2 (1.3 lebih baik)
LOGGING DAN MONITORING:
□ Auth events di-log (login, logout, failed attempts)
□ Authorization failures di-log
□ Sensitive data access di-log
□ Alerting untuk pola yang mencurigakan ada
TESTING:
□ Security test di CI pipeline
□ IDOR test: coba akses resource milik user lain
□ Privilege escalation test: coba set role via body
□ Rate limit test: verifikasi 429 dikembalikan dengan benar
□ Error message test: verifikasi tidak ada detail internal di response
Ringkasan #
- Authentication dan authorization adalah dua hal yang berbeda — berhasil melewati authentication tidak berarti boleh mengakses semua resource. Setiap resource access harus memvalidasi kepemilikan secara eksplisit.
- Default deny bukan paranoia, ini keharusan — semua endpoint baru secara default tertutup. Buka hanya yang secara eksplisit perlu dibuka, bukan sebaliknya.
- Client input tidak pernah bisa dipercaya — URL parameter, query string, body, header — semuanya bisa dimanipulasi. User ID, role, dan permission harus selalu diambil dari konteks autentikasi, bukan dari input.
- IDOR adalah kerentanan paling umum dan paling mudah dicegah — selalu sertakan ownership filter di database query.
WHERE id = ? AND user_id = ?adalah pola yang wajib ada.- Mass assignment adalah pintu masuk privilege escalation — gunakan whitelist field yang boleh diupdate, bukan blacklist. Field seperti
role,is_admin, danstatustidak boleh ada di struct input.- Defense in depth — jangan bergantung pada satu lapisan. Auth middleware bisa di-bypass karena bug, tapi row-level security di database memberikan lapisan kedua. Kombinasi lapisan membuat breach menjadi jauh lebih sulit.
- Fail securely — error untuk client harus generik — detail teknis, nama tabel, stack trace, dan informasi internal tidak boleh sampai ke client. Log detail untuk internal, kirim pesan generik ke client.
- Rate limiting untuk lebih dari sekadar DDoS — rate limiting per IP, per user, dan per endpoint secara berlapis melindungi dari brute force, credential stuffing, dan data scraping.
- Log sebelum dibutuhkan, bukan setelah dibutuhkan — audit trail yang baik memungkinkan respons insiden yang efektif. Tanpa log yang memadai, forensik menjadi hampir tidak mungkin.
- Security harus ada sejak desain, bukan ditambahkan di akhir — API yang dirancang dengan prinsip security sejak awal jauh lebih murah untuk diamankan daripada yang harus di-retrofit setelah production.
← Sebelumnya: GraphQL Federation Berikutnya: Web Application Setup →