Tại sao việc mở một tệp tin lại phức tạp đến thế? Bài học về bảo mật hệ thống
Bài viết này phân tích những rủi ro bảo mật tiềm ẩn đằng sau hành động tưởng chừng đơn giản là mở một tệp tin, đặc biệt là các lỗ hổng liên quan đến đường dẫn, symlink và điều kiện tranh chấp TOCTOU. Tác giả đề xuất sử dụng file descriptor thay vì chuỗi đường dẫn để đảm bảo an toàn, đồng thời chia sẻ kinh nghiệm thực tế từ việc khắc phục lỗ hổng sandbox escape trong dự án Flatpak.
Tại sao việc mở một tệp tin lại phức tạp đến thế? Bài học về bảo mật hệ thống
Đối với một lập trình viên ứng dụng thông thường, việc mở một tệp tin thường rất đơn giản: chỉ cần gọi một hàm trong thư viện chuẩn là xong. Tuy nhiên, đối với những nhà phát triển làm việc tại các ranh giới bảo mật (security boundary), câu trả lời lại hoàn toàn khác: "Đừng tin tưởng bất cứ điều gì".
Bài viết này sẽ đi sâu vào lý do tại sao một thao tác cơ bản như mở tệp tin lại trở thành một cơn ác mộng bảo mật nếu không được xử lý đúng cách, đặc biệt là trong các môi trường sandbox như Flatpak.
Mở tệp tin theo cách khó khăn
Hãy tưởng tượng một kịch bản trong đó có hai quá trình (process): một quá trình có đặc quyền cao và một quá trình có đặc quyền thấp hơn, cùng chia sẻ một cây hệ thống tệp. Quá trình có đặc quyền cao cần thao tác trên tệp thay mặt cho quá trình kia, nhưng muốn giới hạn quyền truy cập chỉ trong một thư mục cụ thể để ngăn chặn việc đánh cắp dữ liệu nhạy cảm (ví dụ: SSH key).
Vấn đề đầu tiên nảy sinh từ đường dẫn tương đối. Nếu đường dẫn chứa ../, kẻ tấn công có thể truy cập ra ngoài thư mục được phép. Dù việc chuẩn hóa (normalize) đường dẫn có thể khắc phục điều này, nhưng thử thách thực sự nằm ở các symlink (liên kết mềm).
Nếu quá trình có đặc quyền thấp có thể tạo symlink trong đường dẫn, nó có thể đánh lừa quá trình có đặc quyền cao truy cập vào các tệp bị cấm. Ngay cả khi bạn giải quyết (resolve) tất cả các symlink, vẫn còn một vấn đề lớn hơn: TOCTOU (Time-of-Check to Time-of-Use).
Đây là một cuộc đua điều kiện (race condition). Trong khoảng thời gian giữa lúc bạn kiểm tra/xác thực đường dẫn và lúc bạn thực sự mở tệp, kẻ tấn công có thể thay đổi cấu trúc hệ thống tệp (ví dụ: thay thế một thư mục bằng symlink). Khi đó, quá trình có đặc quyền cao sẽ vô tình mở một tệp mà nó nghĩ là an toàn, nhưng thực tế lại là tệp do kẻ tấn công điều khiển.
Đường dẫn không phải là tham chiếu tệp
Vấn đề cốt lõi ở đây là một chuỗi đường dẫn (như /home/user/file) chỉ mô tả một vị trí trong không gian tên hệ thống tệp tại một thời điểm nhất định, nó không phải là một tham chiếu cố định đến một tệp. Đến khi bạn đọc xong đường dẫn đó, đối tượng mà nó chỉ đến có thể đã thay đổi.
Nguyên thủy an toàn để giải quyết vấn đề này là file descriptor (mô tả tệp). Khi bạn có một file descriptor trỏ đến một inode, nhân hệ điều hành (kernel) sẽ "ghim" (pin) inode đó lại. Thư mục có thể bị xóa, đổi tên hoặc thay thế bằng symlink, nhưng file descriptor vẫn trỏ đúng đến đối tượng gốc.
Bài học quan trọng là: Không bao giờ nên gọi một quá trình có đặc quyền bằng cách truyền vào một đường dẫn chuỗi. Thay vào đó, hãy truyền file descriptor. Điều này không chỉ an toàn mà còn đóng vai trò là bằng chứng cho thấy quá trình gọi thực sự có quyền truy cập vào tài nguyên đó.
libglnx và việc truy cập đường dẫn an toàn
Trong thực tế, đôi khi việc sử dụng đường dẫn là không thể tránh khỏi. Đây là lúc các thư viện như libglnx (dùng cho các dự án C của GNOME) phát huy tác dụng. Thư viện này cung cấp các hàm API hoạt động dựa trên file descriptor thay vì đường dẫn tuyệt đối.
Một tính năng nổi bật gần đây là glnx_chaseat, được lấy cảm hứng từ chase() của systemd. Hàm này cung cấp khả năng truy cập đường dẫn an toàn bằng cách sử dụng các syscall như openat2 và open_tree, cho phép kiểm soát chặt chẽ việc xử lý symlink và đảm bảo đường dẫn được giải quyết nằm trong phạm vi mong muốn.
Thư viện chuẩn và vấn đề bảo mật
Một điểm đáng lo ngại là các API tiêu chuẩn như POSIX, GLib hay thậm chí là Rust thường dựa hoàn toàn vào đường dẫn. Điều này tạo ra các lỗ hổng bảo mật rất dễ xảy ra. Bạn có thể kiểm toán mã nguồn của mình rất kỹ, nhưng nếu gọi một thư viện bên thứ ba nào đó bên trong lại sử dụng open(path), tính bảo mật mà bạn xây dựng sẽ bị phá vỡ ngay lập tức. Tính bảo mật không thể "kết hợp" (compose) tốt thông qua các API dựa trên đường dẫn.
Bài học từ Flatpak và CVE-2026-34078
Vấn đề này không chỉ là lý thuyết. Gần đây, dự án Flatpak (một hệ thống sandbox cho ứng dụng Linux) đã phải đối mặt với một lỗ hổng nghiêm trọng (CVE-2026-34078) cho phép thoát khỏi sandbox hoàn toàn.
Nguồn gốc của vấn đề nằm ở việc flatpak run được thiết kế như một công cụ dòng lệnh cho người dùng đáng tin cậy, chấp nhận các chuỗi đường dẫn làm đối số. Tuy nhiên, Flatpak Portal (dịch vụ D-Bus cho các ứng dụng sandbox) lại gọi công cụ này trực tiếp từ các ứng dụng không đáng tin cậy.
Khi kết nối này được thiết lập, mọi giả định về độ tin cậy của đầu vào đều trở thành lỗ hổng tiềm năng. Giải pháp không chỉ đơn giản là thay đổi một hàm, mà là phải kiểm toán toàn bộ chuỗi gọi và thay thế mọi chuỗi đường dẫn bằng file descriptor.
Điều này đòi hỏi sự thay đổi lớn ở nhiều thành phần, từ portal, flatpak-run, đến việc xây dựng đối số cho bwrap. May mắn thay, sau những nỗ lực khắc phục, Flatpak hiện đã an toàn hơn và hệ sinh thái đã được trang bị tốt hơn để xử lý lớp vấn đề này.
Việc hiểu rõ sự khác biệt giữa đường dẫn và file descriptor, cũng như các rủi ro tiềm ẩn trong thao tác I/O tệp, là kiến thức thiết yếu cho bất kỳ lập trình viên hệ thống nào muốn xây dựng phần mềm an toàn.

