Python os.path.commonpath: ตรวจขอบเขต path โดยไม่หลง prefix ที่หน้าตาเหมือนกัน
การตรวจว่า path อยู่ใต้ workspace ด้วย str(candidate).startswith(str(root)) พลาดง่าย: /work/app-evil ขึ้นต้นเหมือน /work/app ครับ os.path.commonpath เปรียบเทียบเป็นส่วนของ path จริง ไม่ใช่ตัวอักษรดิบ
from pathlib import Path
import os.path
import tempfile
import unittest
def is_within(root: Path, candidate: Path) -> bool:
resolved_root = root.resolve()
resolved_candidate = candidate.resolve()
return os.path.commonpath([resolved_root, resolved_candidate]) == str(resolved_root)
class CommonPathTests(unittest.TestCase):
def test_similar_prefix_is_outside(self):
with tempfile.TemporaryDirectory() as directory:
parent = Path(directory)
root = parent / "app"
outside = parent / "app-evil" / "file.txt"
root.mkdir()
outside.parent.mkdir()
outside.write_text("x")
self.assertFalse(is_within(root, outside))
self.assertTrue(is_within(root, root / "inside.txt"))
if __name__ == "__main__":
unittest.main()
commonprefix() คืน substring จึงอาจคืน path ที่ไม่สมบูรณ์ ส่วน commonpath() ยก ValueError เมื่อผสม absolute/relative หรือคนละ drive บน Windows ต้องจัดการเป็นผลปฏิเสธ ไม่ใช่ปล่อยผ่าน
การ resolve() ช่วยตาม symlink ที่มีอยู่ แต่ยังมี race ระหว่างตรวจและเปิดไฟล์ในระบบที่ผู้โจมตีแก้ filesystem ได้ งานความปลอดภัยสูงควรใช้ API แบบ directory handle/open-at หรือ sandbox ของระบบปฏิบัติการร่วมด้วย ไม่ใช้ check นี้เป็นเกราะชั้นเดียว
Recap
- ห้ามใช้ string prefix เป็น path boundary
- resolve root/candidate และ fail closed เมื่อเทียบไม่ได้
- ทดสอบชื่อคล้าย,
.., symlink และคนละ drive - ใช้ OS-level containment เมื่อมี attacker-controlled filesystem