Skip to content

Thực hành: Security Audit Challenge

🎯 Mục tiêu

🎯 Sau bài thực hành này, bạn sẽ:

  • Nhận diện các lỗ hổng bảo mật phổ biến trong Python code
  • Sửa lỗi SQL injection, path traversal, unsafe deserialization
  • Áp dụng security best practices vào dự án thực tế

Mô tả bài tập

Bạn được giao nhiệm vụ audit một ứng dụng web Python có nhiều lỗ hổng bảo mật. Nhiệm vụ là tìm ra tất cả vulnerabilities và viết lại code an toàn. Đây là kỹ năng thiết yếu cho mọi developer chuyên nghiệp.

Yêu cầu

Bài 1: SQL Injection

Tìm và sửa lỗ hổng SQL injection trong đoạn code sau.

python
import sqlite3

def get_user_unsafe(username: str) -> dict:
    """⚠️ CODE CÓ LỖ HỔNG — TÌM VÀ SỬA"""
    conn = sqlite3.connect("app.db")
    cursor = conn.cursor()
    query = f"SELECT * FROM users WHERE username = '{username}'"
    cursor.execute(query)
    row = cursor.fetchone()
    conn.close()
    return {"id": row[0], "username": row[1]} if row else None

# Attacker có thể truyền: username = "' OR '1'='1"
# TODO: Viết lại hàm get_user_safe sử dụng parameterized queries

Bài 2: Path Traversal

Sửa lỗi cho phép truy cập file ngoài thư mục cho phép.

python
import os

def read_file_unsafe(filename: str) -> str:
    """⚠️ CODE CÓ LỖ HỔNG — TÌM VÀ SỬA"""
    file_path = os.path.join("/app/uploads", filename)
    with open(file_path, "r") as f:
        return f.read()

# Attacker có thể truyền: filename = "../../etc/passwd"
# TODO: Viết lại với path validation để ngăn directory traversal

Bài 3: Unsafe Deserialization

Tìm lỗ hổng trong việc sử dụng pickle để deserialize dữ liệu.

python
import pickle
import base64

def load_session_unsafe(session_data: str) -> dict:
    """⚠️ CODE CÓ LỖ HỔNG — TÌM VÀ SỬA"""
    decoded = base64.b64decode(session_data)
    return pickle.loads(decoded)

# Attacker có thể craft pickle payload thực thi code tùy ý
# TODO: Thay thế bằng phương pháp an toàn (json, hmac signing)

Bài 4: Hardcoded Secrets

Tìm và sửa các vấn đề về quản lý secrets.

python
class AppConfig:
    """⚠️ CODE CÓ LỖ HỔNG — TÌM VÀ SỬA"""
    DB_PASSWORD = "super_secret_123"
    API_KEY = "sk-abc123def456"
    JWT_SECRET = "my-jwt-secret"

    @staticmethod
    def get_db_url():
        return f"postgresql://admin:{AppConfig.DB_PASSWORD}@localhost/mydb"

# TODO: Refactor để lấy secrets từ environment variables
# Sử dụng os.environ hoặc python-dotenv

Gợi ý

Gợi ý Bài 1
  • Dùng parameterized query: cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
  • Placeholder ? (SQLite) hoặc %s (PostgreSQL) ngăn SQL injection
  • Không bao giờ dùng f-string hoặc .format() cho SQL queries
Gợi ý Bài 2
  • Dùng Path.resolve() để chuẩn hóa path, loại bỏ ..
  • Kiểm tra resolved_path.is_relative_to(base_dir) (Python 3.9+)
  • Hoặc so sánh os.path.commonpath([base_dir, resolved]) với base_dir
Gợi ý Bài 3
  • pickle.loads() có thể thực thi arbitrary code — không an toàn với untrusted data
  • Thay bằng json.loads() cho dữ liệu đơn giản
  • Nếu cần serialize phức tạp, dùng HMAC để verify integrity trước khi load
Gợi ý Bài 4
  • os.environ.get("DB_PASSWORD") lấy từ environment variable
  • python-dotenv load từ file .env (không commit vào git)
  • Raise error rõ ràng nếu secret không được cấu hình

Lời giải tham khảo

Xem lời giải (Bài 1 & 2 — Bài 3, 4 tự hoàn thành từ gợi ý)
python
import sqlite3
from pathlib import Path

# Bài 1: Parameterized query
def get_user_safe(username: str) -> dict | None:
    conn = sqlite3.connect("app.db")
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
    row = cursor.fetchone()
    conn.close()
    return {"id": row[0], "username": row[1]} if row else None

# Bài 2: Path traversal prevention
UPLOAD_DIR = Path("/app/uploads").resolve()

def read_file_safe(filename: str) -> str:
    file_path = (UPLOAD_DIR / filename).resolve()
    if not file_path.is_relative_to(UPLOAD_DIR):
        raise PermissionError("Truy cập file ngoài thư mục không được phép")
    with open(file_path, "r") as f:
        return f.read()