งูสีเขียวต้นฉบับกำลังตรวจเส้นทางไม้สองเส้น โดยหยุดเส้นที่ชื่อคล้ายกันแต่เลี้ยวออกนอกรั้ว

Python os.path.commonpath: ตรวจขอบเขต path โดยไม่หลง prefix ที่หน้าตาเหมือนกัน

พี่โชว์Engineering

การตรวจว่า 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