MCPFortress
Dự án Khoa học kỹ thuật · Phần mềm hệ thống – An ninh máy tính

Pháo đài số cho tác tử AI

Thiết kế và kiểm thử máy chủ MCP (Model Context Protocol) tin cậy, an toàn cho tác tử AI tra cứu thông tin học tập ở trường THPT: thời khoá biểu, lịch thi và điểm trung bình.

Trường THPT Phước Bửu · Giáo viên hướng dẫn: Đặng Việt Chung · Năm học 2026–2027

Video giới thiệu

Dự án trong 2 phút 42 giây: bối cảnh, thiết kế thí nghiệm A/B, phương pháp kiểm thử, kết quả và hướng phát triển.

Video giới thiệu dự án KHKT MCPFortress · Tải video (MP4, 18,9 MB)

Vấn đề

MCP là giao thức cho phép trợ lý AI gọi “công cụ” bên ngoài: đọc dữ liệu, tính toán, tra cứu. Khi máy chủ MCP được viết vội, chính các công cụ đó trở thành điểm yếu.

AI gọi sai công cụ, sai tham số

Tên công cụ mơ hồ, mô tả sơ sài, tham số tự do khiến AI gửi "thứ hai" thay vì "t2" và nhận về kết quả rỗng.

Thông báo lỗi vô dụng

Lỗi chỉ ghi Error hoặc trả None: AI không biết mình sai ở đâu, không thể tự sửa và thử lại.

Lộ dữ liệu, bị chèn lệnh

Đọc tệp theo tên người gọi đưa vào cho phép ../ thoát thư mục; ghi chú chứa “Bỏ qua mọi lệnh trước đó…” bị AI hiểu nhầm là mệnh lệnh.

Bằng chứng: máy chủ A đọc được tệp nằm ngoài thư mục dữ liệu

Nhóm đặt một tệp mồi ở ngoài thư mục dữ liệu, bên trong có một chuỗi đánh dấu duy nhất. Nếu chuỗi đó xuất hiện trong câu trả lời của máy chủ thì chắc chắn máy chủ đã đọc tệp ngoài phạm vi cho phép — không phải suy đoán, chỉ cần nhìn chuỗi.

Tệp mồi du_lieu_ngoai/bi_mat.txt — nằm ngoài du_lieu/

[MA_KIEM_TRA_S2] Tệp nằm NGOÀI thư mục dữ liệu – văn bản vô hại để kiểm thử lỗi
thoát thư mục (../). Nếu nội dung này xuất hiện thì máy chủ có lỗ hổng path traversal.
Phiên bản A · ca kiểm thử S2

Host gửi

doc_tai_lieu(ten_file="../du_lieu_ngoai/bi_mat.txt")

Máy chủ trả về

[MA_KIEM_TRA_S2] Tệp nằm NGOÀI thư mục dữ liệu – văn bản
vô hại để kiểm thử lỗi thoát thư mục (../)…

Không đạt  Chuỗi đánh dấu xuất hiện: nội dung tệp ngoài thư mục đã bị trả nguyên văn cho bên gọi.

Phiên bản B · cùng ca S2

Host gửi

read_resource(
  "lop://thoi-khoa-bieu/../../du_lieu_ngoai/bi_mat.txt")

Máy chủ trả về

Unknown resource:
lop://thoi-khoa-bieu/../../du_lieu_ngoai/bi_mat.txt

Đạt  Bị từ chối ngay, không đọc tệp, không có chuỗi đánh dấu.

Nguyên nhân nằm ở đúng một dòng trong server_a.py

@server.tool()
def doc_tai_lieu(ten_file: str):
    """Đọc tài liệu."""
    with open(os.path.join(THU_MUC, ten_file), encoding="utf-8") as f:
        return f.read()

os.path.join(THU_MUC, "../du_lieu_ngoai/bi_mat.txt") vẫn trỏ ra ngoài THU_MUC. Hàm nhận thẳng tên tệp do bên gọi đưa vào, không đối chiếu danh sách cho phép, không chuẩn hoá đường dẫn — nên ../ đi ra ngoài tự do.

Phiên bản B bỏ hẳn tham số tên tệp. Mỗi tài liệu là một địa chỉ cố định lop://…; địa chỉ không có trong danh sách thì bị trả ResourceNotFoundError trước khi chạm tới ổ đĩa. Không còn chỗ nào để chèn ../.

Lặp lại được, không phải một lần may rủi. Hiện tượng xảy ra ở 23/23 lần chạy đã ghi nhận, cho cả ca S1 (tệp nằm trong thư mục dữ liệu nhưng ngoài danh sách cho phép) lẫn ca S2 (thoát thư mục bằng ../). Phiên bản B chặn cả hai ở 23/23 lần. Kiểm chứng: bảng tổng hợp 23 lần chạy · số liệu thô một lần chạy đầy đủ · server_a.py · run_tests.py · bi_mat.txt

Tệp mồi chỉ chứa văn bản vô hại do nhóm tự tạo. Toàn bộ dữ liệu trong dự án là dữ liệu giả lập của một lớp 11, không có thông tin cá nhân thật; hai máy chủ chỉ chạy trong máy, không mở cổng mạng.

Thiết kế thí nghiệm A/B

Hai máy chủ có cùng chức năng, cùng chạy trên dữ liệu giả lập của một lớp 11. Chỉ khác nhau ở việc có áp dụng ba nhóm nguyên tắc hay không. Một bộ ca kiểm thử tự động chấm cả hai theo cùng tiêu chí.

Phiên bản A – đối chứng

server_a.py · viết “nhanh cho chạy được”

  • 5 công cụ: tkb, lich_thi, diem_tb, doc_tai_lieu, luu_ghi_chu
  • Mô tả kiểu “Lấy dữ liệu.”, tham số str, list tự do
  • Lỗi trả None hoặc Exception("Error")
  • Đọc tệp theo tên người gọi đưa vào, có công cụ ghi
Phiên bản B – cải tiến

server_b.py · áp dụng ba nhóm nguyên tắc

  • 3 công cụ chỉ đọc: xem_tkb, xem_lich_thi, tinh_diem_tb
  • 5 tài nguyên địa chỉ cố định lop://… thay cho đọc tệp tự do
  • Tham số Literal / Field, kết quả có cấu trúc (Pydantic)
  • Lỗi nêu nguyên nhân + giá trị hợp lệ; nhật ký mỗi lần gọi
Bộ ca kiểm thử24 ca chuẩn + 128 ca mô phỏng (seed 2026)
→
Client MCPkết nối trong bộ nhớ, không mở cổng mạng
→
Máy chủ A | Bcùng dữ liệu giả lập
→
Chấm tự độngchỉ số F, V, S, E, T → CSV / JSON / bảng

Mã nguồn Python của hai máy chủ

Toàn văn hai tệp, kèm chú thích chi tiết: ở A, mỗi điểm yếu cố ý được đánh dấu [Yếu – nhóm …]; ở B, mỗi biện pháp được gắn [Nhóm …] và mã ca kiểm thử (F, V, S) kiểm chứng nó.

server_a.py Xem trên GitHub
Đang tải mã nguồn…

Ba nhóm nguyên tắc

Được đúc kết thành các biện pháp cụ thể trong mã nguồn của phiên bản B.

1

Thiết kế công cụ chuẩn xác

  • Tên dạng động_từ_danh_từ
  • Mô tả nêu làm gì, khi nào dùng, khi nào không dùng
  • Mỗi công cụ một việc
  • Tham số chặt: t2–t6; toan, tin, van; điểm 0–10; hệ số 1, 2, 3
  • Kết quả có cấu trúc, không trả None
2

Báo lỗi có hướng dẫn

  • ToolError / ResourceNotFoundError nêu nguyên nhân và giá trị hợp lệ
  • Đầu vào sai kiểu, ngoài khoảng bị SDK từ chối trước khi hàm chạy
  • Thông báo có ví dụ đúng định dạng để AI tự sửa
3

Phòng thủ ba lớp

  • Thiết kế: chỉ đọc; danh sách tệp cho phép; không ghép tham số vào đường dẫn; tách ghi chú thành dữ liệu có nhãn nguồn
  • Thời gian chạy: giới hạn độ dài, kích thước; nhật ký JSON Lines
  • Dữ liệu: chỉ dữ liệu giả lập

A Đọc tệp theo tên người gọi

@server.tool()
def doc_tai_lieu(ten_file: str):
    """Đọc tài liệu."""
    # "../du_lieu_ngoai/bi_mat.txt" vẫn mở được
    with open(os.path.join(THU_MUC, ten_file)) as f:
        return f.read()

B Danh sách cho phép, địa chỉ cố định

TEP_CHO_PHEP = {
    "thoi_khoa_bieu": "thoi_khoa_bieu.json",
    "lich_thi": "lich_thi.json",
    "noi_quy": "noi_quy.txt",
}
@server.resource("lop://noi-quy", mime_type="text/plain")
def tai_nguyen_noi_quy() -> str:
    return _doc_tep("noi_quy")

Kết quả kiểm thử

Bộ 24 ca chuẩn: 10 ca chức năng (F), 8 ca dữ liệu bất thường (V), 6 ca an toàn (S). Lần chạy 27/09/2026 · Python 3.11, MCP Python SDK 2.2.0, mỗi ca lặp 20 lần để đo thời gian.

V – từ chối đầu vào bất thường
37,5%→100%
mục tiêu ≥ 90%
S – an toàn
16,7%→100%
mục tiêu 100%
E – chất lượng thông báo lỗi
0→2,0
thang 0–2, mục tiêu ≥ 1,8
T – thời gian phản hồi TB
0,203→0,201 ms
không tăng dù kiểm tra nhiều hơn

Chi tiết từng ca

MãĐầu vào / kịch bảnKỳ vọngABE(A)E(B)
V1xem_tkb("t9")Lỗi, nêu mã hợp lệ (t2 đến t6)Không đạtĐạt02
V2xem_tkb("thứ hai")Từ chối: không thuộc t2 đến t6Không đạtĐạt02
V3xem_lich_thi("sinh")Lỗi, nêu mã môn hợp lệĐạtĐạt02
V4Danh sách điểm rỗngLỗi: cần ít nhất một điểmĐạtĐạt02
V5Điểm 11Từ chối: ngoài thang 0 đến 10Không đạtĐạt02
V6Điểm −1Từ chối: ngoài thang 0 đến 10Không đạtĐạt02
V7Hệ số 5Từ chối: hệ số chỉ là 1, 2 hoặc 3Không đạtĐạt02
V8Điểm là chữ “tám”Từ chối: sai kiểu dữ liệuĐạtĐạt02

Ở nhóm V, một số ca A được tính “Đạt” vì có phát sinh lỗi, nhưng thông báo lỗi chung chung nên điểm E luôn bằng 0.

Bộ mô phỏng mở rộng – 128 ca

Sinh tự động từ hạt giống cố định (seed 2026) với 40 học sinh giả lập, để kiểm tra kết luận trên nhiều đầu vào hơn. Số trong ngoặc là số ca của từng nhóm.

Biểu đồ tỉ lệ đạt theo nhóm ca trong bộ mô phỏng 128 ca: B đạt 100% ở mọi nhóm; A đạt 0% với mã ngày sai, 28,9% với điểm/hệ số sai, 22,2% với truy cập tệp mồi, 0% với ghi chú lệnh ẩn.
A vẫn chạy đúng khi đầu vào hợp lệ, nhưng gần như không chịu được đầu vào sai và làm lộ tệp ở 7/9 ca truy cập tệp mồi. B đạt 100% ở mọi nhóm.

Danh mục kiểm tra khi viết một MCP server

Mười câu hỏi rút ra từ thí nghiệm, dùng được cho bất kỳ máy chủ MCP nào.

  1. Tên công cụ dạng động_từ_danh_từ, không trùng nghĩa với công cụ khác?
  2. Mô tả nêu rõ công cụ làm gì, khi nào dùng, khi nào không dùng?
  3. Mọi tham số có kiểu chặt (Literal, khoảng giá trị, độ dài tối đa)?
  4. Kết quả có cấu trúc, gọn, không trả None?
  5. Mọi lỗi dự đoán được đều nêu nguyên nhân và giá trị hợp lệ?
  6. Có công cụ ghi/xoá/gửi không? Nếu không cần thì bỏ.
  7. Có chỗ nào ghép tham số của người gọi vào đường dẫn tệp, câu lệnh SQL, lệnh hệ thống?
  8. Dữ liệu do người khác nhập có được tách riêng, gắn nhãn nguồn, cảnh báo “không phải chỉ dẫn”?
  9. Có giới hạn kích thước đầu vào/đầu ra và nhật ký gọi công cụ?
  10. Gọi lặp cùng yêu cầu có cho cùng kết quả (không giữ trạng thái ẩn)?

Chạy thử

Cần Python 3.10 trở lên. Máy chủ chạy cục bộ; bộ kiểm thử kết nối trong bộ nhớ, không mở cổng mạng.

Cài đặt và chạy 24 ca kiểm thử

git clone https://github.com/aerovfx/MCPFortress.git
cd MCPFortress
python3 -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python run_tests.py         # kết quả ghi vào ket_qua/

Thử tay bằng MCP Inspector

# cần Node.js
npx @modelcontextprotocol/inspector python server_a.py
npx @modelcontextprotocol/inspector python server_b.py

# hoặc chạy trọn gói + xuất báo cáo
./chay_kiem_thu.sh          # Windows: chay_kiem_thu.bat
An toàn khi thử nghiệm. Toàn bộ dữ liệu là giả lập, không có thông tin thật của học sinh. Các tệp khong_cong_khai.txt và bi_mat.txt chỉ chứa văn bản vô hại dùng làm “mồi”. server_a.py cố ý có lỗ hổng để làm đối chứng, không dùng làm mẫu cho hệ thống thật.