Laravel 13.40 มีอะไรใหม่ แจ้งเตือน job ที่ช้า และแก้ช่องโหว่ใน Concurrency::run()

Laravel 13.40 เพิ่ม callback ที่ทำงานเมื่อ queue job หรืองานใน scheduler ใช้เวลาเกินที่ตั้งไว้ ให้เช็คด้วย commit ID ว่า read replica ได้ข้อมูลที่เพิ่งเขียนแล้วหรือยัง แทรก middleware ก่อนหรือหลังตัวที่ Laravel ใส่ไว้ได้ และแก้ช่องโหว่ RCE ใน Concurrency::run() แบบ process driver

เผยแพร่
เวลาอ่าน
4 นาที
สรุปจาก
Paul Redmond · laravel-news.com
ในโพสต์นี้ 7 หัวข้อ
  1. ช่องโหว่ใน Concurrency::run() แบบ process driver
  2. แจ้งเตือนเมื่อ queue job ใช้เวลานานเกินไป
  3. เช็คว่า read replica ได้ข้อมูลที่เพิ่งเขียนแล้วหรือยัง
  4. แทรก middleware ก่อนหรือหลังตัวที่ Laravel ใส่ไว้
  5. รับค่าจาก request แบบ strict
  6. transaction ฟื้นตัวเองได้หลัง connection หลุด
  7. ของที่แก้เพิ่มใน 13.40

ช่องโหว่ใน Concurrency::run() แบบ process driver

Concurrency::run() (เปิดตัวตอน Laracon US 2024) แตกงานหลายชิ้นไปรันพร้อมกัน driver เริ่มต้นคือ process ซึ่งเปิด child process ด้วย php artisan แยกต่อหนึ่ง task แล้วให้ child พิมพ์ผลออกมา ตัวแม่อ่านข้อความนั้นแล้ว unserialize() กลับเป็นค่าใน PHP

ปัญหาคือตัวแม่แยกไม่ออกว่าข้อความไหนเป็นผลจริง ถ้า task พิมพ์ข้อความที่ผู้ใช้ควบคุมได้ออกมา ผู้ใช้ก็ปลอมข้อความให้หน้าตาเหมือนผลลัพธ์ได้ แล้วตัวแม่จะ unserialize() ของที่ผู้ใช้ส่งมา ซึ่งเป็นทางคลาสสิกที่ทำให้รันโค้ดบนเซิร์ฟเวอร์ได้ (remote code execution)

@Arr0way รายงานเรื่องนี้ และ PR #62297 แก้ด้วยการให้ตัวแม่สุ่ม key ใหม่ให้แต่ละ task child ต้องห่อผลลัพธ์แล้วเซ็นด้วย key นั้น ตัวแม่จะข้ามข้อความที่เซ็นไม่ถูก ผลพลอยได้คือ error Trying to access array offset on null ที่เคยเกิดเมื่อ task พิมพ์อะไรออกมาก็หายไปด้วย

แอปที่ไม่ได้เรียก Concurrency::run() หรือตั้ง driver เป็น sync หรือ fork ไม่เข้าเงื่อนไขนี้ แต่ก็ควรอัปเดตตามรอบปกติอยู่ดี ถ้ายังอยู่บน Laravel 12 รุ่นที่แก้เรื่องนี้คือ 12.71.0

แจ้งเตือนเมื่อ queue job ใช้เวลานานเกินไป

Laravel มีตัวจับเวลาแบบนี้มานานแล้วสำหรับ query (DB::whenQueryingForLongerThan()) request และคำสั่ง artisan แต่ยังไม่มีของ queue job Jack Bayliss เพิ่ม Queue::whenJobRunsLongerThan() ให้ (PR #62141) โดยฟัง event JobAttempted ที่ได้ค่าเวลาใน 13.39 จึงทำงานทั้งกับ job ที่สำเร็จ ล้มเหลว หรือโดน release กลับเข้า queue

use Carbon\CarbonInterval;
use Illuminate\Support\Facades\Queue;

Queue::whenJobRunsLongerThan(CarbonInterval::seconds(90), function ($job, $duration) {
    Log::warning("{$job->resolveName()} took {$duration}ms");
});

ส่วนงานใน scheduler ใช้ Schedule::whenTaskRunsLongerThan() (PR #62233) ซึ่งทำงานหลังงานจบหรือล้มเหลว

use Carbon\CarbonInterval;
use Illuminate\Console\Scheduling\Event;
use Illuminate\Support\Facades\Schedule;

Schedule::whenTaskRunsLongerThan(CarbonInterval::minutes(45), function (Event $task, float $runtime) {
    Log::warning("Scheduled task [{$task->getSummaryForDisplay()}] took {$runtime}s");
});

ตัวอย่างที่ได้ใช้จริงคือ job ที่เรียก API ภายนอก อย่างส่งใบกำกับภาษีหรือเช็คสถานะการโอน ปกติเสร็จในไม่กี่วินาที ถ้าวันไหนเริ่มใช้เวลาเป็นนาที เรามักรู้ตัวเมื่อ job ชน timeout ไปแล้ว ตั้ง callback ไว้ส่ง LINE หรือ Telegram ตั้งแต่เกินเกณฑ์จะเห็นปัญหาก่อนหนึ่งจังหวะ

เช็คว่า read replica ได้ข้อมูลที่เพิ่งเขียนแล้วหรือยัง

13.38 เพิ่ม PreventStaleDatabaseReads ที่ให้ผู้ใช้ที่เพิ่งบันทึกข้อมูลอ่านจากฐานข้อมูลหลักต่ออีกสองวินาที เป็นการเดาว่า replica น่าจะตามทันในสองวินาที รุ่นนี้ Tobias Petry เพิ่มทางที่ไม่ต้องเดา (PR #62198) คือขอ commit ID จากฐานข้อมูลหลักหลังเขียน แล้วถาม replica ว่ามี commit นั้นหรือยัง

use Illuminate\Support\Facades\DB;

// หลังเขียนลงฐานข้อมูลหลัก
$order = $user->orders()->create($attributes);
$commitId = DB::getLastCommitId();

// ตอนอ่านจาก replica
DB::hasCommitId($commitId);          // เช็คตอนนี้เลย
DB::hasCommitId($commitId, wait: 2); // รอได้ไม่เกินสองวินาที

ถ้าเขียนหลายครั้ง mergeCommitIds() รวมหลาย ID เป็นตัวเดียวแล้วรอทีเดียวได้ ใช้ได้กับ

ฐานข้อมูล เงื่อนไข
MySQL ต้องเปิด GTID
MariaDB ต้องเปิด binary log
PostgreSQL เช็คได้ แต่การ wait ต้องใช้ PostgreSQL 19

driver อื่นจะโยน RuntimeException ถ้ายังไม่ได้ตั้ง replica เลย เรื่องนี้ยังไม่ต้องสนใจ ส่วนใครที่ใช้ PostgreSQL replication อยู่ ลองอ่านความต่างของ physical กับ logical replication ประกอบได้ครับ

แทรก middleware ก่อนหรือหลังตัวที่ Laravel ใส่ไว้

ตั้งแต่ Laravel 11 เราตั้ง middleware ใน bootstrap/app.php ถ้าอยากให้ middleware ของเรารันต่อจากตัวมาตรฐานตัวหนึ่ง เดิมต้องประกาศลำดับทั้งชุดเอง Jamie York เพิ่ม insertBefore() กับ insertAfter() (PR #62261)

->withMiddleware(function (Middleware $middleware) {
    $middleware->insertAfter(TrustProxies::class, StripCrossOriginReferer::class);
    $middleware->insertBefore(TrimStrings::class, SanitizeStrings::class);
})

ถ้าเป็น group อย่าง web ใช้ insertBeforeInGroup() กับ insertAfterInGroup() และ replace() รับ array ได้แล้วด้วย

รับค่าจาก request แบบ strict

$request->integer(), float(), boolean(), string() และ str() รับ strict: true ได้แล้ว (PR #62254) โหมดนี้ถ้าค่าที่ส่งมาแปลงไม่ได้ จะคืนค่า default แทนการเดา

// ?count=abc123
$request->integer('count', 5);               // 0
$request->integer('count', 5, strict: true); // 5

เดิม ?name[]=chris ที่ส่ง array มาแทน string จะพังด้วย Array to string conversion โหมด strict คืน default แทน ส่วน boolean() แบบ strict รับแค่ 1, true, on, yes, 0, false, off และ no

transaction ฟื้นตัวเองได้หลัง connection หลุด

Volodya Kurshudyan แก้สองเรื่องที่เจอแล้วงงมาก

  • connection หลุดกลาง transaction (PR #62211) เดิม PDO ตัวเก่ายังจำว่ามี transaction เปิดอยู่ transaction ถัด ๆ ไปจึงพังด้วย There is already an active transaction ทุกตัว จนกว่าจะรีสตาร์ต queue worker หรือ Octane ตอนนี้ Laravel ทิ้ง PDO ตัวนั้นแล้วต่อใหม่ใน query ถัดไป
  • lock wait timeout หรือ deadlock ใน transaction ที่ซ้อนกัน (PR #62248) Laravel จะ rollback กลับไปที่ savepoint ของ transaction ชั้นในนั้น เดิมถ้า transaction ชั้นนอก catch exception ไว้ ก็จะ commit ข้อมูลที่ชั้นในเขียนไปแล้วไปด้วย

ของที่แก้เพิ่มใน 13.40

  • queue:work แสดง RELEASED แทน DONE สำหรับ job ที่โดนส่งกลับเข้า queue เช่นจาก middleware WithoutOverlapping หรือ RateLimited (PR #62180) ดู log แล้วไม่เข้าใจผิดว่างานเสร็จ
  • scheduler ยิง event ScheduledTaskSkipped เมื่องาน onOneServer() รันไปแล้วบนเครื่องอื่น (PR #62256)
  • ล้าง cache แบบมี tag บน Redis ไม่บล็อก Redis ตอนล้างหลาย tag พร้อมกัน (PR #62215)
  • createOrFirst() บน BelongsToMany หาแถวจาก write connection ไม่หลงไปอ่าน replica ที่ยังไม่มีแถวใหม่ (PR #62223)
  • Laravel บันทึก batch job ที่ model หายไปแล้วว่าล้มเหลว ไม่หายไปเงียบ ๆ (PR #62280)

รายการ PR ทั้งหมด 41 ตัวอยู่ในโพสต์ต้นฉบับของ Laravel News ครับ

อ่านต่อ

ถูกอ้างถึงใน

ล่าสุด

ดูทั้งหมด →