Insiden serangan siber tingkat lanjut (seperti kasus nation-state targeting core auth endpoints) sering kali mengeksploitasi diskrepansi parsing antara reverse proxy dan aplikasi, atau melompat tahapan (state-jumping) pada alur otentikasi multi-step. Mengandalkan Web Application Firewall (WAF) saja tidak menjamin mitigasi payload yang dimanipulasi secara spesifik. Solusi defensif yang berkelanjutan adalah memvalidasi batas otentikasi langsung di lapisan aplikasi melalui automated regression test suites di pipeline CI.
Vulnerability Vector: Header Smuggling & State Jumping
Penyerang sering menggunakan dua vektor utama untuk membobol alur otentikasi tanpa memicu signature WAF standar:
- Header Parsing Discrepancies: Injeksi raw bytes (null-byte
\x00, multiline header values, atau doubleAuthorizationheaders) yang diproses secara permisif oleh runtime aplikasi backend namun diabaikan oleh upstream proxy. - State Machine Desynchronization: Melewati fase intermediat (misalnya mengirim request Step 3: SMS/TOTP validation tanpa pernah mengeksekusi Step 1: Password authentication atau Step 2: Challenge issuance), mengandalkan session store yang implicitly menginisialisasi intermediate states sebagai truthy.
Uji regresi CI harus mengeksekusi request langsung ke HTTP entry point aplikasi secara deterministik, memverifikasi bahwa parser internal menolak format anomali secara konsisten sebelum pipeline deployment disetujui.
Arsitektur Test Harness Deterministik
Uji regresi keamanan tingkat CI harus cepat, terisolasi, dan tidak bergantung pada dependensi jaringan eksternal. Gunakan in-memory HTTP server dan in-memory state store (seperti Redis test mock atau slice internal) untuk mengisolasi boundary logic.
Harness ini memanfaatkan modul httptest dan standard library net/http di Go untuk menyuntikkan raw bytes dan manipulasi state secara presisi.
package auth_test
import (
"bytes"
"context"
"crypto/subtle"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
)
type SessionState int
const (
StateInit SessionState = iota
StatePasswordVerified
StateMFAIssued
StateFullyAuthenticated
)
type Session struct {
ID string
State SessionState
ExpiresAt time.Time
}
type AuthServer struct {
mu sync.Mutex
sessions map[string]*Session
}
func NewAuthServer() *AuthServer {
return &AuthServer{
sessions: make(map[string]*Session),
}
}
func (s *AuthServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 1. Strict Header Validation: Tolak multiple Authorization headers
authHeaders := r.Header.Values("Authorization")
if len(authHeaders) > 1 {
http.Error(w, "Ambiguous Authorization Headers", http.StatusBadRequest)
return
}
// 2. Strict Header Validation: Tolak control characters dalam Header
for name, values := range r.Header {
for _, v := range values {
for _, b := range []byte(v) {
if b < 0x20 || b == 0x7f {
http.Error(w, fmt.Sprintf("Malformed character in %s", name), http.StatusBadRequest)
return
}
}
}
}
switch r.URL.Path {
case "/auth/step1":
s.handleStep1(w, r)
case "/auth/step2-verify-mfa":
s.handleStep2(w, r)
default:
http.NotFound(w, r)
}
}
func (s *AuthServer) handleStep1(w http.ResponseWriter, r *http.Request) {
sessionID := "sess_" + fmt.Sprintf("%d", time.Now().UnixNano())
s.mu.Lock()
s.sessions[sessionID] = &Session{
ID: sessionID,
State: StatePasswordVerified,
ExpiresAt: time.Now().Add(5 * time.Minute),
}
s.mu.Unlock()
w.Header().Set("Set-Cookie", "SID=" + sessionID + "; HttpOnly")
w.WriteHeader(http.StatusOK)
}
func (s *AuthServer) handleStep2(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("SID")
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
s.mu.Lock()
sess, exists := s.sessions[cookie.Value]
s.mu.Unlock()
// Enforce strict sequential state transition
if !exists || sess.State != StatePasswordVerified {
http.Error(w, "Invalid State Sequence", http.StatusForbidden)
return
}
var payload struct {
Code string `json:"code"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "Bad Payload", http.StatusBadRequest)
return
}
// Constant-time check untuk mitigasi timing attack
expectedCode := "849201"
if subtle.ConstantTimeCompare([]byte(payload.Code), []byte(expectedCode)) != 1 {
http.Error(w, "Invalid Challenge", http.StatusUnauthorized)
return
}
sess.State = StateFullyAuthenticated
w.WriteHeader(http.StatusOK)
}
Uji Validasi Evasion Header Parsing
Penyerang sering mengirim raw socket data yang memuat null bytes atau duplikasi header untuk memintas proxy parser. Menggunakan high-level HTTP client library sering kali gagal mereproduksi masalah ini karena client secara otomatis menormalkan data. Gunakan low-level net.Dial pada HTTP mock server untuk mengirim evasion payload secara mentah.
func TestRawHeaderEvasionRejection(t *testing.T) {
srv := httptest.NewServer(NewAuthServer())
defer srv.Close()
conn, err := net.Dial("tcp", srv.Listener.Addr().String())
if err != nil {
t.Fatalf("Gagal inisialisasi raw socket: %v", err)
}
defer conn.Close()
// Evasion: Injeksi null-byte dan duplicate Authorization headers
rawPayload := "POST /auth/step1 HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Authorization: Bearer valid_token\r\n" +
"Authorization: Bearer injected\x00_token\r\n" +
"Content-Length: 0\r\n\r\n"
if _, err := conn.Write([]byte(rawPayload)); err != nil {
t.Fatalf("Gagal write raw payload: %v", err)
}
respBuf := make([]byte, 1024)
n, _ := conn.Read(respBuf)
response := string(respBuf[:n])
if !bytes.Contains(respBuf[:n], []byte("400 Bad Request")) {
t.Errorf("Evasion berhasil lolos, server tidak mengembalikan HTTP 400:\n%s", response)
}
}
Uji Replay Timing Attack & State Jumping
Pola bypass state machine terjadi saat penyerang mengeksekusi tahapan validasi akhir menggunakan identitas dummy tanpa inisialisasi state awal. Assertion harus memverifikasi bahwa state transitif ditolak dengan HTTP 403 Forbidden atau HTTP 401 Unauthorized, bukan HTTP 500 Internal Server Error (yang mengindikasikan nil-pointer exception atau unhandled condition).
func TestStrictStateMachineBypass(t *testing.T) {
auth := NewAuthServer()
ts := httptest.NewServer(auth)
defer ts.Close()
client := &http.Client{Timeout: 2 * time.Second}
// 1. Eksekusi MFA Verify langsung dengan session ID acak (State Jumping Attack)
bypassBody := bytes.NewBufferString(`{"code":"849201"}`)
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/auth/step2-verify-mfa", bypassBody)
req.AddCookie(&http.Cookie{Name: "SID", Value: "sess_fake_bypass_123"})
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Request error: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
t.Fatalf("State machine bypass lolos. Expected 403, got: %d", resp.StatusCode)
}
}
Integrasi pada Pipeline CI
Agar regresi ini berjalan efektif tanpa menambah overhead build time, terapkan prinsip-prinsip berikut:
- Parallel Execution: Jalankan security regression suite terpisah dari end-to-end (E2E) browser test. Karena berbasis in-memory mock, runtime suite harus di bawah 5 detik.
- Exit Code Enforcement: Konfigurasikan runner (seperti GitHub Actions atau GitLab CI) agar membatalkan build apabila terdapat failure pada suite
*_security_test.go. - Deterministic Assertions: Hindari
time.Sleeppada verifikasi replay timing. Gunakan clock mocking ataucrypto/subtleverification counter untuk menjaga determinisme uji tanpa flakiness.
Verifikasi auth di level unit/integration test menjamin keamanan software supply chain sebelum binary terdistribusi ke stage deployment, menutup cela di runtime bahkan saat layer perimeter firewall mengalami bypass.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!