Python shutil.which: หา executable ก่อนเรียก subprocess อย่างมีขอบเขต
โปรแกรมที่ต้องเรียกเครื่องมือภายนอกควรแยกสองคำถาม: คำสั่งนี้อนุญาตหรือไม่ และ executable อยู่ที่ไหน shutil.which ช่วยตอบข้อหลังโดยค้นตาม PATH หรือ path ที่เรากำหนด แต่ไม่ใช่เครื่องตรวจความน่าเชื่อถือของไฟล์ครับ
WHY: หาเจอไม่เท่ากับอนุญาตให้รัน
เหมือนพนักงานบอกว่ากุญแจอยู่ลิ้นชักไหน แต่หัวหน้าต้องตัดสินใจอีกชั้นว่าประตูนั้นเปิดได้หรือไม่ เราจึงใช้ allowlist กับชื่อคำสั่งก่อน แล้วค้นเฉพาะ directory ที่ระบบตั้งใจ ไม่รับชื่อหรือ PATH ทั้งชุดจากผู้ใช้โดยตรง
import os
import shutil
import tempfile
import unittest
from pathlib import Path
ALLOWED = {"report-tool"}
def resolve_tool(name: str, search_path: str) -> str:
if name not in ALLOWED:
raise ValueError("command is not allowed")
found = shutil.which(name, path=search_path)
if found is None:
raise FileNotFoundError(name)
return found
class WhichTests(unittest.TestCase):
def test_finds_only_allowed_executable(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
tool = Path(tmp, "report-tool")
tool.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
tool.chmod(0o700)
self.assertEqual(resolve_tool("report-tool", tmp), str(tool))
with self.assertRaises(ValueError):
resolve_tool("other-tool", tmp)
def test_missing_tool(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
with self.assertRaises(FileNotFoundError):
resolve_tool("report-tool", tmp)
if __name__ == "__main__":
unittest.main()
HOW: ส่ง argv โดยไม่ผ่าน shell
เมื่อได้ absolute path แล้ว ให้เรียก subprocess.run([path, "--flag", value], shell=False, check=True, timeout=...) และกำหนด working directory กับ environment เท่าที่จำเป็น อย่าต่อ command string จาก input เพราะ which ไม่ป้องกัน shell injection และอย่าเชื่อไฟล์เพียงเพราะอยู่ก่อนใน PATH
ในระบบ production ควร pin directory ที่ admin ควบคุม ตรวจ owner/permission ตาม threat model และจัดการกรณีไฟล์ถูกสลับหลังตรวจแต่ก่อนรัน หากต้องการความมั่นคงสูงกว่า PATH discovery ให้กำหนด absolute executable ใน config ที่ deploy มาพร้อมระบบ
Recap
- ตรวจ allowlist ก่อนค้น executable
- ส่ง
path=ที่ควบคุมเองเมื่อขอบเขตสำคัญ - เรียก subprocess ด้วย argv และ
shell=False - ตั้ง timeout, cwd และ environment แบบขั้นต่ำ
- อย่าถือว่า “หาเจอ” แปลว่าไฟล์เชื่อถือได้