งูต้นฉบับกำลังตามริบบิ้นทางลัดจากป้ายเชื่อมไปยังกล่องปลายทาง

Python Path.readlink: อ่านปลายทาง symlink โดยไม่เผลอ resolve ทั้งเส้น

พี่โชว์Engineering

เครื่องมือ audit symlink มักต้องตอบว่า “ลิงก์นี้เขียนปลายทางว่าอะไร” ไม่ใช่ “สุดท้ายไปจบที่ path ไหน” ครับ Path.readlink() คืน target ที่เก็บอยู่ใน symlink โดยรักษาความเป็น relative ส่วน resolve() เดินเส้นทางและทำให้เป็น absolute

WHY: คำตอบสองแบบใช้คนละงาน

ถ้า current ชี้ไป releases/green การอ่านลิงก์ควรได้ path relative นั้นเพื่อแสดง config ตามจริง แต่การเปิดไฟล์ต้องตีความ relative target จาก directory ของ symlink เหมือนป้ายทางลัดเขียนว่า “ห้องข้าง ๆ” เราต้องรู้ก่อนว่าป้ายตั้งอยู่ตรงไหน

from pathlib import Path
from tempfile import TemporaryDirectory
import unittest


class ReadLinkTests(unittest.TestCase):
    def test_relative_target_is_preserved(self):
        with TemporaryDirectory() as temporary:
            root = Path(temporary)
            target = root / "release.txt"
            target.write_text("ready", encoding="utf-8")
            link = root / "current"
            link.symlink_to(target.name)

            self.assertEqual(link.readlink(), Path("release.txt"))
            self.assertEqual(link.resolve(), target.resolve())
            self.assertEqual(link.read_text(encoding="utf-8"), "ready")

    def test_broken_link_still_reveals_its_stored_target(self):
        with TemporaryDirectory() as temporary:
            link = Path(temporary) / "missing"
            link.symlink_to("not-created.txt")
            self.assertEqual(link.readlink(), Path("not-created.txt"))
            self.assertFalse(link.exists())


if __name__ == "__main__":
    unittest.main()

Security boundary

อย่าใช้ resolve() แล้วตรวจ prefix แบบ string เพื่ออนุญาต path ที่ผู้ใช้ควบคุม เพราะ symlink อาจเปลี่ยนระหว่างตรวจและเปิดไฟล์ เกิด TOCTOU race ได้ สำหรับ sandbox จริงควรใช้ API ระดับ descriptor, เปิด relative จาก directory ที่เชื่อถือได้ และกำหนด policy ว่าตาม symlink ได้หรือไม่

readlink() ไม่รับประกันว่า target มีอยู่ และไม่ตรวจว่าปลายทางยังอยู่ใน root จึงเหมาะกับการ inspect มากกว่าการ authorize หาก path ไม่ใช่ symlink จะได้ OSError; จับเฉพาะ error ที่คาดและรายงาน path ให้ชัด

Recap

  • readlink() อ่าน target ที่เก็บใน symlink
  • target แบบ relative ต้องตีความจาก parent ของลิงก์
  • resolve() ตอบ path ปลายทาง ไม่ใช่ config ดิบ
  • อย่าใช้ resolve-check-open เป็น security sandbox