Python urljoin: ต่อ URL โดยไม่เปิดทางให้ input เปลี่ยน host
urllib.parse.urljoin ทำตามกฎ URL มาตรฐาน ดังนั้น argument ที่เป็น URL เต็มหรือเริ่มด้วย // สามารถเปลี่ยน host จาก base ได้ พฤติกรรมนี้ถูกต้องสำหรับ browser แต่เสี่ยงเมื่อ path มาจากผู้ใช้และโปรแกรมนำผลไป redirect หรือ fetch ต่อครับ
WHY: ป้ายทางแยกอาจพาออกจากรั้ว
เราตั้งต้นที่โกดังของบริษัทและคิดว่าต่อเพียงเลขชั้น แต่ป้ายที่ผู้ใช้ส่งมาอาจเขียนเส้นทางไปอีกจังหวัด การ join จึงต้องตามด้วย policy check ว่า scheme และ host ยังอยู่ในขอบที่ไว้ใจ
from urllib.parse import urljoin, urlsplit
import unittest
def join_same_origin(base: str, candidate: str) -> str:
base_parts = urlsplit(base)
joined = urljoin(base, candidate)
joined_parts = urlsplit(joined)
if joined_parts.scheme.lower() != base_parts.scheme.lower():
raise ValueError("scheme changed")
if joined_parts.netloc.lower() != base_parts.netloc.lower():
raise ValueError("origin changed")
return joined
class SafeJoinTests(unittest.TestCase):
def test_relative_path_stays_on_origin(self) -> None:
self.assertEqual(
join_same_origin("https://example.test/app/", "reports/7"),
"https://example.test/app/reports/7",
)
def test_absolute_and_scheme_relative_urls_are_rejected(self) -> None:
for value in ("https://evil.test/x", "//evil.test/x"):
with self.subTest(value=value), self.assertRaises(ValueError):
join_same_origin("https://example.test/app/", value)
if __name__ == "__main__":
unittest.main()
HOW: same-origin ยังไม่ใช่ SSRF defense ทั้งชุด
ตัวอย่างเหมาะกับ base ที่กำหนดโดยระบบและต้องการกัน host เปลี่ยน แต่ production ยังต้อง normalize port, IDNA hostname, credential ใน URL, redirect chain และ DNS resolution ตาม threat model หากนำ URL ไปเรียก network ต้องบล็อก private/loopback/link-local ranges หลัง resolve และตรวจทุก redirect ไม่ใช่ตรวจ string ครั้งแรกแล้วจบ
ถ้าโจทย์รับเฉพาะ path ภายในเว็บ ทางที่ง่ายกว่าคือปฏิเสธ input ที่มี scheme หรือ netloc ตั้งแต่ต้น แล้วประกอบ route ด้วย API ของ framework อย่า decode ซ้ำหลายรอบจน %2f หรือ backslash เปลี่ยนความหมายต่างกันระหว่าง proxy กับแอป
Recap
- ถือ argument ขวาของ
urljoinเป็น URL ไม่ใช่ path ธรรมดา - ตรวจ scheme และ host หลัง join
- สำหรับ network fetch ให้ตรวจ DNS, IP ranges และ redirect ทุก hop
- ลดรูป input เป็น internal path เมื่อทำได้
- ทดสอบ absolute URL,
//host, port, encoded separator และ redirect