A04 - Insecure Design
Definisi dan Penjelasan
Insecure Design adalah kategori risiko keamanan yang berfokus pada kerentanan yang muncul dari kesalahan desain arsitektur dan cacat logika bisnis, bukan dari bug pada implementasi teknis atau coding. Ini adalah tambahan baru di OWASP Top 10 2021 yang sebelumnya tidak ada sebagai kategori terpisah di versi 2017.
Berbeda dengan kerentanan teknis (seperti SQL Injection atau XSS) yang bisa diperbaiki dengan patch kode, Insecure Design memerlukan perubahan fundamental pada arsitektur dan desain aplikasi. Kerentanan ini sudah ada sejak tahap perencanaan dan desain - bahkan sebelum satu baris kode pun ditulis.
Konsep kunci dalam Insecure Design:
- Security by Design - Keamanan harus diintegrasikan sejak fase desain, bukan ditambahkan setelah aplikasi selesai
- Threat Modeling - Identifikasi potensi ancaman sebelum implementasi
- Business Logic Flaws - Cacat dalam logika bisnis yang bisa dieksploitasi
- Missing Security Controls - Kontrol keamanan yang seharusnya ada sejak awal
Mengapa Insecure Design Berbahaya?
| Aspek | Dampak |
|---|---|
| Akar Masalah | Kesalahan ada di level arsitektur, bukan kode |
| Biaya Perbaikan | Sangat mahal - perlu redesign ulang |
| Deteksi | Sulit dideteksi oleh scanner/SAST/DAST otomatis |
| Cakupan | Biasanya mempengaruhi banyak komponen |
| Eksploitasi | Sering membutuhkan pemahaman mendalam tentang bisnis |
Perbedaan Insecure Design vs Implementation Bug
| Aspek | Insecure Design | Implementation Bug |
|---|---|---|
| Tahap muncul | Fase desain/perencanaan | Fase coding |
| Deteksi alat | Sulit (butuh threat modeling) | Mudah (SAST, DAST) |
| Biaya perbaikan | Tinggi (redesign) | Rendah (patch) |
| Contoh | Tidak ada rate limiting, forgot password flaw | SQL Injection, XSS |
Contoh-contoh Insecure Design
1. Business Logic Bypass
Attacker memanipulasi urutan atau alur proses bisnis untuk mendapatkan keuntungan.
Contoh:
- Checkout tanpa pembayaran
- Mendapatkan diskon berulang kali
- Transfer uang ke akun sendiri lalu klaim refund
2. Rate Limiting Tidak Ada
Endpoint kritis (login, registrasi, reset password) tidak memiliki pembatasan kecepatan.
Dampak:
- Brute force password
- Account enumeration
- SMS/email bombing
3. Flawed Forgot Password Logic
Mekanisme lupa password yang bisa dieksploitasi.
Contoh:
- Security question yang mudah ditebak
- Token reset password bisa ditebak
- Link reset password tidak pernah expired
- Tidak ada verifikasi identitas sebelum reset
4. 2FA Bypass
Two-factor authentication yang bisa di-bypass karena kesalahan desain.
Contoh:
- 2FA hanya di halaman login, tapi endpoint API tidak dilindungi
- OTP bisa ditebak (terlalu pendek, tidak ada rate limiting)
- Backup code tidak dirotasi
5. Race Condition
Terjadi ketika aplikasi tidak menangani akses konkuren dengan benar.
Contoh:
# RENTAN - Race condition pada transfer uang
def transfer(sender, receiver, amount):
balance = get_balance(sender)
if balance >= amount:
deduct(sender, amount)
# 🔴 Thread lain membaca balance di sini
add(receiver, amount)
6. Missing Access Control pada Workflow
Setiap step dalam workflow seharusnya memvalidasi otorisasi.
Contoh: Aplikasi yang memeriksa role hanya di halaman pertama wizard, tetapi step-step selanjutnya tidak.
7. Default Credentials / Hardcoded Config
Akun default dengan password yang diketahui publik.
8. Infinite Money / Coupon Abuse
Sistem kupon/diskon yang bisa digunakan berkali-kali.
Dampak Keamanan
| Dampak | Deskripsi | Contoh |
|---|---|---|
| Financial Loss | Kerugian finansial langsung | Abuse diskon, pencurian saldo |
| Account Takeover | Pengambilalihan akun | Brute force tanpa rate limiting |
| Data Breach | Kebocoran data | Workflow bypass akses data |
| Denial of Service | Layanan tidak tersedia | No rate limiting + abuse |
| Reputational Damage | Hilangnya kepercayaan | Keamanan buruk tersebar luas |
Contoh Kerentanan (Code Snippets)
Contoh 1: Rate Limiting Tidak Ada (Rentan)
# RENTAN - Endpoint login tanpa rate limiting
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
user = authenticate(username, password)
if user:
return jsonify({"token": create_token(user)})
return jsonify({"error": "Invalid credentials"}), 401
Dampak: Attacker bisa melakukan brute force jutaan password.
Contoh 2: Rate Limiting yang Benar (Aman)
# AMAN - Rate limiting dengan Flask-Limiter
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(app, key_func=get_remote_address)
@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute") # Max 5 percobaan per menit
def login():
username = request.form['username']
password = request.form['password']
user = authenticate(username, password)
if user:
return jsonify({"token": create_token(user)})
return jsonify({"error": "Invalid credentials"}), 401
Contoh 3: Forgot Password - Token Dapat Ditebak (Rentan)
# RENTAN - Token dapat ditebak
import random
def generate_reset_token():
# Hanya 10000 kemungkinan (0000-9999)
return f"{random.randint(0, 9999):04d}"
# RENTAN - Token tidak pernah expired
reset_token = generate_reset_token()
save_to_db(user_id, reset_token)
# Token berlaku selamanya sampai digunakan!
Contoh 4: Forgot Password yang Aman
# AMAN - Token dengan secrets library dan expiration
import secrets
from datetime import datetime, timedelta
def generate_reset_token(user_id):
# 32 byte true random - tidak bisa ditebak
token = secrets.token_hex(32)
expired_at = datetime.utcnow() + timedelta(hours=1)
save_to_db(user_id, {
'token': hash_token(token), # Hash sebelum simpan
'expired_at': expired_at,
'used': False
})
return token # Return plaintext untuk dikirim via email
def verify_reset_token(user_id, token):
record = get_reset_record(user_id)
if not record:
return False
if record['used']:
return False
if datetime.utcnow() > record['expired_at']:
return False
if not compare_token(record['token'], token):
return False
return True
Contoh 5: 2FA Bypass - Hanya di Frontend (Rentan)
// RENTAN - 2FA hanya di frontend
async function login() {
const token = await getJWT();
if (is2FAEnabled()) {
// Tampilkan form 2FA
show2FAForm();
const otp = await getOTP();
if (!verifyOTP(otp)) {
return;
}
}
// Setelah 2FA, panggil API tanpa verifikasi ulang
await callAPI(token); // 🔴 API endpoint tidak memverifikasi 2FA!
}
Contoh 6: Race Condition pada Stok Barang (Rentan)
# RENTAN - Race condition
@app.route('/api/order', methods=['POST'])
def create_order():
product_id = request.json['product_id']
quantity = request.json['quantity']
# 🔴 Gap antara SELECT dan UPDATE
stock = get_stock(product_id)
if stock >= quantity:
update_stock(product_id, stock - quantity)
create_order_record(product_id, quantity)
return jsonify({"success": True})
return jsonify({"error": "Insufficient stock"}), 400
Eksploitasi: Kirim 100 request simultan untuk produk dengan stok 5 - semuanya berhasil!
Contoh 7: Race Condition - Diperbaiki dengan Atomic Operation
# AMAN - Gunakan atomic operation / locking
@app.route('/api/order', methods=['POST'])
def create_order():
product_id = request.json['product_id']
quantity = request.json['quantity']
# Atomic decrement dengan kondisi
# Hanya berhasil jika stok >= quantity
result = db.execute("""
UPDATE products
SET stock = stock - ?
WHERE id = ? AND stock >= ?
""", (quantity, product_id, quantity))
if result.rowcount > 0:
create_order_record(product_id, quantity)
return jsonify({"success": True})
return jsonify({"error": "Insufficient stock"}), 400
Contoh 8: Workflow Bypass (Rentan)
# RENTAN - Tidak memvalidasi role di setiap step
@app.route('/checkout/step1', methods=['POST'])
def checkout_step1():
# Cek role hanya di step 1
if not current_user.is_authenticated:
return redirect('/login')
session['cart'] = request.json
return jsonify({"next": "/checkout/step2"})
@app.route('/checkout/step2', methods=['POST'])
def checkout_step2():
# 🔴 Tidak ada pengecekan role!
# User yang sudah logout masih bisa akses step ini
process_payment(session['cart'])
return jsonify({"success": True})
Contoh 9: Workflow Bypass (Diperbaiki)
# AMAN - Validasi di setiap step
def require_checkout_access():
if not current_user.is_authenticated:
abort(401)
if 'checkout_session' not in session:
abort(400, "Invalid checkout session")
@app.route('/checkout/step1', methods=['POST'])
@login_required
def checkout_step1():
session['checkout_session'] = str(uuid.uuid4())
session['cart'] = request.json
return jsonify({"next": "/checkout/step2"})
@app.route('/checkout/step2', methods=['POST'])
@login_required
@require_checkout_access
def checkout_step2():
process_payment(session['cart'])
session.pop('checkout_session', None)
return jsonify({"success": True})
Cara Eksploitasi (Relevan ke Lab)
1. Business Logic Bypass - Ticket Lab
Skenario: Sistem pembelian tiket dengan workflow:
Pilih tiket → Masukkan ke keranjang → Checkout → Bayar → Dapat tiket
Eksploitasi:
- Tambahkan tiket ke keranjang
- Langsung akses endpoint konfirmasi tanpa melewati pembayaran
- Jika berhasil, Anda mendapatkan tiket gratis
2. Rate Limiting Abuse
Skenario: Endpoint login tanpa rate limiting
# Brute force dengan hydra
hydra -l admin -P /usr/share/wordlists/rockyou.txt target.com http-post-form "/login:username=^USER^&password=^PASS^:Invalid credentials"
# Atau dengan script Python sederhana
for password in passwords:
r = requests.post(url, data={"username": "admin", "password": password})
if "Invalid credentials" not in r.text:
print(f"Found: {password}")
break
3. 2FA Bypass
- Login dengan kredensial yang valid
- Capture semua request menggunakan Burp Suite
- Perhatikan endpoint API yang dipanggil setelah 2FA
- Coba akses endpoint tersebut langsung tanpa melalui 2FA
- Jika berhasil, 2FA dapat di-bypass
4. Race Condition
# Script untuk race condition
import threading
import requests
url = "http://target.com/api/order"
data = {"product_id": 1, "quantity": 1}
def send_request():
r = requests.post(url, json=data, cookies=cookies)
print(r.status_code, r.text)
# Kirim 20 request simultan
threads = []
for i in range(20):
t = threading.Thread(target=send_request)
threads.append(t)
t.start()
for t in threads:
t.join()
5. Coupon/Diskon Abuse
- Dapatkan kupon diskon
- Gunakan kupon sekali
- Coba gunakan kupon yang sama lagi
- Jika sistem tidak menandai kupon sebagai "used", Anda bisa menggunakannya berulang kali
Cara Pencegahan
1. Threat Modeling Sejak Awal
Lakukan threat modeling di setiap tahap desain menggunakan metodologi seperti STRIDE (Microsoft):
| Kategori | Ancaman | Contoh |
|---|---|---|
| Spoofing | Pemalsuan identitas | Akun palsu, session hijacking |
| Tampering | Modifikasi data | Manipulasi harga, saldo |
| Repudiation | Penyangkalan aksi | Tidak ada log audit |
| Information Disclosure | Kebocoran informasi | Error yang menampilkan detail |
| Denial of Service | Gangguan layanan | Flood request |
| Elevation of Privilege | Eskalasi privilege | User jadi admin |
2. Security by Design Principles
Prinsip Keamanan Desain:
- Least Privilege: Setiap komponen hanya punya akses minimal
- Defense in Depth: Berlapis-lapis pertahanan
- Fail Secure: Gagal ke mode aman (deny by default)
- Complete Mediation: Validasi di setiap akses
- Economy of Mechanism: Desain sesederhana mungkin
- Open Design: Keamanan tidak bergantung pada kerahasiaan
- Psychological Acceptability: Mudah digunakan, tetap aman
3. Implementasi Rate Limiting
# Rate limiting berlapis
# 1. IP-based (per alamat IP)
# 2. User-based (per akun)
# 3. Endpoint-based (per endpoint)
limiter = Limiter(
app,
key_func=lambda: f"{get_remote_address()}:{current_user.id}"
)
# Endpoint kritis
@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute, 20 per hour")
def login(): ...
@app.route('/register', methods=['POST'])
@limiter.limit("3 per hour")
def register(): ...
@app.route('/reset-password', methods=['POST'])
@limiter.limit("2 per hour")
def reset_password(): ...
# Endpoint API
@app.route('/api/transfer', methods=['POST'])
@limiter.limit("10 per minute, 100 per hour")
def transfer(): ...
4. Secure Workflow Design
# Setiap state dalam workflow harus tervalidasi
WORKFLOW_STATES = {
'init': ['select_items', 'checkout'],
'select_items': ['checkout'],
'checkout': ['payment'],
'payment': ['confirmation'],
'confirmation': ['completed'],
'completed': []
}
def validate_transition(current_state, next_state):
if next_state not in WORKFLOW_STATES.get(current_state, []):
abort(400, "Invalid workflow transition")
5. Race Condition Prevention
# 1. Gunakan database transactions
@app.route('/api/transfer', methods=['POST'])
def transfer():
db.begin()
try:
sender_balance = db.execute(
"SELECT balance FROM accounts WHERE id = ? FOR UPDATE",
(sender_id,)
).fetchone()[0]
if sender_balance < amount:
db.rollback()
return jsonify({"error": "Insufficient funds"}), 400
db.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?",
(amount, sender_id))
db.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?",
(amount, receiver_id))
db.commit()
return jsonify({"success": True})
except:
db.rollback()
return jsonify({"error": "Transaction failed"}), 500
# 2. Atau gunakan pessimistic locking
# 3. Atau gunakan atomic operations
# 4. Atau gunakan queue/async processing
6. Secure Forgot Password
Desain Reset Password yang Aman:
- Token: 32+ byte, true random (secrets.token_hex)
- Expiration: 1 jam maksimal
- One-time use: Token tidak bisa dipakai dua kali
- Hash token di database (jangan simpan plaintext)
- Rate limiting: 2-3 request per jam per akun
- Notifikasi: Email/SMS bahwa password direset
- No user enumeration: Jangan bedakan "email tidak ada" vs "email terdaftar"
7. 2FA yang Benar
# 2FA harus diterapkan di layer SERVER, bukan hanya frontend
# Setiap endpoint sensitif harus memvalidasi 2FA
def require_2fa(f):
@wraps(f)
def decorated(*args, **kwargs):
if current_user.is_2fa_enabled and not session.get('2fa_verified'):
return jsonify({"error": "2FA verification required"}), 401
return f(*args, **kwargs)
return decorated
@app.route('/api/profile/email', methods=['PUT'])
@login_required
@require_2fa
def update_email():
# Hanya bisa diakses setelah 2FA
pass
8. Testing dan Review
Metodologi Pengujian:
1. Threat Modeling Review: Sebelum implementasi
2. Architecture Risk Analysis: Analisis risiko arsitektur
3. Security User Stories: Tulis requirement keamanan sebagai user stories
4. Abuse Cases: Test skenario penyalahgunaan (bukan hanya use case normal)
5. Penetration Testing: Fokus pada business logic flaws
6. Code Review: Review dengan perspektif keamanan desain
CWE Mapping
| CWE | Deskripsi |
|---|---|
| CWE-840 | Business Logic Errors (Kesalahan Logika Bisnis) |
| CWE-657 | Violation of Secure Design Principles (Pelanggaran Prinsip Desain Aman) |
| CWE-799 | Improper Control of Interaction Frequency (Rate Limiting) |
| CWE-362 | Concurrent Execution using Shared Resource (Race Condition) |
| CWE-306 | Missing Authentication for Critical Function |
| CWE-352 | Cross-Site Request Forgery (CSRF) |
| CWE-602 | Client-Side Enforcement of Server-Side Security |
| CWE-640 | Weak Password Recovery Mechanism |
| CWE-308 | Use of Single-factor Authentication |
| CWE-441 | Unintended Proxy or Intermediary (Confusion) |
Lab Terkait
| Lab | Konsep | Tingkat Kesulitan |
|---|---|---|
| Ticket (Workflow Bypass) | Business logic bypass pada sistem tiket | Medium |
| WebGoat | Business logic flaws, race condition, 2FA bypass | Easy-Hard |
Contoh Konkret dari Lab
Ticket Lab - Workflow Bypass
Skenario Lab: Sistem e-ticket dengan workflow:
- Pilih tiket
- Masukkan ke keranjang
- Checkout
- Pembayaran
- Konfirmasi
- Tiket diterbitkan
Langkah eksploitasi:
- Buka aplikasi dan pilih tiket
- Buka Burp Suite dan intercept request
- Tambahkan tiket ke keranjang (request normal)
- Coba akses langsung endpoint konfirmasi:
POST /api/tickets/confirm - Jika server tidak memvalidasi status pembayaran, tiket akan diterbitkan gratis
- Jika berhasil, cek apakah tiket benar-benar ada di profil Anda
Variasi eksploitasi:
- Coba akses endpoint dengan metode HTTP yang berbeda (GET vs POST)
- Manipulasi parameter quantity menjadi negatif atau desimal
- Coba pesan dengan harga 0
- Coba gunakan kupon yang sudah expired
WebGoat - Business Logic Flaws
WebGoat Lessons terkait:
- Business Logic Flaws - Pelajaran tentang bagaimana logika bisnis bisa dimanipulasi
- Race Condition - Eksploitasi konkurensi pada transfer uang
- 2FA Bypass - Bypass mekanisme two-factor authentication
Langkah-langkah di WebGoat (Race Condition):
- Buka lesson "Race Condition"
- Perhatikan endpoint transfer uang
- Kirim beberapa request transfer secara simultan
- Jika saldo tidak terkunci dengan benar, Anda bisa transfer lebih dari saldo yang dimiliki
Kesimpulan
Insecure Design adalah kategori kerentanan yang unik karena berfokus pada akar masalah - kesalahan desain yang sudah ada sebelum aplikasi ditulis. Berbeda dengan kerentanan implementasi yang bisa diperbaiki dengan patch, insecure design sering membutuhkan redesain ulang yang mahal dan memakan waktu.
Pencegahan terbaik adalah pendekatan Security by Design: lakukan threat modeling sejak awal, implementasikan rate limiting di endpoint kritis, desain workflow dengan validasi di setiap tahap, gunakan transaksi database untuk mencegah race condition, dan selalu pikirkan skenario penyalahgunaan (abuse cases) di samping skenario normal (use cases).
Ingatlah bahwa keamanan bukanlah fitur yang bisa "ditambahkan" setelah aplikasi selesai - keamanan harus menjadi bagian integral dari desain arsitektur sejak hari pertama.