พอเอาเว็บ Laravel ไปไว้หลัง Cloudflare หรือหลัง nginx ที่ทำ HTTPS ให้ อาการแรกที่มักเจอคือลิงก์และ asset ออกมาเป็น http:// เบราว์เซอร์บล็อกเป็น Mixed Content แล้ว Livewire หรือ Inertia ก็หยุดทำงาน อีกอาการที่ไม่ค่อยมีใครสังเกตคือ $request->ip() ได้ IP ของ Cloudflare ทุก request rate limit ที่นับตาม IP จึงนับผู้ใช้หลายคนเป็นคนเดียว
คำตอบที่หาเจอเร็วที่สุดคือบรรทัดนี้
$middleware->trustProxies(at: '*');
ทั้งสองอาการหายจริง แต่ก็เปิดช่องให้ปลอม IP ไปด้วย
Laravel รู้จักผู้ใช้จริงผ่าน header ที่ proxy ส่งมา
เมื่อมี proxy อยู่ตรงกลาง คนที่ต่อเข้า PHP คือ proxy ไม่ใช่ผู้ใช้ REMOTE_ADDR จึงเป็น IP ของ Cloudflare และ request ระหว่าง proxy กับเซิร์ฟเวอร์ก็อาจเป็น HTTP ธรรมดา ข้อมูลของผู้ใช้จริงมากับ header
X-Forwarded-Forรายการ IP ที่ request ผ่านมาX-Forwarded-Protoผู้ใช้เปิดเว็บด้วยhttpหรือhttpsX-Forwarded-Hostโดเมนที่ผู้ใช้พิมพ์
ใครก็ส่ง header พวกนี้มาเองได้ TrustProxies จึงบอก Laravel ว่าให้เชื่อ header เหล่านี้เฉพาะเมื่อ request มาจาก IP ที่เราระบุไว้
* ทำให้ผู้ใช้ปลอม IP ได้แม้จะผ่าน Cloudflare
ตามเอกสารของ Cloudflare ถ้า request มี X-Forwarded-For ติดมาอยู่แล้ว Cloudflare จะต่อ IP ที่ต่อเข้ามาไว้ท้ายรายการ ผู้ใช้ที่ส่ง X-Forwarded-For: 1.2.3.4 มาเอง จะไปถึงเซิร์ฟเวอร์เป็น 1.2.3.4, <IP จริง>
Laravel ไล่รายการนี้จากขวาไปซ้าย ข้าม IP ที่อยู่ในรายการ proxy ที่เชื่อถือ แล้วหยิบตัวแรกที่ไม่ใช่ proxy เป็น IP ของผู้ใช้ ถ้าเชื่อแค่ Cloudflare ตัวที่ได้คือ IP จริงที่ Cloudflare เติมให้ แต่ถ้าเชื่อ * ทุก IP ในรายการก็นับเป็น proxy หมด Laravel จึงย้อนไปหยิบตัวซ้ายสุด ซึ่งเป็นค่าที่ผู้ใช้พิมพ์มาเอง
เราลองส่ง request แบบเดียวกันเข้า Request ของ Symfony ที่ Laravel ใช้ ได้ผลแบบนี้
trust '*' ip=1.2.3.4
trust Cloudflare ip=203.0.113.7
ผลที่ตามมามีหลายเรื่อง
- rate limit ใช้ไม่ได้ คนที่ลองรหัสผ่านซ้ำ ๆ หรือยิงฟอร์มติดต่อเปลี่ยน IP ปลอมทุกครั้ง ก็ไม่เคยโดนจำกัด
- log และสถิติผิด IP ที่บันทึกไว้ตอนมีเหตุผิดปกติเชื่อไม่ได้
- allowlist ตาม IP หลุด หน้า admin ที่เปิดให้เฉพาะ IP ของออฟฟิศ ใครพิมพ์ IP นั้นมาก็เข้าได้
ส่วน X-Forwarded-Host อันตรายกว่า ถ้า Laravel เชื่อ header นี้ ผู้ใช้ก็กำหนดโดเมนในลิงก์ที่แอปสร้างได้ ลิงก์รีเซ็ตรหัสผ่านในอีเมลอาจชี้ไปที่โดเมนของคนอื่น ซึ่งค่าเริ่มต้นของ TrustProxies เชื่อ header นี้ด้วย
เชื่อเฉพาะ IP ของ Cloudflare และ header สองตัว
->withMiddleware(function (Middleware $middleware): void {
$middleware->trustProxies(headers: Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_PROTO);
$middleware->trustHosts();
})
return [
// Loopback plus Cloudflare's published ranges (cloudflare.com/ips, checked 2026-10-26)
'proxies' => [
'127.0.0.1',
'::1',
'173.245.48.0/20',
'103.21.244.0/22',
// … ช่วง IPv4 ที่เหลือ
'2400:cb00::/32',
'2606:4700::/32',
// … ช่วง IPv6 ที่เหลือ
],
];
- ไม่ส่ง
at:เมื่อไม่ได้ระบุ IPTrustProxiesจะอ่านรายการจากconfig('trustedproxy.proxies')เอง เราจึงเก็บรายการยาว ๆ ไว้ในไฟล์ config แยก แล้วเขียนวันที่เช็กไว้ในคอมเมนต์ - ช่วง IP ของ Cloudflare คัดจาก cloudflare.com/ips ทั้ง IPv4 และ IPv6
- loopback สำหรับ request ที่มาจากเครื่องเดียวกัน เช่น nginx ที่ส่งต่อให้ PHP ในเครื่อง หรือ Cloudflare Tunnel ที่ต่อเข้าแอปจาก
127.0.0.1 - เชื่อแค่
FORกับPROTOสองตัวนี้คือสิ่งที่เราต้องการจริง ส่วนHOST,PORTและPREFIXไม่ต้องเชื่อ เพราะโดเมนของเว็บมีอยู่แล้วในAPP_URL
trustHosts() ปิดอีกทาง คือรับเฉพาะ request ที่ Host ตรงกับโดเมนใน APP_URL หรือ subdomain ของโดเมนนั้น ที่เหลือตอบ 400 ไปเลย middleware นี้ข้ามให้เองบนเครื่อง local และตอนรัน test
เขียน test กันไม่ให้ใครแก้กลับเป็น *
วันหนึ่งที่ Livewire ขึ้น Mixed Content อีก คนในทีมหรือ AI agent ก็อาจแก้กลับเป็น at: '*' เพราะเป็นคำตอบที่หาเจอง่ายที่สุด test สั้น ๆ ช่วยกันเรื่องนี้ได้ repo ของเว็บนี้มี test แบบนี้
beforeEach(function (): void {
Route::get('/_test/client-ip', fn (): ?string => request()->ip());
});
it('ignores addresses a client prepends to the Cloudflare forwarded chain', function (): void {
$this->withServerVariables(['REMOTE_ADDR' => '172.68.10.20'])
->get('/_test/client-ip', ['X-Forwarded-For' => '198.51.100.1, 203.0.113.9'])
->assertSeeText('203.0.113.9');
});
it('ignores a forwarded host even from a trusted proxy', function (): void {
Route::get('/_test/host', fn (): string => request()->getHost());
$this->withServerVariables(['REMOTE_ADDR' => '172.68.10.20'])
->get('/_test/host', ['X-Forwarded-Host' => 'evil.example'])
->assertDontSeeText('evil.example');
});
172.68.10.20 อยู่ในช่วงของ Cloudflare ถ้าใครเปลี่ยนเป็น * test แรกจะได้ 198.51.100.1 แล้ว fail พร้อมชื่อ test ที่บอกว่าพังเรื่องอะไร ใน repo จริงยังมี test ว่า IP จาก loopback และ IPv6 ของ Cloudflare ส่งต่อได้ และ X-Forwarded-Proto ยังใช้ได้ด้วย
ช่วง IP ของ Cloudflare เปลี่ยนได้
Cloudflare ไม่ค่อยเปลี่ยนรายการนี้ แต่ก็เปลี่ยนได้ ถ้ามีช่วงใหม่แล้วเราไม่ได้เพิ่ม request ที่มาจากช่วงนั้นจะได้ IP ของ Cloudflare แทนผู้ใช้อีกครั้ง ทางที่ง่ายคือเปิดหน้า cloudflare.com/ips เทียบเป็นระยะ แล้วแก้วันที่ในคอมเมนต์ ถ้าไม่อยากดูแลเอง แพ็กเกจ monicahq/laravel-cloudflare โหลดรายการจาก Cloudflare มาเก็บใน cache ให้
ปิดทางที่ไม่ผ่าน Cloudflare ด้วย
trustProxies แก้ฝั่งแอป ถ้าเราดูแลเซิร์ฟเวอร์เองยังมีอีกสองชั้นที่ควรทำ
- รับเฉพาะ request จาก Cloudflare ตั้ง firewall หรือ nginx ให้ปฏิเสธ IP อื่น คนที่รู้ IP ของเซิร์ฟเวอร์จะได้ยิงตรงเข้ามาไม่ได้ หรือใช้ Cloudflare Tunnel ซึ่งไม่ต้องเปิด port ให้ใครเลย
- ให้ nginx คืน IP จริงก่อนถึง PHP ด้วย
set_real_ip_fromกับreal_ip_header CF-Connecting-IPREMOTE_ADDRที่ Laravel เห็นจะเป็น IP ของผู้ใช้ตั้งแต่ต้น และไม่อยู่ในรายการ proxy Laravel จึงไม่อ่านX-Forwarded-Forที่ผู้ใช้ส่งมาเองเลย
บน shared hosting ที่แก้ nginx ไม่ได้ trustProxies แบบในโพสต์นี้คือสิ่งที่เราทำได้จากฝั่งแอป และพอแล้วสำหรับเรื่อง IP กับลิงก์ ค่านี้เป็นหนึ่งในค่าที่เราตั้งตั้งแต่วันแรกของโปรเจกต์ Laravel ใหม่ ส่วนงานอื่นกับ Cloudflare อย่างล้าง cache หรือตั้ง WAF จาก artisan ลองดูแพ็กเกจ laravel-cloudflare ได้ครับ





