BlogTech

[Debug Thực Chiến – Kỳ 2] Lệnh Truy Nã “Go-http-client”: Khi ZaloPay Bị Firewall Tống Cổ Ngay Cửa

Tác giả Cậu bé chăn bò
Cậu bé chăn bò 14 phút đọc

Chào anh em, lại là tôi — Cậu bé chăn bò đây.

Ở Kỳ 1, chúng ta đã dùng tcpdump lôi cổ được thằng bug SNI ra ánh sáng. Sau khi cấu hình lại default_server cho Nginx, Access Log đã bắt đầu ghi nhận request từ ZaloPay chui vào hệ thống. Tưởng xong — nhưng không.

Đập vào mắt tôi trong file log không phải mã 200 OK thần thánh, mà là một dòng đỏ rực:

POST /wp-json/fuver/v1/zalopay-webhook HTTP/1.1" 403 Forbidden

Gói tin đã lọt qua Nginx, nhưng bị một kẻ sát nhân vô hình chém đứt đầu ngay tắp lự. Mời anh em bước vào Kỳ 2: hành trình đục lỗ bọc thép qua hai tầng bảo vệ mà chính tay mình dựng lên.


TL;DR cho anh em bận rộn:

  • Triệu chứng: Nginx nhận Webhook ZaloPay nhưng trả về 403 Forbidden. Postman test vẫn pass 100%.
  • Nguyên nhân tầng 1 (Nginx): 8G Firewall chặn User-Agent Go-http-client/1.1 — cái tên mặc định của HTTP Client Golang.
  • Nguyên nhân tầng 2 (WordPress): Một hàm chống DDoS nằm trong functions.php cũng blacklist đúng cái User-Agent đó.
  • Giải pháp: Whitelist đường dẫn Webhook tại Nginx + IP Whitelisting trong functions.php.

1. Cô Lập Lỗi: Postman Vs ZaloPay

Trước khi mổ xẻ, cần biết 403 đến từ tầng nào — Nginx hay WordPress? Tôi không đoán mò. Tôi dùng đòn Isolate Debugging: tạo một file PHP thuần, không có logic gì cả, chỉ làm một việc duy nhất là hứng data và ghi log.

php

<?php
$raw_data = file_get_contents('php://input');
file_put_contents(__DIR__ . '/zalo_raw_debug.log', $raw_data);
echo json_encode(["return_code" => 1, "return_message" => "success"]);

File này được đặt thẳng ở thư mục gốc, không qua WordPress routing. Tôi trỏ Callback URL trên ZaloPay Merchant Portal sang file này và quẹt thử một đơn.

Kết quả: File log trắng trơn. Request bị Nginx từ chối trước khi kịp chạm vào PHP.

Tôi mở Postman, bắn thẳng vào cùng cái link đó. Kết quả: Log ghi nhận đầy đủ, trả về success.

Cùng một file. Cùng một server. Điểm khác biệt duy nhất: HTTP Headers mà hai bên mang theo.


2. Thủ Phạm Lộ Diện: User-Agent “Go-http-client/1.1”

Tôi dở header mà ZaloPay gửi đi lên soi. Và ngay dòng User-Agent:

User-Agent: Go-http-client/1.1

Đây là cái tên mặc định mà thư viện HTTP Client của Golang tự động đính kèm khi dev không khai báo User-Agent tường minh.

Vấn đề không phải ở Golang — đây là ngôn ngữ tuyệt vời, được dùng cho hệ thống Microservices chịu tải cao như ZaloPay vì khả năng xử lý đồng thời (Goroutines) cực mạnh. Vấn đề nằm ở chỗ Golang cũng là ngôn ngữ hacker yêu thích để viết tool scan, Brute-force, Botnet — vì build ra file thực thi đơn giản và chạy siêu nhanh.

Hệ quả: các hệ thống WAF toàn cầu (Cloudflare, ModSecurity, 8G Firewall…) đều đã học thuộc lòng cái User-Agent này và áp một luật bất thành văn: thấy Go-http-client là chặn, không cần trình bày.

Web của tôi đang dùng 8G Firewall tích hợp tại tầng Nginx. ZaloPay lấp ló ngoài cửa, 8G check thấy Go-http-client, chém không thương tiếc. Đây là nỗi oan của ZaloPay — bị vạ lây vì cái User-Agent mặc định quá “hacker-friendly”.


3. Fix Tầng Nginx: Mở Đường Máu Cho Webhook

Bắt đúng bệnh rồi thì bốc thuốc. Tôi mở file config Nginx, thêm một rule Regex để 8G Firewall bỏ qua kiểm tra với các đường dẫn Webhook:

nginx

# Whitelist Webhook thanh toán khỏi 8G Firewall
if ($request_uri ~* "^/wp-json/fuver/v1/(acb-webhook|vtp-webhook|zalopay-webhook)") {
    set $bypass_8g 1;
}

Reload Nginx. Gói tin đã xuyên qua tầng server, chính thức chạm được vào mã nguồn WordPress.

Nhưng nếu anh em nghĩ đến đây là xong thì hơi vội.


4. Trùm Cuối Trong Bóng Tối: wp_die Từ functions.php

Test lại đơn thứ tư. Data vào được rồi, nhưng WordPress trả về:

json

{
    "code": "wp_die",
    "message": "Access denied.",
    "data": { "status": 403 }
}

wp_die là thứ đặc sản không lẫn vào đâu được của WordPress. Nginx không bao giờ đẻ ra cái lỗi này. Nghĩa là Nginx đã cho qua, nhưng một thứ gì đó bên trong WordPress đang làm thủ tục tiễn khách.

Web không cài Wordfence, không cài iThemes Security. Tôi mò thẳng vào functions.php. Và đây rồi — di sản của một ông dev đời trước:

php

// Block suspicious user agents
function block_suspicious_user_agents() {
    $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
    $blocked_agents = array(
        'libwww-perl', 'sqlmap', 'nikto', 'Go-http-client' // <-- kẻ thù ở đây
    );
    
    foreach ($blocked_agents as $agent) {
        if (stripos($user_agent, $agent) !== false) {
            wp_die('Access denied.', 'Security Check', array('response' => 403));
        }
    }
}
add_action('init', 'block_suspicious_user_agents');

Hook init là một trong những hook chạy sớm nhất của WordPress. Khi ZaloPay chọc vào REST API endpoint, nó chưa kịp đến được Class giải mã Webhook thì đã bị tóm cổ, check thấy Go-http-client, và bị chém không trượt phát nào.


5. Giải Pháp Đúng Nghĩa: IP Whitelisting

Lúc này nhiều anh em sẽ bảo: xóa Go-http-client ra khỏi cái mảng blacklist là xong. Đừng làm vậy.

Hàm đó đang ngăn hàng nghìn request rác mỗi ngày. Nếu xóa điều kiện mà không có gác cổng thay thế, hacker dùng Botnet Golang nã vào đúng cái endpoint Webhook đang mở — VPS của anh em sẽ không trụ được lâu.

Giải pháp đúng: không xét thẻ căn cước (User-Agent) nữa, xét thẳng biển số xe (IP Address). IP thuộc dải ZaloPay thì cho qua, IP khác thì chặn — kể cả khi nó cũng mang Go-http-client.

php

function block_suspicious_user_agents() {
    $request_uri = $_SERVER['REQUEST_URI'] ?? '';
    $client_ip   = $_SERVER['REMOTE_ADDR'] ?? '';

    // Bọc thép riêng cho Webhook ZaloPay
    if (strpos($request_uri, '/wp-json/fuver/v1/zalopay-webhook') !== false) {
        $zalopay_ips = array(
            '118.102.5.66', // Sandbox
            '113.163.x.x',  // Production
        );

        if (in_array($client_ip, $zalopay_ips)) {
            return; // IP chuẩn ZaloPay → cho đi
        }

        wp_die('Invalid Webhook Source.', 'Security', array('response' => 403));
    }

    // Giữ nguyên logic check User-Agent cho các route khác
    $user_agent     = $_SERVER['HTTP_USER_AGENT'] ?? '';
    $blocked_agents = array('libwww-perl', 'sqlmap', 'nikto', 'Go-http-client');

    foreach ($blocked_agents as $agent) {
        if (stripos($user_agent, $agent) !== false) {
            wp_die('Access denied.', 'Security Check', array('response' => 403));
        }
    }
}
add_action('init', 'block_suspicious_user_agents');

Góc Thực Hành: Tự Tay Test Tường Lửa

Lý thuyết xuông thì hơi khô khan. Tôi có code nhanh một cái sa bàn mô phỏng lại y hệt logic của hệ thống tường lửa mà chúng ta vừa setup ở trên.

Anh em thử đóng vai hacker, hoặc đóng vai Postman, thử bật/tắt cái tính năng “IP Whitelist” bên trên xem gói tin nó bị chém đứt đầu ở tầng nào nhé. Đảm bảo test xong là hiểu tận gốc rễ vấn đề luôn:

waf-simulation.sh — ZaloPay Webhook Debug / Kỳ 2
Bật IP Whitelisting cho Webhook
📡
Client
🔥
Nginx + 8G
⚙️
WP init
🛒
WooCommerce
Chọn nguồn gửi và nhấn ▶ để mô phỏng…

Kết: Cái Ting Ting Trọn Vẹn

Lưu file. Tôi cầm điện thoại lên, mở ZaloPay, quẹt đơn hàng thứ năm.

Không cần nhìn log nữa. Nhìn thẳng vào dashboard WooCommerce. Nhấn F5. Trạng thái đơn hàng chuyển từ Pending Payment sang Processing — mượt mà, không cần giải thích thêm.

Cái âm thanh Ting Ting lúc này nghe mới thực sự trọn vẹn.


Hành trình này tốn kha khá bát mì tôm và nơ-ron thần kinh, nhưng nó là một case study hội tụ đủ ba tầng kiến thức:

Tầng Network (SNI)Tầng Server WAF (User-Agent)Tầng Application (WordPress Hooks)

Dù là MoMo, VNPay hay bất kỳ cổng thanh toán nào dở chứng — cứ nắm chắc tư duy “Cô lập → Đào sâu từng tầng”, anh em sẽ tóm được hết.


Hết rồi, trên đây là những chia sẻ thực tế về trải nghiệm của tôi khi code và gặp bug khá đau đầu, nên muốn lưu lại làm bài học cũng như là chia sẻ với ae coder, cảm ơn ae đã đón đọc bài viết của tôi, thấy hay thì để lại 1 cmt, 1 share nhé!

Bình luận (0)