Psalm 7 มีอะไรใหม่ เปิด taint analysis ให้ทุกครั้งที่รัน และใช้ capabilities บอก side effect

Psalm 7 ออกเมื่อ 2 พฤศจิกายน 2026 รัน taint analysis ไล่ข้อมูลจากผู้ใช้ไปถึงจุดอันตรายพร้อมกับตรวจชนิดข้อมูลในรอบเดียว เปลี่ยนระบบ pure มาใช้ capabilities ที่บอกได้ว่าฟังก์ชันมี side effect แบบไหน หา dead code จากกราฟการเรียกจริง และต้องใช้ PHP 8.2 ถึง 8.6 แบบ 64-bit

เผยแพร่
เวลาอ่าน
4 นาที
สรุปจาก
Eric L. Barnes · phpnews.net
ในโพสต์นี้ 6 หัวข้อ
  1. Psalm กับ PHPStan ทำงานคล้ายกัน แต่ Psalm ไล่เส้นทางของข้อมูลได้
  2. taint analysis รันพร้อมการตรวจปกติทุกครั้ง
  3. capabilities บอกว่าฟังก์ชันมี side effect แบบไหน
  4. dead code หาจากกราฟการเรียกจริง
  5. ของอื่นในรุ่นนี้
  6. อัปเกรดจาก Psalm 6

Psalm กับ PHPStan ทำงานคล้ายกัน แต่ Psalm ไล่เส้นทางของข้อมูลได้

ทั้งสองตัวเป็น static analysis คืออ่านโค้ดทั้งโปรเจกต์เพื่อหาบั๊กโดยไม่ต้องรัน ในโปรเจกต์ Laravel เรามักใช้ PHPStan กับ Larastan เป็นหลัก (โพสต์นี้แยกให้เห็นว่าเครื่องมือแต่ละแบบทำหน้าที่อะไร) จุดที่ Psalm เด่นกว่ามานานคือ taint analysis

แนวคิดคือมี source กับ sink source คือที่ที่ข้อมูลจากคนนอกเข้ามา เช่น $_GET, body ของ request หรือไฟล์ที่อัปโหลด sink คือที่ที่ข้อมูลไปแล้วอันตราย เช่น query SQL, shell_exec() หรือ HTML ที่ไม่ escape ถ้า Psalm เห็นทางที่ข้อมูลจาก source ไปถึง sink โดยไม่ผ่านการกรอง ก็รายงานเป็น TaintedSql, TaintedShell, TaintedHtml ตามชนิดของ sink

taint analysis รันพร้อมการตรวจปกติทุกครั้ง

ใน Psalm 6 taint analysis เป็นอีกโหมดที่ต้องสั่งแยก รุ่น 7 รวมการตรวจชนิดข้อมูล การตรวจความปลอดภัย และการหา dead code ไว้ในรอบเดียว (PR #11384) taint analysis จึงเปิดอยู่เสมอ ที่ทำได้เพราะเปลี่ยนมาเก็บ taint เป็น bitmask และสรุปผลของแต่ละฟังก์ชันให้สั้นลงก่อนเชื่อมเป็นกราฟ

ข้อมูลไหลผ่านได้หลายทางกว่าเดิม ทั้ง closure, generator, trait และ parameter ที่รับแบบ reference

$run = function (string $command): void {
    shell_exec($command);
};

$run((string) $_GET['cmd']); // TaintedShell ผ่าน closure

ของใหม่อื่นในส่วนนี้

  • ชนิดของ issue ใหม่ เช่น TaintedNosql สำหรับ MongoDB, TaintedLlmPrompt สำหรับ prompt injection เมื่อเอาข้อความของผู้ใช้ใส่ prompt ของ AI ตรง ๆ และ TaintedUrlComponent กับ TaintedUrlPath สำหรับ URL ที่ประกอบจากข้อมูลของผู้ใช้
  • source ใหม่ เช่น php://input, ค่าใน $_SERVER และ $_FILES ที่ client ส่งมาเอง, $argv และข้อความของ exception
  • stub ของไลบรารีที่ใช้บ่อย เช่น Guzzle, PSR-7, Monolog, PhpSpreadsheet บอก Psalm ว่าเมธอดไหนเป็น source หรือ sink และโหลดเฉพาะเมื่อโปรเจกต์ใช้ไลบรารีนั้น
  • รายงานแบบ SARIF ให้ issue ด้านความปลอดภัยมีระดับ SECURITY ใช้ส่งขึ้นหน้า code scanning ของ GitHub ได้

ถ้ายังไม่อยากเปิด ใส่ runTaintAnalysis="false" ใน psalm.xml ได้ตามคู่มืออัปเกรด

capabilities บอกว่าฟังก์ชันมี side effect แบบไหน

เดิม Psalm แบ่งฟังก์ชันแค่ว่า pure (คืนค่าจาก argument อย่างเดียว ไม่แตะอะไรข้างนอก) หรือไม่ pure รุ่น 7 (PR #11984) ยืมแนวคิด contexts and capabilities ของ Hack มาให้ฟังก์ชันประกาศว่ามี side effect อะไรได้บ้าง ได้แก่ read-props, write-this-props, write-props, read-globals, write-globals, write-refs, io และ time โดยมี pure กับ impure อยู่สองปลาย

/** @psalm-capabilities time */
function today(): string
{
    return date('Y-m-d'); // ผ่าน เพราะประกาศว่าอ่านเวลาได้
}

/** @psalm-pure */
function todayWrong(): string
{
    return date('Y-m-d'); // ImpureFunctionCall เพราะ date() ต้องใช้ time
}

Psalm จัดกลุ่มฟังก์ชันของ PHP เองไว้แล้วด้วย อ่านเขียนไฟล์และ random_int() ต้องมี io, time() กับ date() ที่ไม่ส่ง timestamp ต้องมี time ส่วน mt_rand() กับ ini_set() ต้องมี write-globals โค้ดที่เคยติด @psalm-pure แล้วเรียกพวกนี้จะเริ่มโดนรายงาน

@psalm-pure กับ @psalm-immutable ยังเป็นคำที่แนะนำให้ใช้ ส่วน @psalm-mutation-free และ @psalm-external-mutation-free ยังใช้ได้แต่ไม่อยู่ในเอกสารแล้ว เทียบได้กับ read-props และ read-props|write-this-props|write-refs ตามลำดับ นอกจากนี้มี purity template ให้ฟังก์ชันอย่าง array_map() เป็น pure เมื่อ callback ที่ส่งเข้ามาเป็น pure

dead code หาจากกราฟการเรียกจริง

PR #11939 เปลี่ยนวิธีนับว่าโค้ดไหนยังใช้อยู่ ตอนนี้โค้ดต้องเรียกถึงได้จาก entry point ถึงจะนับว่าใช้ ถ้ามีสองคลาสที่เรียกกันเองแต่ไม่มีใครเรียกถึง Psalm จะรายงาน UnusedClass ทั้งคู่ exception ที่สร้างขึ้นแต่ไม่เคย throw ก็โดนรายงานด้วย

กับโปรเจกต์ Laravel ต้องดูผลส่วนนี้ให้ดี เพราะ Laravel เรียก controller, listener และ job ผ่าน route, event และ container ซึ่ง Psalm อาจมองไม่เห็นถ้า plugin ไม่ได้บอกไว้

ของอื่นในรุ่นนี้

  • plugin มากับตัว plugin ของ PHPUnit, Symfony, Laravel, Mockery และ webmozart/assert เปิดให้เองเมื่อ composer.json ใช้ไลบรารีนั้น (loadLibraryPlugins)
  • template มีค่าเริ่มต้นได้ เช่น @template TError of Throwable = RuntimeException
  • รองรับ PHP 8.5 และ 8.6 อ่านชนิดข้อมูลของแต่ละขั้นใน pipe |> ได้
  • --init-ci สร้าง workflow ของ GitHub Actions ให้ และ Docker image ย้ายไปอยู่ที่ ghcr.io/psalm/psalm
  • ออก release ทุกวัน ไม่ต้องตาม dev-master อีก

อัปเกรดจาก Psalm 6

composer require --dev vimeo/psalm:^7.0 -W
  • ต้องใช้ PHP 8.2 ถึง 8.6 แบบ 64-bit และ psalm/plugin-phpunit ^0.21
  • capability แต่ละตัวหมายถึงตัวเองอย่างเดียวแล้ว write-props ไม่รวม read-props เหมือนเดิม ต้องเขียนเต็มเป็น read-props|write-props
  • คำแนะนำให้ใส่ annotation (MissingPureAnnotation และพวก) ปิดไว้เป็นค่าเริ่มต้น ถ้า baseline เดิมมีรายการพวกนี้ ให้รัน --update-baseline ลบออก หรือเปิด suggestPurityAnnotations="true"
  • คนเขียน plugin ให้ require psalm/psalm-plugin-api แทน เพราะเลขเวอร์ชันของ Psalm ไม่ได้บอกการเปลี่ยนของ plugin API แล้ว

รายการเปลี่ยนทั้งหมดอยู่ใน release notes และบทความต้นฉบับครับ

อ่านต่อ

ล่าสุด

ดูทั้งหมด →