Khi xây dựng một Web API bằng Python Flask, việc nhận dữ liệu từ client và xử lý dữ liệu đó ở phía server là thao tác rất quen thuộc.
Nhưng một câu hỏi quan trọng thường bị bỏ qua:
Server sẽ làm gì nếu input mà client gửi lên không phải là giá trị mà developer dự kiến?
Trong lab này, chúng ta xây dựng một Flask API có chức năng đọc file từ tham số filename, sau đó theo dõi toàn bộ data flow từ HTTP request → Python application → filesystem để tìm ra lỗ hổng Path Traversal.
Mục tiêu không chỉ là biết chuỗi ../, mà quan trọng hơn là hiểu vì sao một input từ HTTP request có thể tác động trực tiếp đến filesystem của server.
1️⃣ DỰNG FLASK APPLICATION
Trước tiên cài Flask:
py -m pip install flask
Tạo file app.py:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def home():
return "Home page"
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000)
Chạy application:
py app.py
Truy cập:
Nếu nhận được Home page, Flask application đã hoạt động.
Ở bước này chưa có vấn đề bảo mật. Điểm đáng chú ý chỉ bắt đầu xuất hiện khi application cho phép client chỉ định file nào cần đọc.
2️⃣ BIẾN HTTP INPUT THÀNH FILESYSTEM OPERATION
Tạo API:
from flask import Flask, request
@app.route('/get_file', methods=['GET'])
def get_file():
filename = request.args['filename']
return f"Content of the file {filename} is...\n\n{cat(filename)}"
Hàm cat():
def cat(filename):
with open(filename) as file:
data = file.read()
return data
Hãy nhìn vào data flow:
HTTP Request
↓
request.args['filename']
↓
filename
↓
open(filename)
↓
Filesystem
↓
HTTP Response
Vấn đề nằm chính ở đây.
Giá trị filename không được tạo bởi server mà đến trực tiếp từ HTTP request của client. Sau đó giá trị này được truyền thẳng vào open().
Nói cách khác, application đang cho client tham gia quyết định server sẽ mở file nào.
3️⃣ KIỂM TRA VỚI FILE HỢP LỆ
Tạo:
welcome.txt
passwords.txt
Ví dụ welcome.txt:
Hello student
Và passwords.txt:
username=cisco
password=cisco
Request bình thường:
Application trả về nội dung của welcome.txt.
Thoạt nhìn API hoạt động hoàn toàn bình thường.
Nhưng hãy đặt một câu hỏi khác:
Nếu client không gửi tên file mà developer mong muốn thì sao?
4️⃣ PATH TRAVERSAL XUẤT HIỆN Ở ĐÂU?
Di chuyển passwords.txt ra ngoài thư mục chứa app.py.
Sau đó thử:
Ký hiệu:
../
đại diện cho thư mục cha.
Vì application không kiểm tra đường dẫn trước khi gọi:
open(filename)
nên filesystem có thể xử lý:
../passwords.txt
như một đường dẫn hợp lệ.
Nếu request thành công và server trả về nội dung passwords.txt, chúng ta đã chứng minh được Path Traversal.
Điểm quan trọng ở đây không phải đơn giản là "../ nguy hiểm".
Vấn đề thực sự là:
Đây là một cách nhìn rất quan trọng khi làm DevNet Security.
Thay vì chỉ nhìn vào một đoạn code, hãy đặt câu hỏi:
Dữ liệu bắt đầu từ đâu → đi qua những function nào → cuối cùng tác động đến tài nguyên nào?
5️⃣ FIX: KIỂM SOÁT INPUT
Một cách tiếp cận trong lab là kiểm tra format của filename trước khi đưa vào open().
Ví dụ:
import re
def sanitize_string(filename):
if re.search(r'^[\w\-\.]+$', filename):
pass
else:
raise ValueError("Can not use special characters")
Sau đó:
def cat(filename):
sanitize_string(filename)
with open(filename) as file:
data = file.read()
return data
Bây giờ thử lại:
Input chứa ký tự không nằm trong pattern cho phép sẽ bị từ chối trước khi đi tới:
open(filename)
Data flow lúc này trở thành:
HTTP Request
↓
filename
↓
Validation
↓
Valid? ─── No ──→ Reject
│
Yes
↓
open()
↓
Filesystem
Chúng ta đã thêm một lớp kiểm soát giữa input của client và filesystem.
6️⃣ NHƯNG REGEX CÓ PHẢI GIẢI PHÁP TỐT NHẤT?
Đây là điểm đáng suy nghĩ sau khi hoàn thành lab.
Việc blacklist hoặc giới hạn ký tự có thể giúp minh họa cách ngăn payload trong bài lab, nhưng trong một ứng dụng thực tế, chỉ dựa vào việc lọc ký tự không phải lúc nào cũng là thiết kế an toàn nhất.
Một hướng tốt hơn là:
Không cho client tự do quyết định filesystem path.
Thay vào đó, application nên:
Ví dụ về tư duy thiết kế:
User Input
↓
Normalize Path
↓
Resolve Absolute Path
↓
Check Allowed Directory
↓
Allowed? ── No → Reject
│
Yes
↓
Read File
Đây mới là tư duy quan trọng khi chuyển từ lab sang application thực tế.
🎯 KẾT LUẬN
Một Flask API vài dòng code có thể trở thành bài học rất rõ về Application Security.
Từ một request:
/get_file?filename=...
dữ liệu có thể đi qua:
HTTP
↓
Flask
↓
request.args
↓
Python variable
↓
open()
↓
Filesystem
Nếu developer không kiểm soát đúng điểm giao giữa untrusted input và sensitive resource, một chức năng tưởng như đơn giản là “đọc file” có thể trở thành điểm yếu bảo mật.
Vì vậy, giá trị lớn nhất của lab này không nằm ở việc nhớ payload ../.
Nó nằm ở một thói quen quan trọng của DevNet:
Đừng chỉ hỏi “API này có chạy không?”
Hãy hỏi:
“Input từ client đang đi đâu, được xử lý như thế nào và cuối cùng có thể tác động đến tài nguyên nào trên hệ thống?”
Đó chính là cách chuyển từ tư duy viết code cho chạy sang tư duy xây dựng application an toàn.
Nhưng một câu hỏi quan trọng thường bị bỏ qua:
Server sẽ làm gì nếu input mà client gửi lên không phải là giá trị mà developer dự kiến?
Trong lab này, chúng ta xây dựng một Flask API có chức năng đọc file từ tham số filename, sau đó theo dõi toàn bộ data flow từ HTTP request → Python application → filesystem để tìm ra lỗ hổng Path Traversal.
Mục tiêu không chỉ là biết chuỗi ../, mà quan trọng hơn là hiểu vì sao một input từ HTTP request có thể tác động trực tiếp đến filesystem của server.
1️⃣ DỰNG FLASK APPLICATION
Trước tiên cài Flask:
py -m pip install flask
Tạo file app.py:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def home():
return "Home page"
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000)
Chạy application:
py app.py
Truy cập:
Nếu nhận được Home page, Flask application đã hoạt động.
Ở bước này chưa có vấn đề bảo mật. Điểm đáng chú ý chỉ bắt đầu xuất hiện khi application cho phép client chỉ định file nào cần đọc.
2️⃣ BIẾN HTTP INPUT THÀNH FILESYSTEM OPERATION
Tạo API:
from flask import Flask, request
@app.route('/get_file', methods=['GET'])
def get_file():
filename = request.args['filename']
return f"Content of the file {filename} is...\n\n{cat(filename)}"
Hàm cat():
def cat(filename):
with open(filename) as file:
data = file.read()
return data
Hãy nhìn vào data flow:
HTTP Request
↓
request.args['filename']
↓
filename
↓
open(filename)
↓
Filesystem
↓
HTTP Response
Vấn đề nằm chính ở đây.
Giá trị filename không được tạo bởi server mà đến trực tiếp từ HTTP request của client. Sau đó giá trị này được truyền thẳng vào open().
Nói cách khác, application đang cho client tham gia quyết định server sẽ mở file nào.
3️⃣ KIỂM TRA VỚI FILE HỢP LỆ
Tạo:
welcome.txt
passwords.txt
Ví dụ welcome.txt:
Hello student
Và passwords.txt:
username=cisco
password=cisco
Request bình thường:
Application trả về nội dung của welcome.txt.
Thoạt nhìn API hoạt động hoàn toàn bình thường.
Nhưng hãy đặt một câu hỏi khác:
Nếu client không gửi tên file mà developer mong muốn thì sao?
4️⃣ PATH TRAVERSAL XUẤT HIỆN Ở ĐÂU?
Di chuyển passwords.txt ra ngoài thư mục chứa app.py.
Sau đó thử:
Ký hiệu:
../
đại diện cho thư mục cha.
Vì application không kiểm tra đường dẫn trước khi gọi:
open(filename)
nên filesystem có thể xử lý:
../passwords.txt
như một đường dẫn hợp lệ.
Nếu request thành công và server trả về nội dung passwords.txt, chúng ta đã chứng minh được Path Traversal.
Điểm quan trọng ở đây không phải đơn giản là "../ nguy hiểm".
Vấn đề thực sự là:
Untrusted input từ client đang được sử dụng trực tiếp để xác định đường dẫn trên filesystem.
Đây là một cách nhìn rất quan trọng khi làm DevNet Security.
Thay vì chỉ nhìn vào một đoạn code, hãy đặt câu hỏi:
Dữ liệu bắt đầu từ đâu → đi qua những function nào → cuối cùng tác động đến tài nguyên nào?
5️⃣ FIX: KIỂM SOÁT INPUT
Một cách tiếp cận trong lab là kiểm tra format của filename trước khi đưa vào open().
Ví dụ:
import re
def sanitize_string(filename):
if re.search(r'^[\w\-\.]+$', filename):
pass
else:
raise ValueError("Can not use special characters")
Sau đó:
def cat(filename):
sanitize_string(filename)
with open(filename) as file:
data = file.read()
return data
Bây giờ thử lại:
Input chứa ký tự không nằm trong pattern cho phép sẽ bị từ chối trước khi đi tới:
open(filename)
Data flow lúc này trở thành:
HTTP Request
↓
filename
↓
Validation
↓
Valid? ─── No ──→ Reject
│
Yes
↓
open()
↓
Filesystem
Chúng ta đã thêm một lớp kiểm soát giữa input của client và filesystem.
6️⃣ NHƯNG REGEX CÓ PHẢI GIẢI PHÁP TỐT NHẤT?
Đây là điểm đáng suy nghĩ sau khi hoàn thành lab.
Việc blacklist hoặc giới hạn ký tự có thể giúp minh họa cách ngăn payload trong bài lab, nhưng trong một ứng dụng thực tế, chỉ dựa vào việc lọc ký tự không phải lúc nào cũng là thiết kế an toàn nhất.
Một hướng tốt hơn là:
Không cho client tự do quyết định filesystem path.
Thay vào đó, application nên:
- Chỉ cho phép truy cập một thư mục được xác định trước.
- Kiểm tra đường dẫn sau khi chuẩn hóa.
- Đảm bảo file cuối cùng vẫn nằm trong thư mục được phép.
- Chỉ cho phép những file cần thiết thay vì mở tùy ý mọi path.
Ví dụ về tư duy thiết kế:
User Input
↓
Normalize Path
↓
Resolve Absolute Path
↓
Check Allowed Directory
↓
Allowed? ── No → Reject
│
Yes
↓
Read File
Đây mới là tư duy quan trọng khi chuyển từ lab sang application thực tế.
🎯 KẾT LUẬN
Một Flask API vài dòng code có thể trở thành bài học rất rõ về Application Security.
Từ một request:
/get_file?filename=...
dữ liệu có thể đi qua:
HTTP
↓
Flask
↓
request.args
↓
Python variable
↓
open()
↓
Filesystem
Nếu developer không kiểm soát đúng điểm giao giữa untrusted input và sensitive resource, một chức năng tưởng như đơn giản là “đọc file” có thể trở thành điểm yếu bảo mật.
Vì vậy, giá trị lớn nhất của lab này không nằm ở việc nhớ payload ../.
Nó nằm ở một thói quen quan trọng của DevNet:
Đừng chỉ hỏi “API này có chạy không?”
Hãy hỏi:
“Input từ client đang đi đâu, được xử lý như thế nào và cuối cùng có thể tác động đến tài nguyên nào trên hệ thống?”
Đó chính là cách chuyển từ tư duy viết code cho chạy sang tư duy xây dựng application an toàn.