White-Box Auditing & Săn Bug Thực Chiến
~14 phútTaint Analysis & Static Code AuditingMiễn phí

White-Box Audit Go & Node.js: SQL Formatting và Prototype Pollution

Mổ xẻ các lỗi mã nguồn đặc thù trong Go và JavaScript: Format string SQL trong pgx/database/sql, Prototype Pollution qua merge/clone và ReDoS.

TL;DR: Trong khi Go được coi là một ngôn ngữ an toàn bộ nhớ (Memory-safe), việc sử dụng các hàm định dạng chuỗi như fmt.Sprintf để ghép câu truy vấn SQL hoặc cấu hình parser không chặt chẽ vẫn thường xuyên gây ra SQL Injection và Command Injection. Ngược lại, trong thế giới Node.js, tính chất kế thừa nguyên mẫu (Prototype Inheritance) là nguồn gốc của lỗ hổng Prototype Pollution: khi ứng dụng merge đệ quy một đối tượng JSON từ client mà không kiểm tra thuộc tính __proto__, kẻ tấn công có thể ghi đè các hàm gốc của Object.prototype, dẫn tới vượt qua kiểm tra quyền hoặc thậm chí Remote Code Execution qua các child process.

1. Scenario 1 (Golang) — Cạm bẫy ghép chuỗi mệnh đề ORDER BY

Một microservice xử lý đơn hàng viết bằng Go (dùng framework Chi và thư viện pgx). Lập trình viên hiểu rất rõ nguyên lý Parameterized Query nên đã dùng $1, $2 cho các mệnh đề WHERE. Tuy nhiên, khi cần hỗ trợ người dùng sắp xếp danh sách theo cột tùy chọn (sortBy) và thứ tự (sortDir):

Go
// Vulnerable Go Controller
func GetOrders(w http.ResponseWriter, r *http.Request) {
    sortBy := r.URL.Query().Get("sort_by")   // Tainted Source
    sortDir := r.URL.Query().Get("sort_dir") // Tainted Source

    // Parameterized queries do NOT support column names as parameters!
    // Developer falls back to string formatting:
    query := fmt.Sprintf("SELECT id, total, status FROM orders WHERE user_id = $1 ORDER BY %s %s", sortBy, sortDir)
    
    rows, err := db.Query(r.Context(), query, currentUserID)
    // ...
}

Do cơ sở dữ liệu SQL không cho phép tham số hóa tên cột hoặc từ khóa ASC/DESC, lập trình viên đã dùng fmt.Sprintf để ghép chuỗi. Kẻ tấn công chỉ cần truyền: ?sort_by=(CASE WHEN (SELECT ASCII(SUBSTRING(password,1,1)) FROM users WHERE id=1)>70 THEN id ELSE status END) để biến mệnh đề sắp xếp thành một kênh suy luận Blind SQL Injection hoàn chỉnh.

2. Scenario 2 (Node.js) — Đột biến gen trong Prototype Inheritance

Một ứng dụng Node.js (Express) cung cấp API cho phép người dùng cập nhật cài đặt giao diện (Theme preferences). Lập trình viên viết một hàm tiện ích để gộp (merge) cấu hình gửi lên vào đối tượng cấu hình mặc định của hệ thống:

JavaScript
// Vulnerable recursive merge function in Node.js
function deepMerge(target, source) {
    for (let key in source) {
        if (typeof source[key] === 'object' && source[key] !== null) {
            if (!target[key]) target[key] = {};
            deepMerge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
    return target;
}

Kẻ tấn công gửi một JSON payload chứa thuộc tính đặc biệt:

JSON
{
  "theme": "dark",
  "__proto__": {
    "isAdmin": true,
    "shell": "/bin/sh"
  }
}

Khi hàm deepMerge duyệt qua key __proto__, nó không gán vào bản thân đối tượng target mà gán thẳng vào Object.prototype — mẫu nguyên bản của mọi đối tượng trong toàn bộ tiến trình Node.js. Kể từ thời điểm này, bất kỳ đối tượng nào trong hệ thống, bao gồm cả một đối tượng người dùng mới được tạo:

JavaScript
const user = {};
console.log(user.isAdmin); // Output: true!

Toàn bộ hệ thống kiểm tra phân quyền if (user.isAdmin) trong toàn bộ ứng dụng bị sụp đổ hoàn toàn.

Cơ chế Ô nhiễm Nguyên mẫu (Prototype Pollution) trong Node.js

3. Analogy — Chỉnh sửa DNA tế bào gốc và đại dịch di truyền

Hãy hình dung nguyên lý di truyền sinh học:

  • Mỗi con người sinh ra đều thừa hưởng các đặc tính chung từ bộ mã gen người nguyên thủy (Tế bào gốc / Object.prototype): ví dụ có 2 mắt, 1 trái tim và khả năng hít thở.
  • Nếu một người đi phẫu thuật thẩm mỹ (sửa đổi thuộc tính của riêng cá nhân họ: user.color = 'blue'), chỉ có người đó thay đổi, thế hệ sau hoặc những người khác trong xã hội không hề bị ảnh hưởng.
  • Prototype Pollution giống như việc một kẻ độc ác tiêm một đoạn mã biến đổi gen trực tiếp vào buồng ấp tế bào gốc của toàn bộ loài người. Kể từ giây phút đó, mọi đứa trẻ sinh ra trong tương lai đều tự động mang thêm một chiếc đuôi hoặc bị nhiễm độc tố mà không ai hiểu nguyên nhân từ đâu.

4. Các Sink nhạy cảm trong Go và Node.js hoạt động ra sao?

4.1. Mổ xẻ Go Sinks: Exec và Database Formatting

Trong các dự án Go lớn, kiểm thử viên hộp trắng cần đặt cờ cảnh báo tại các vị trí:

  1. fmt.Sprintf trong SQL: Luôn truy vết biến truyền vào format string %s xem có bắt nguồn từ r.URL.Query() hay json.Unmarshal() hay không.
  2. exec.Command thiếu kiểm soát:
    Go
    // Dangerous: Running shell commands with user input
    cmd := exec.Command("bash", "-c", "ping -c 1 " + targetHost)
    
    Nếu targetHost chứa ; hoặc |, kẻ tấn công có thể chèn lệnh tùy ý. Giải pháp là không gọi qua shell, truyền danh sách đối số trực tiếp: exec.Command("ping", "-c", "1", targetHost).

4.2. Từ Prototype Pollution leo thang lên RCE trong Node.js

Nhiều người lầm tưởng Prototype Pollution chỉ có thể sửa đổi thuộc tính logic. Trong thực tế, trên môi trường Node.js, Prototype Pollution có thể dẫn thẳng tới Remote Code Execution thông qua hàm child_process.fork hoặc child_process.spawn:

Khi một thư viện trong dự án gọi lệnh thực thi tiến trình con:

JavaScript
const { spawn } = require('child_process');
spawn('git', ['status']); // Safe-looking command invocation

Hàm spawn đọc các tùy chọn mặc định từ đối tượng options nội bộ. Khi Object.prototype bị ô nhiễm các thuộc tính:

  • shell: true
  • NODE_OPTIONS: "--inspect"
  • env: { ... }

Node.js sẽ kích hoạt shell hệ thống để thực thi câu lệnh kèm theo các biến môi trường do kẻ tấn công chỉ định, biến một lệnh git status đơn giản thành một đòn tấn công RCE toàn diện.

Thử đoán trước

Trong Go, nếu lập trình viên bắt buộc phải cho phép người dùng sắp xếp dữ liệu theo các cột khác nhau (id, created_at, price), biện pháp nào an toàn nhất để loại bỏ hoàn toàn SQL Injection trong mệnh đề ORDER BY?

Biện pháp an toàn duy nhất là sử dụng Bản đồ ánh xạ Whitelist (Column Whitelist Mapping): Tạo một switch-case hoặc một map[string]string cố định ánh xạ chuỗi đầu vào của người dùng thành tên cột an toàn trong mã nguồn:

Go
var allowedSortColumns = map[string]string{
    "id":         "id",
    "created_at": "created_at",
    "price":      "price",
}
// Fallback to safe default column "id" if key not found
col, ok := allowedSortColumns[sortBy]
if !ok { col = "id" }

5. Pitfalls & Giải pháp phòng thủ triệt để

❌ Bẫy thường gặp trong Node.js: Chỉ lọc key "proto"

Nhiều lập trình viên chỉ viết regex chặn chuỗi "__proto__". Kẻ tấn công có thể thay thế bằng đường dẫn gián tiếp qua constructor: {"constructor": {"prototype": {"isAdmin": true}}} Bộ lọc sẽ bị qua mặt hoàn toàn.

✅ Giải pháp chuẩn mực: Khóa chặt môi trường

  1. Phòng thủ Go: Sử dụng Code Generation (như sqlc): Sử dụng công cụ biên dịch như sqlc để sinh mã Go từ các file SQL thuần có kiểm tra kiểu dữ liệu compile-time, loại bỏ hoàn toàn việc ghép chuỗi động bằng fmt.Sprintf.
  2. Phòng thủ Node.js: Ngăn chặn Prototype Pollution:
    • Tạo Object không có prototype: const safeObj = Object.create(null); (đối tượng này không có __proto__, miễn nhiễm 100%).
    • Đóng băng nguyên mẫu: Object.freeze(Object.prototype); ngay khi ứng dụng khởi động.
    • Sử dụng Map: Dùng cấu trúc dữ liệu Map cho các tập hợp key-value động từ người dùng thay vì plain object.
    • Chạy Node với cờ an toàn: Sử dụng cờ runtime --disable-proto=delete trên Node.js v12+.

6. Liên hệ các bài khác

Kiến thức audit đa ngôn ngữ giúp bạn nhận diện các điểm yếu tương đồng trên nhiều nền tảng:

7. Tóm tắt

  • Trong Go, việc dùng fmt.Sprintf trong các truy vấn SQL (đặc biệt là ORDER BY và tên bảng) là nguyên nhân hàng đầu gây ra SQLi.
  • Luôn sử dụng danh sách trắng (Whitelist Mapping) cho các mệnh đề SQL không thể tham số hóa.
  • Prototype Pollution trong JavaScript xảy ra khi merge không an toàn dữ liệu người dùng, cho phép sửa đổi hành vi của mọi object trong tiến trình.
  • Prototype Pollution có thể leo thang thành Remote Code Execution thông qua các tùy chọn thực thi của child_process.
  • Phòng thủ Prototype Pollution bằng Object.create(null), Object.freeze(Object.prototype) hoặc sử dụng Map.

8. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Tại sao cơ chế Prepared Statements trong các hệ quản trị CSDL lại không hỗ trợ tham số hóa tên cột trong mệnh đề ORDER BY?
  2. Q2
    Trong Node.js, tại sao payload '{"constructor": {"prototype": {"polluted": true}}}' lại có tác dụng tương đương với '__proto__'?
  3. Q3
    Làm thế nào để một lỗ hổng Prototype Pollution dẫn tới Remote Code Execution khi ứng dụng sử dụng child_process.spawn()?
  4. Q4
    Cờ runtime --disable-proto=delete trong Node.js bảo vệ ứng dụng như thế nào?
  5. Q5
    Tại sao việc dùng thư viện 'sqlc' trong Go lại giúp loại bỏ gần như 100% các lỗ hổng SQL Injection?

Bài tiếp theo: Viết Custom Rules Semgrep: Tự động hóa Audit trong CI/CD

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Viết Custom Rules Semgrep: Tự động hóa Audit trong CI/CD