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

  1. WASM Plugin Engine & Sandboxing
  2. Semantic AST SQLi/XSS Profiler
  3. Protocol-Aware Honeypot & Deception Steering
  4. Token Bucket Rate Limiter & Redis Failover
  5. 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:

  1. Attacker kirim request ke /wp-admin (yang seharusnya diblokir plugin block-admin).
  2. Plugin crash karena trap/sandbox violation → Err dikembalikan.
  3. 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

VarianPayloadHasilKeterangan
Tautologi MySQL1' OR 1=1--✅ 403SQL parser mendeteksi tautologi
UNION SELECT1' UNION SELECT password FROM users--✅ 403UNION SELECT flagged
Dollar-quote PG1' $$OR 1$$=$$1$$--✅ 200 (pass)Bukan bypass$$ adalah dollar-quote literal, parser benar menganggapnya aman
MySQL /*!50000*/1'/*!50000UNION SELECT*/✅ 403MariaDB comment-style injection terdeteksi
XSS IIFE komplex(()=>{alert(1)})()✅ 403IIFE function terdeteksi
Tag splitting<scr<script>ipt>alert(1)</script>✅ 403Evasion gagal
Unicode fullwidth<script>✅ 403Normalized

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:

PortServiceSynthetic HandshakeVerified?
3306MySQL8.0.35 native password auth, dummy user root✅ nc
5432PostgreSQLSSL refuse, N response (no cleartext)✅ nc
6379Redis-NOAUTH Authentication required.✅ nc
22SSH (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

  1. Port 22 — tidak bisa bind non-root di test environment (expected, dokumentasi di report).
  2. 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).
  3. 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/login pattern → 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.0

Dampak: 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.0

Limit=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:

  1. Coba lock REDIS_CLIENT
  2. Jika None → fallback check_rate_limit_local
  3. 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.

SkenarioHasil RedisHasil Local
Redis matiN/A1 allowed + 9 blocked (10 concurrent auth)
/api/users controlN/A3×200
Crash/hang?❌ Tidak ada❌ Tidak ada

Kesimpulan: Fail-safe (bukan fail-closed). Rate limiting tetap ditegakkan tanpa blocking request valid.

Ringkasan

KomponenTemuanSeverityStatus
WASM sandboxError crash → fail-open (bypass)🔴 Critical✅ PATCHED
WASM sandboxFuel 100k → infinite loop crash🟡 Medium✅ PATCHED (50k)
WASM sandbox403 response hang 5s🟡 Medium✅ PATCHED
HoneypotPayload generators = dead code, tanpa listener🟡 Medium✅ PATCHED
Rate limitSpecificity (/* shadow auth)🔴 High✅ PATCHED
Rate limitCapacity floor < 1 = permanent 429🔴 High✅ PATCHED
Rate limitScope per-IP (attack locks all)🔴 High✅ PATCHED
Rate limitRedis failover⚪ Info✅ Verified safe
AST ProfilerDollar-quote $$ lewat (benar)⚪ Info✅ Verified
AST ProfilerSafe Poisoning resistant⚪ Info✅ Verified

Total: 6 bug ditemukan, 6 patched. 135/135 test pass. Commit 0dc9630.