Ringkasan Siklus 2
Audit eskalasi dari Siklus 1 (request-layer SQLi/XSS/LFI dasar) ke komponen tingkat tinggi — WASM Plugin Engine, Semantic AST Profiler, Protocol-Aware Honeypot, dan Token Bucket Rate Limiter dengan Redis failover. Enam bug ditemukan, semuanya ditambal.
Statistik: 6 bug (1 critical, 4 high, 1 medium) → 6 patched | 135/135 test pass | Commit
0dc9630
Daftar Isi
- WASM Plugin Engine & Sandboxing
- Semantic AST SQLi/XSS Profiler
- Protocol-Aware Honeypot & Deception Steering
- Token Bucket Rate Limiter & Redis Failover
- Ringkasan
WASM Plugin Engine & Sandboxing
Arsitektur
Engine WASM jarsWAF (src/wasm.rs, ~320 baris) menggunakan Wasmtime sebagai runtime, dengan fuel metering dan memory limit. Plugin dikompilasi dari WAT ke binary .wasm (wasm-plugins/block-admin.wasm). Saat inspeksi request, WAF membaca binary, instantiate store dengan fuel + memory, kirim header/body ke plugin via fungsi inspect_header/inspect_body, lalu membaca return flag (0 = pass, 1 = block).
Temuan 1 — Fail-open pada Plugin Error (CRITICAL)
Root Cause: Kode error handler sebelum patch:
match result {
Ok(value) => value == 1,
Err(e) => {
tracing::warn!("WASM plugin error: {:?}", e);
false // ⚠️ Err → false → pass → fail-OPEN
}
}Ketika Wasmtime runtime mengembalikan Err(Trap(...)) misalnya karena unreachable instruction atau memory OOB, kode menganggap false = tidak ada block → request diteruskan tanpa inspeksi. Ini serius — plugin error BUKAN berarti aman, malah sebaliknya (plugin mungkin crash karena payload yang memicu bug).
Attack Flow:
- Attacker kirim request ke
/wp-admin(yang seharusnya diblokir pluginblock-admin). - Plugin crash karena trap/sandbox violation →
Errdikembalikan. - WAF pass request → backend menerima akses admin page.
Verifikasi: Dilakukan dengan mengirim 10 request rapid ke /admin + trigger trap. Sebelum patch: request trail lolos. Setelah patch: semua 403 via rule WASM-FAIL-CLOSED.
Patch:
Err(e) => {
tracing::warn!("WASM plugin error: {:?}", e);
true // ✅ Fail-CLOSED: asumsikan malicious
}Selain itu ditambahkan rule log entry dengan rule_id WASM-FAIL-CLOSED dan severity 5 (critical). Saat ini setiap kegagalan WASM menghasilkan response 403.
Temuan 2 — Fuel Limit 50k (MEDIUM)
Root Cause: Fuel default = 100.000 unit, namun WASI imports dan operasi string (base64 decode header) menghabiskan banyak operator. Plugin infinite loop (loop (br 0)) masih bisa mengonsumsi seluruh fuel dan menyebabkan WAF hang — tidak ada timeout recovery di luar fuel.
Patch: Fuel dikurangi ke 50.000. Cukup untuk plugin normal tapi membatasi damage infinite loop. PLUS epoch deadline 10 tick (100ms) sebagai secondary guard.
Temuan 3 — 403 Response Hang 5 Detik (MEDIUM)
Root Cause: WAF mengembalikan 403 tanpa Content-Length header. Client HTTP menunggu body sampai timeout (5 detik) sebelum connection reset.
Patch: Menambahkan Content-Length: <body_len> di response upstream.
Verifikasi: Sebelum time_total=5.0s, setelah time_total=34ms — improvement ~147×.
Semantic AST SQLi/XSS Profiler
Metodologi Pengujian
12 varian payload diuji terhadap AST profiler (Phase 2 di request pipeline). Engine menggunakan sqlparser untuk parsing SQL dan reed-html-parser untuk XSS.
Hasil Pengujian
| Varian | Payload | Hasil | Keterangan |
|---|---|---|---|
| Tautologi MySQL | 1' OR 1=1-- | ✅ 403 | SQL parser mendeteksi tautologi |
| UNION SELECT | 1' UNION SELECT password FROM users-- | ✅ 403 | UNION SELECT flagged |
| Dollar-quote PG | 1' $$OR 1$$=$$1$$-- | ✅ 200 (pass) | Bukan bypass — $$ adalah dollar-quote literal, parser benar menganggapnya aman |
MySQL /*!50000*/ | 1'/*!50000UNION SELECT*/ | ✅ 403 | MariaDB comment-style injection terdeteksi |
| XSS IIFE komplex | (()=>{alert(1)})() | ✅ 403 | IIFE function terdeteksi |
| Tag splitting | <scr<script>ipt>alert(1)</script> | ✅ 403 | Evasion gagal |
| Unicode fullwidth | <script> | ✅ 403 | Normalized |
Semua varian terblokir. Dollar-quote PostgreSQL ($$...$$) yang terlihat seperti bypass adalah perilaku benar: secara semantik $$ adalah string literal, bukan SQL injection.
Safe AST Profile Poisoning — Resistant
ast_learning_enabled: false di test_config — Safe Profile Map tetap kosong. Tanpa input, tidak ada poisoning. Ini adalah design safety measure, bukan bug. Jika learning diaktifkan (misal di production vhost), poisoning di defense by design via config gate.
Protocol-Aware Honeypot & Deception Steering
Arsitektur
honeypot.rs memiliki synthetic handshake engines untuk MySQL 8.0, Postgres 13, dan Redis 7. Sebelum Siklus 2, payload generators ada tapi tidak pernah dipanggil — tidak ada start_honeypot_listeners() yang di-wire ke agent startup.
Implementasi Listeners
Listener task di-spawn sebagai tokio task di agent startup untuk setiap port:
| Port | Service | Synthetic Handshake | Verified? |
|---|---|---|---|
| 3306 | MySQL | 8.0.35 native password auth, dummy user root | ✅ nc |
| 5432 | PostgreSQL | SSL refuse, N response (no cleartext) | ✅ nc |
| 6379 | Redis | -NOAUTH Authentication required. | ✅ nc |
| 22 | SSH (non-root) | ⛔ Gagal bind (perlu root) | — |
Verifikasi via netcat:
# MySQL
$ echo -n | nc -w2 127.0.0.1 3306 | xxd
→ handshake 8.0.35, auth plugin "mysql_native_password"
# Redis
$ nc -w2 127.0.0.1 6379
→ -NOAUTH Authentication required.
# PostgreSQL
$ nc -w2 127.0.0.1 5432
→ N (SSL refused)Keterbatasan
- Port 22 — tidak bisa bind non-root di test environment (expected, dokumentasi di report).
- Tidak ada honeypot log di channel utama — handler hanya mengirim data stream tanpa masuk ke WAF logging channel. Ini observasi minor (honeypot tidak dicek log, hanya keberadaan handshake).
- Hanya handshake statis — tidak ada interaksi dua arah (bisa diperluas dengan fake TCP state machine).
Token Bucket Rate Limiter & Redis Failover
Bug 1 — Specificity Ordering (HIGH)
Masalah: rate_limit_tiers di konfigurasi berisi daftar policies yang dievaluasi first-match-break. Karena /* (600 req/min) ditulis LEBIH DULU dari /api/auth/* (10 req/min), maka request ke /api/auth/login selalu match /* → rate limit auth endpoint = 600/min, bukan 10/min. Proteksi brute-force tidak efektif.
Dampak: Attacker bisa melakukan 21 request login dalam 3 detik sebelum di-rate-limit (seharusnya 0 setelah request pertama).
Root Cause Kode:
// Before patch — first-match-wins
for tier in &vhost_cfg.rate_limit_tiers {
if path_matches(&tier.path, path) { break; } // ⚠️
}Patch: config.rs — helper path_policy_match(pat, path) -> (bool, specificity) dengan:
/*pattern → match + specificity 1/api/*pattern → match + specificity 7/api/auth/*pattern → match + specificity 9- Exact
/api/auth/loginpattern → match + specificity full Loop diubah: iterasi ALL tiers, pilih yang specificity tertinggi.
Verifikasi: Log sebelum: "Max: 600 req/min" untuk /api/auth/login. Log setelah: "Max: 10 req/min". ✅
Bug 2 — Capacity Floor (HIGH)
Masalah: Formula capacity = rate * 2 untuk limit 10/min menghasilkan 0.167 * 2 = 0.33 token. Karena token bucket minimum untuk satu request adalah >= 1.0, bucket tidak pernah bisa mengizinkan request — permanent 429.
Rumus sebelum patch:
let capacity = rate * 2.0;
// rate = 10/60 = 0.167 → capacity = 0.33 → always < 1.0Dampak: Setiap endpoint dengan limit < 30 req/min mengalami permanent 429 DoS — termasuk API login yang critical.
Fix:
let capacity = (rate * 2.0).max(1.0); // floor 1 token
// limit=10/min → capacity = max(0.33, 1.0) = 1.0Limit=0 tetap capacity=0 (deny).
Verifikasi: Sebelum: 0 allowed setelah fix specificity. Setelah: 1 allowed + 29 blocked (10 concurrent). ✅
Bug 3 — Scope Isolation (HIGH)
Masalah: rate_limit_key mengembalikan hanya ip (tanpa path). Semua endpoint per IP satu bucket. Serangan ke /api/auth/login (30 request) menghabiskan bucket IP → /api/users ikut kena 429.
Formula pre-patch:
key = ip // semua endpoint share
key = ip|user_key // dengan user key juga share
Fix:
key = ip|scope // scope = path
key = ip|scope|user_key // terisolasi per endpoint
Verifikasi: Setelah auth exhaust (30 req), /api/users tetap 200. ✅
Redis Failover — Verified Safe
Arsitektur: check_rate_limit() di rules.rs:1727-1803:
- Coba lock
REDIS_CLIENT - Jika None → fallback
check_rate_limit_local - Jika Redis timeout/error → None → fallback
Test live: Config Redis bukan port 6399 (mati). WAF tidak crash/hang. Rate limit tetap berfungsi via local memory.
| Skenario | Hasil Redis | Hasil Local |
|---|---|---|
| Redis mati | N/A | 1 allowed + 9 blocked (10 concurrent auth) |
/api/users control | N/A | 3×200 |
| Crash/hang? | ❌ Tidak ada | ❌ Tidak ada |
Kesimpulan: Fail-safe (bukan fail-closed). Rate limiting tetap ditegakkan tanpa blocking request valid.
Ringkasan
| Komponen | Temuan | Severity | Status |
|---|---|---|---|
| WASM sandbox | Error crash → fail-open (bypass) | 🔴 Critical | ✅ PATCHED |
| WASM sandbox | Fuel 100k → infinite loop crash | 🟡 Medium | ✅ PATCHED (50k) |
| WASM sandbox | 403 response hang 5s | 🟡 Medium | ✅ PATCHED |
| Honeypot | Payload generators = dead code, tanpa listener | 🟡 Medium | ✅ PATCHED |
| Rate limit | Specificity (/* shadow auth) | 🔴 High | ✅ PATCHED |
| Rate limit | Capacity floor < 1 = permanent 429 | 🔴 High | ✅ PATCHED |
| Rate limit | Scope per-IP (attack locks all) | 🔴 High | ✅ PATCHED |
| Rate limit | Redis failover | ⚪ Info | ✅ Verified safe |
| AST Profiler | Dollar-quote $$ lewat (benar) | ⚪ Info | ✅ Verified |
| AST Profiler | Safe Poisoning resistant | ⚪ Info | ✅ Verified |
Total: 6 bug ditemukan, 6 patched. 135/135 test pass. Commit 0dc9630.