EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Web / APP_NAME ภาษาไทยทำให้ cookie เหลือ -session: pin ค่าก่อนอัป Laravel 13
Web

APP_NAME ภาษาไทยทำให้ cookie เหลือ -session: pin ค่าก่อนอัป Laravel 13

โดย Ton-Or ·7 พ.ย. 2026·อ่าน 11 นาที
APP_NAME ภาษาไทยทำให้ cookie เหลือ -session: pin ค่าก่อนอัป Laravel 13

ชื่อ cookie ที่ผู้ใช้ถืออยู่ถูกกำหนดจากนิพจน์ default ที่ไม่มีใครเคยเขียนลง .env ถ้าจะอัปเกรดข้ามเมเจอร์ ต้องจดค่าจริงไว้แล้ว pin ให้แน่นก่อนแตะ composer

สรุปใน 1 นาที

อาการ/ปัญหา
APP_NAME ภาษาไทยทำให้ชื่อ session cookie เป็น -session อัปเมเจอร์แล้วผู้ใช้เสี่ยงหลุดล็อกอิน
ใครควรอ่าน
คนดูแลแอป Laravel หลายตัวที่ตั้ง APP_NAME เป็นภาษาไทย
ทางแก้แบบสั้น
อ่านค่า cookie, cache prefix และ redis prefix จริงจากแอปที่รันอยู่ แล้ว pin ลง .env ก่อนอัปเกรด 12 เป็น 13
ข้ามไปขั้นตอน

พอดีผมได้งานอัป Laravel จาก 12 เป็น 13 ให้แอปราวหนึ่งโหลของผู้ดูแลโฮสต์รายหนึ่ง บวกแอปบิลลิ่งอีกตัว และอีกหนึ่งแอปที่อยู่ใต้ผู้ดูแลอีกราย แอปที่ใหญ่สุดมี advisory ค้างอยู่หลายรายการในหลายแพ็กเกจ เลยต้องอัป ก่อนรันจริงผมเขียนสคริปต์ dry run ไล่ดูทีละแอป ตอนแรกผมกังวลแค่ว่าโค้ดจะเข้ากับ 13 ไหม ซึ่งเป็นความกังวลที่ผิดจุด สิ่งที่น่ากลัวกว่าคือ ชื่อ cookie ของ session ที่ผู้ใช้ทุกคนถืออยู่ ไม่ได้ถูกเขียนไว้ที่ไหนเลย มันถูกคำนวณจากชื่อแอป และแอปส่วนใหญ่ของผมตั้งชื่อเป็นภาษาไทย

ชื่อ cookie มาจาก APP_NAME โดยที่ไม่มีใครบอก

ใน config ของ Laravel ค่า default ของสามอย่างที่ใช้แยกแอปออกจากกัน ถูกสร้างจากนิพจน์เดียวกัน คือเอา APP_NAME ไปผ่าน Str::slug แล้วต่อท้าย ผมเปิดดูในโครงของ Laravel 13 ได้ตรงนี้

// config/session.php
'cookie' => env('SESSION_COOKIE', Str::slug((string) env('APP_NAME', 'laravel')).'-session'),

// config/cache.php
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),

// config/database.php  (redis options)
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),

พอ APP_NAME เป็นภาษาไทย Str::slug แปลงตัวอักษรไทยไม่ได้ ผลที่ออกมาคือสตริงว่าง ชื่อจริงที่ใช้อยู่เลยกลายเป็น -session, -cache- และ -database- มันใช้งานได้ปกติ ไม่มี error ไม่มี warning และไม่มีใครสังเกตมาตลอด

APP_NAME = ชื่อภาษาไทยStr::slug(APP_NAME)ได้สตริงว่างcookie ของ session-sessionprefix ของ cache-cache-prefix ของ redis-database-ถ้า default หรือ APP_NAME เปลี่ยน ชื่อเปลี่ยนเงียบ ๆbrowser ยังส่ง cookie ชื่อเก่า เซิร์ฟเวอร์หาชื่อใหม่ = ทุกคนหลุดล็อกอินkey ใน Redis ชื่อเก่ากลายเป็นของค้าง ไม่มีใครอ่าน
ชื่อทั้งสามมาจากนิพจน์ default ตัวเดียวกัน แม้ APP_NAME ไทยจะทำให้ได้ชื่อประหลาด แต่มันใช้งานได้ตราบที่ไม่มีอะไรเปลี่ยน

ทำไมถึงอันตรายตอนอัปเมเจอร์

ปัญหาไม่ได้อยู่ที่ชื่อมันแปลก ปัญหาอยู่ที่ชื่อนั้นขึ้นกับนิพจน์ default ล้วน ๆ ถ้ากรอบงานเปลี่ยนนิพจน์นั้นในเมเจอร์ใหม่ หรือวันไหนมีคนแก้ APP_NAME ชื่อทั้งสามจะเปลี่ยนไปเงียบ ๆ ผลคือ browser ของผู้ใช้ยังส่ง cookie ชื่อเก่ามา แต่เซิร์ฟเวอร์ไปหาชื่อใหม่ ทุกคนเลยหลุดล็อกอินพร้อมกัน ส่วน key ใน Redis ที่ขึ้นต้นด้วย prefix เก่าก็กลายเป็นของค้างที่ไม่มีใครอ่านอีก cache ทั้งก้อนถูกทิ้งโดยปริยาย

ผมยืนยันไม่ได้ว่า 13 เปลี่ยน default

ผมไม่ได้เทียบ default ระหว่าง 12 กับ 13 ทีละบรรทัด เลยบอกไม่ได้ว่านิพจน์เปลี่ยนจริงหรือเปล่า ผมเลยไม่ขอเขียนว่าเมเจอร์ใหม่เปลี่ยนมัน บทเรียนยืนอยู่ได้ทั้งสองทาง คือค่าที่ผู้ใช้ถืออยู่ต้องไม่ขึ้นกับค่า default ที่เราไม่ได้เขียนเอง ผมเลย pin ไว้เป็นการป้องกันไว้ก่อน

เคาน์เตอร์ฝากเสื้อโค้ตที่ตั๋วทุกใบถูกประทับชื่อจากป้ายภาษาไทยที่เครื่องประทับอ่านไม่ออก ตั๋วเลยมีแค่ขีดกับคำว่า ticket
ตั๋วทุกใบเขียนจากป้ายที่เครื่องอ่านไม่ออก ใช้ได้ตราบที่เครื่องกับป้ายไม่เปลี่ยน พอเปลี่ยนตั๋วเก่าก็ไม่มีใครรับ

ไล่ดูก่อนว่าแอปไหนโดนผลกระทบ

อย่าเพิ่ง pin มั่วทุกแอป ผมไล่ดูก่อนว่าแอปไหนที่ชื่อทั้งสามมาจากนิพจน์ default จริง แอปที่ตั้ง SESSION_COOKIE ไว้ใน .env อยู่แล้วไม่ต้องทำอะไร ส่วนแอปที่ APP_NAME เป็นภาษาไทยจะมีชื่อขึ้นต้นด้วยขีดกลางให้เห็นทันที ผมเลยเขียนลูปสั้น ๆ พิมพ์ค่าจริงของทุกแอปออกมาเป็นตารางเดียว อ่านอย่างเดียว ไม่แก้อะไร

# พิมพ์ชื่อ cookie / cache prefix / redis prefix ที่แต่ละแอปใช้อยู่จริงตอนนี้
for app in /home/webuser/domains/example.com/app-a /home/webuser2/domains/example.org/app-b; do
  printf '%s: ' "$app"
  (cd "$app" && /usr/local/php84/bin/php artisan tinker --execute="
    echo json_encode([config('session.cookie'), config('cache.prefix'), config('database.redis.options.prefix')]);")
  echo
done

ผลที่ออกมาจะแยกแอปได้เป็นสามกลุ่ม กลุ่มที่ pin ไว้แล้ว กลุ่มที่ใช้ชื่อภาษาอังกฤษปกติ และกลุ่มที่เป็นขีดกลางนำหน้า สองกลุ่มแรกชื่อไม่ขึ้นกับ default ในแบบที่เสี่ยง ส่วนกลุ่มสุดท้ายคือกลุ่มที่ผมต้อง pin ทุกตัว

กลุ่ม ชื่อ cookie ตัวอย่าง เสี่ยงตอนอัปเมเจอร์ไหม
ตั้ง SESSION_COOKIE ใน .env แล้ว ค่าที่ตั้ง ไม่เสี่ยง ชื่อไม่ขึ้นกับ default
APP_NAME เป็นอังกฤษ app-a-session เสี่ยงถ้า default หรือชื่อแอปเปลี่ยน
APP_NAME เป็นไทย -session เสี่ยงเท่ากัน และมองเห็นยากกว่า

อ่านค่าจริงจากแอปที่รันอยู่ แล้ว pin ลง .env

วิธีแก้ไม่ซับซ้อน คือก่อนรัน composer ให้อ่านค่าที่แอปใช้จริงตอนนี้ แล้วเขียนค่านั้นตรง ๆ ลง .env ให้เลิกพึ่งนิพจน์ default ผมอ่านจากสองทาง ทางแรกดู cookie ที่ browser ได้รับจากแอปจริง ทางที่สองถามจากตัวแอปเอง

# ใช้ PHP ตัวเดียวกับที่ FPM ของแอปนี้ใช้ (ใส่พาธเต็ม)
cd /home/webuser/domains/example.com/app-a
/usr/local/php84/bin/php artisan tinker --execute="
echo config('session.cookie'), PHP_EOL;
echo config('cache.prefix'), PHP_EOL;
echo config('database.redis.options.prefix'), PHP_EOL;"

ค่าที่ได้คือสามบรรทัดที่ผมเดาไว้ ผมใส่ลง .env โดยครอบเครื่องหมายคำพูดไว้ทุกค่า เพราะค่าที่ขึ้นต้นด้วยขีดกลางจะปลอดภัยกว่าถ้าครอบ แม้ dotenv ตัวที่ใช้อยู่จะอ่านได้ทั้งสองแบบ

# .env  (ค่าต้องตรงกับที่แอปใช้อยู่ตอนนี้เป๊ะ ๆ)
SESSION_COOKIE="-session"
CACHE_PREFIX="-cache-"
REDIS_PREFIX="-database-"

หลังอัปจริง ผู้ใช้ที่ล็อกอินอยู่ยังล็อกอินอยู่ต่อ และ cache ไม่ถูกทิ้ง ซึ่งเป็นผลที่ผมต้องการ

ภาพก่อนและหลัง ฝั่งซ้ายลูกค้าถือตั๋วที่เคาน์เตอร์ใหม่ไม่รู้จัก ฝั่งขวาผู้จัดการเขียนข้อความของตั๋วเก่าลงการ์ดติดไว้ที่เคาน์เตอร์
การ pin คือการเขียนข้อความของตั๋วเก่าลงการ์ดแปะไว้ ตั๋วที่ลูกค้าถืออยู่เลยยังใช้ได้หลังเปลี่ยนเคาน์เตอร์

ข้อควรระวังคือ pin เพื่อความต่อเนื่อง ถ้าวันหนึ่งอยากเปลี่ยนชื่อเหล่านี้จริงให้เปลี่ยนโดยตั้งใจ พร้อมแจ้งว่าผู้ใช้จะถูกให้ล็อกอินใหม่หนึ่งรอบ อย่าให้มันเกิดขึ้นเองเพราะใครแก้ชื่อแอป

ทำไมไม่เปลี่ยน APP_NAME เป็นภาษาอังกฤษไปเลย

คำถามที่น่าจะตามมาคือ ถ้าชื่อแอปทำให้เกิดเรื่องนี้ ก็แก้ชื่อเป็นอังกฤษให้จบได้ไหม คำตอบคือทำได้ แต่ไม่ใช่ในจังหวะอัปเมเจอร์ เพราะการแก้ชื่อก็คือการเปลี่ยนชื่อ cookie ผลเหมือนกันทุกประการ ผู้ใช้ทุกคนหลุดล็อกอินและ cache ถูกทิ้ง ซึ่งไปปนกับความเสี่ยงของการอัปเวอร์ชัน ถ้ามีอะไรผิดปกติจะแยกไม่ออกว่าเกิดจากอะไร

ผมเลยแยกเป็นสองงานคนละวัน วันอัปเวอร์ชันให้ทุกอย่างเหมือนเดิมทุกจุดที่ผู้ใช้มองเห็น แล้วถ้าอยากเปลี่ยนชื่อสวย ๆ ค่อยนัดวันที่ยอมให้ล็อกอินใหม่ แจ้งผู้ใช้ล่วงหน้า และเปลี่ยนผ่านค่า SESSION_COOKIE ที่ pin ไว้ ไม่ใช่ผ่าน APP_NAME ซึ่งใช้กับอย่างอื่นด้วย เช่นชื่อในอีเมล

ไม่ pin

ชื่อขึ้นกับ default

อัปได้เร็วกว่าหนึ่งขั้น แต่ถ้า default เปลี่ยนหรือมีคนแก้ APP_NAME ผู้ใช้ทั้งระบบหลุดล็อกอิน และแคชถูกทิ้งโดยไม่มีใครรู้ว่าทำไม

pin ไว้

ชื่อคงที่ตามที่เขียน

เพิ่มสามบรรทัดใน .env ต่อแอป อัปเมเจอร์แล้วผู้ใช้ไม่รู้สึกอะไร และเปลี่ยนชื่อทีหลังได้โดยตั้งใจ ในวันที่เลือกเอง

สูตรอัปเป็นชุด ที่ผมใช้ได้ผล

พอจัดการเรื่อง pin ได้ ผมเอาไปใส่ในสคริปต์อัปเป็นชุด ซึ่งทำต่อหนึ่งแอปตามลำดับในผังนี้ ลำดับสำคัญตรงที่ pin มาก่อน composer เสมอ

1 สำรองไฟล์vendor, lock, .envเป็น tarball zstd2 dry runดูว่าจะเปลี่ยนอะไรยังไม่แตะของจริง3 pin ใน .envSESSION_COOKIECACHE/REDIS_PREFIX4 composer updateไปที่ ^13ในกรอบที่ constraint ให้5 composer auditหลายรายการ -> 0แอปใหญ่สุด6 ล้างแล้วสร้าง cacheconfig route view7 เช็ก runner ทุกตัวworker cron subdomainใช้ PHP รุ่นถูกไหม8 smoke test106 หน้า = 200POST ไม่มี key = 401
ลำดับต่อหนึ่งแอป pin ค่าก่อน composer เสมอ และจบด้วยการทดสอบผู้เรียกจากภายนอกที่ไม่ผ่าน CSRF
  1. สำรอง vendor, composer.json, composer.lock และ .env เป็น tarball แบบ zstd ต่อแอป
  2. รัน dry run ดูก่อนว่าจะเปลี่ยนอะไร ยังไม่แตะของจริง
  3. pin SESSION_COOKIE, CACHE_PREFIX, REDIS_PREFIX ลง .env
  4. อัปเป็น ^13 เฉพาะในกรอบที่ composer อนุญาต
  5. รัน composer audit ต้องได้ศูนย์
  6. ล้างแล้วสร้างแคช config, route, view ใหม่
  7. เช็ก runner ทุกตัวของแอปว่าใช้ PHP รุ่นที่ถูกต้อง
  8. smoke test ทุกหน้าและทดสอบผู้เรียกจากภายนอก

สำรองไว้ตั้งแต่ข้อแรกก็เพื่อให้มีทางย้อนกลับ ถ้า smoke test หรือ audit ไม่ผ่านแอปไหน ทางย้อนกลับคือแตก tarball ของแอปนั้นทับ ล้างแคช แล้วรัน smoke test ซ้ำเพื่อยืนยันว่าหน้าเว็บกลับมาเหมือนก่อนแตะ ผมวางให้ย้อนเป็นรายแอป ไม่ย้อนทั้งชุด เพราะแอปที่ผ่านแล้วไม่ควรถูกลากกลับเพราะแอปอื่นพัง

# ย้อนกลับเฉพาะแอปที่ไม่ผ่าน
cd /home/webuser/domains/example.com/app-a
zstd -dc /root/pre-l13/app-a.tar.zst | tar -xf -
/usr/local/php84/bin/php artisan optimize:clear

ข้อ 7 มีกับดักของมันเอง คือ worker และ cron ที่ใช้ PHP คนละรุ่นกับเว็บ ผมเขียนแยกไว้ใน บทความเรื่อง platform_check ตรงนี้บอกแค่ว่าต้องเช็กก่อนจบงาน ส่วนข้อ 5 ผมใช้แนวเดียวกับ รายการตรวจ security ของ Laravel คือให้ composer audit เป็นตัวตัดสิน

# สำรองก่อนทุกแอป (ตัวอย่างหนึ่งแอป)
cd /home/webuser/domains/example.com/app-a
tar -cf - vendor composer.json composer.lock .env | zstd -T2 -19 -o /root/pre-l13/app-a.tar.zst

composer audit            # ก่อนอัป ต้องมี advisory ให้เห็น
composer update "laravel/*" --with-dependencies
composer audit            # หลังอัป ต้องได้ No security vulnerability advisories found
-> 0advisory แอปใหญ่สุด
16แอปที่ audit สะอาด
106หน้าที่ทดสอบ
43/43เทสต์ของหนึ่งแอป

ตัวเลขที่ได้คือ advisory ของแอปใหญ่สุดลดจากหลายรายการเหลือศูนย์ และทั้ง 16 แอปผ่าน audit สะอาด ผม smoke test 106 หน้าจาก 12 โดเมน ได้ 200 ทั้งหมด log ของ Laravel ไม่มี error ใหม่ ล็อกอินและแผงแอดมินที่สร้างด้วย Livewire เปิดได้ และ queue worker ยังรันอยู่บน PHP 8.4 ก่อนแตะของจริง ผมเอาชุดเทสต์ของหนึ่งแอปไปรันบน 13 ในเครื่องก่อนแล้วผ่าน 43 จาก 43

เจอแอปที่ตกหล่นเพราะนับตามเจ้าของบัญชี

รอบแรกผมมีแอปหนึ่งตัวที่หลุดรายการ เพราะมันอยู่ใต้ผู้ดูแลโฮสต์คนละรายกับชุดที่ผมไล่ ผมเลยเปลี่ยนมาไล่หาจากไฟล์ artisan ทั้งเครื่อง ไม่ไล่ตามว่าบัญชีไหนเป็นของใคร

# หาทุกแอป Laravel ทั้งเครื่อง ไม่สนว่าเจ้าของบัญชีเป็นใคร
find /home -maxdepth 6 -name artisan -not -path '*/vendor/*' 2>/dev/null | sort

ผู้เรียกจากภายนอกที่ POST เข้ามาโดยไม่ผ่าน CSRF

แอปหนึ่งในชุดนี้รับข้อมูลต่อเนื่องจากอุปกรณ์ภายนอกที่ POST เข้ามาที่ endpoint ซึ่งยกเว้น CSRF ไว้ใน bootstrap/app.php ผมเลยกังวลว่าการอัปจะทำให้อุปกรณ์เหล่านั้นหยุดส่งโดยที่ฝั่งผมไม่รู้

// bootstrap/app.php  (endpoint ที่อุปกรณ์ภายนอกเรียก ตรวจสิทธิ์ด้วย API key ของตัวเอง)
->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: [
        'devices/*',
    ]);
})

วิธีเช็กที่ผมใช้มีสามข้อ ข้อแรก POST โดยไม่ใส่ API key ต้องได้ 401 ซึ่งเป็นการตอบของ endpoint เอง ถ้าได้ 419 แปลว่า CSRF กลับมาครอบ endpoint นี้ ข้อสอง อุปกรณ์ยังเพิ่มแถวใหม่ในตารางต่อเนื่อง ข้อสาม สั่งส่งด้วยมือหนึ่งครั้งแล้วต้องได้ 200 พร้อม body ที่มี ok:true รอบนี้ผ่านทั้งสามข้อ ไม่ต้องแก้อะไรฝั่งอุปกรณ์เลย

# ไม่มี key ต้องได้ 401 ไม่ใช่ 419
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://example.com/devices/push -d '{}'

ของแถม โค้ดบน production ไม่ตรงกับต้นฉบับในเครื่อง

อีกเรื่องที่เจอระหว่างทางคือ ต้นฉบับของแอปตัวหนึ่งในเครื่องผมล้าหลัง production ไปไกล เพราะเครื่องมือที่เคยใช้พัฒนาถูกถอดไปแล้ว ผมดึงโค้ดจาก production กลับมาไว้ในที่ทำงาน รัน composer install แล้วใช้สคริปต์เช็กการเลื่อนของไฟล์เทียบกัน ได้ว่าไฟล์เหมือนกันครบ 76 จาก 76 ไฟล์ บทเรียนคือ ก่อนแก้อะไรให้ดึงของจริงลงมาก่อนเสมอ ไม่เชื่อว่าสำเนาในเครื่องคือเวอร์ชันที่รันอยู่

อีกข้อที่ผมเอาไปใช้ต่อคือ ทุกแอปใหม่ที่สร้างหลังจากนี้ ผมใส่สามบรรทัดนี้ลง .env ตั้งแต่วันแรก ด้วยชื่อที่ตั้งใจเลือกเอง ไม่ปล่อยให้เป็นผลจากการแปลงชื่อแอป ต้นทุนคือสามบรรทัด แลกกับการที่เมเจอร์หน้าไม่ต้องมานั่งกังวลเรื่องนี้อีก และไม่ต้องไล่เช็กย้อนหลังทีละแอปเหมือนรอบนี้

สรุปคือ ก่อนอัปเมเจอร์ให้จดชื่อ cookie กับ prefix ที่แอปใช้จริง pin ลง .env แล้วค่อยแตะ composer ส่วนจะ pin หรือไม่ก็ได้ถ้าคุณพร้อมให้ผู้ใช้ล็อกอินใหม่ทั้งระบบ ตามความเหมาะสมของแต่ละที่ แต่ผมเลือกให้ไม่มีใครหลุดล็อกอิน เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย