นโยบายความปลอดภัยเนื้อหาเว็บไซต์: การตั้งค่า CSP ความเสี่ยงจากการถูกบายพาส และการนำไปใช้
หนึ่ง CSP กันอะไรได้จริง
CSP ส่งกฎผ่าน HTTP response header เบราว์เซอร์ใช้กฎนี้ตัดสินใจว่าทรัพยากรใดโหลดได้ ป้องกันปัญหาหลักสองอย่าง: สคริปต์อันตรายจากการฉีด XSS และโดเมนบุคคลที่สามที่ไม่ได้รับอนุญาตถูกฝังเข้าไปในหน้าเว็บ
มันไม่ใช่ไฟร์วอลล์ CSP กัน SQL injection ไม่ได้ และกันเซิร์ฟเวอร์ถูกบุกรุกก็ไม่ได้ มันดูแลการโหลดทรัพยากรฝั่งเบราว์เซอร์ ทำงานร่วมกับ HTTPS และ HSTS วิธีตั้งค่า HTTPS ดูที่การตั้งค่า HTTPS สำหรับเว็บไซต์อิสระ。
ปัญหาที่เว็บไซต์อิสระเจอบ่อยคือสคริปต์บุคคลที่สามเยอะเกินไป: GA4, GTM, Hotjar, แชทออนไลน์, พิกเซลโฆษณา ทุกครั้งที่เพิ่มสคริปต์ ต้องเพิ่มโดเมนใน whitelist ของ CSP ไม่งั้นเบราว์เซอร์จะบล็อกสคริปต์ การจัดการสคริปต์บุคคลที่สามดูที่การจัดการสคริปต์บุคคลที่สามสำหรับเว็บไซต์อิสระ。
ข้อเท็จจริงที่มักถูกมองข้ามคือ CSP แทบไม่มีผลเสียต่อ SEO Googlebot เองไม่ได้จำกัดทรัพยากรนอกเหนือจาก JS การตั้งค่า CSP ที่ดีไม่กระทบการจัดเก็บข้อมูล กังวลว่า CSP จะกระทบการครอว์ลนั้นเกินเหตุ
| คำสั่ง | ควบคุมอะไร | ค่าที่แนะนำ |
|---|---|---|
| default-src | ทรัพยากรทั้งหมดเป็นค่าเริ่มต้น | ‘self’ |
| script-src | แหล่งที่มาของสคริปต์ JS | ‘self’ + โดเมนสถิติ |
| style-src | แหล่งที่มาของ CSS | ‘self’ ‘unsafe-inline’ |
| img-src | แหล่งที่มาของรูปภาพ | ‘self’ data: + CDN |
| object-src | ปลั๊กอิน | ‘none’ (ต้องปิด) |
| frame-ancestors | ใครฝังเว็บนี้ได้ | ‘none’ |
สอง ขั้นตอนการติดตั้งสามระยะ: รายงาน, รวบรวม, บังคับใช้
ข้อผิดพลาดที่พบบ่อยที่สุดคือใส่ header Content-Security-Policy ไปเลย วิธีนี้จะบล็อกสคริปต์ทั้งหมดในเว็บที่ไม่อยู่ใน whitelist ทันที GA4, แชทออนไลน์, พิกเซลพังหมด ฝ่ายธุรกิจจะร้องเรียนทันที
วิธีที่ถูกต้องคือใช้ header Content-Security-Policy-Report-Only ก่อน เบราว์เซอร์จะรายงานการละเมิดเท่านั้น ไม่บล็อก เก็บ log สองสัปดาห์ แล้วเพิ่มโดเมนที่ใช้จริงทั้งหมดเข้า whitelist
ตัวอย่างสถานการณ์ที่หนึ่ง: เพิ่ม CSP ให้เว็บ WordPress เริ่มจากส่ง Report-Only
สองวันต่อมาในรายงานปรากฏโดเมน googletagmanager.com, hotjar.com, youtube.com สามโดเมน
เพิ่มทีละรายการเข้า script-src และ frame-src สองสัปดาห์ต่อมารายงานเหลือแค่หลักหน่วยต่อวัน จึงสลับเป็นโหมดบังคับใช้
| ระยะ | response header | การกระทำ | ระยะเวลา |
|---|---|---|---|
| ขั้นแรก | Content-Security-Policy-Report-Only | รายงานอย่างเดียว ไม่บล็อก | 14 วัน |
| ขั้นที่สอง | เหมือนเดิม ค่อย ๆ เพิ่มไวท์ลิสต์ | การละเมิดลดลงทุกวัน | จนเหลือหลักหน่วย |
| ขั้นที่สาม | Content-Security-Policy | บล็อกจริง | ระยะยาว |
| ตลอดกระบวนการ | เพิ่ม report-to หรือ report-uri | เก็บรายงานการละเมิด | เปิดไว้ตลอด |
สคริปต์อินไลน์คือหลุมที่ใหญ่ที่สุด ธีม WordPress และปลั๊กอินหลายตัวเขียน <script> ลงในหน้าเว็บโดยตรง ซึ่ง CSP บล็อกอินไลน์ไว้เป็นค่าเริ่มต้น มีสองวิธี วิธีแรกคือย้ายสคริปต์อินไลน์ออกไปเป็นไฟล์ js ภายนอก วิธีที่สองคือใช้แฮช sha256 เพื่ออนุญาตเฉพาะบล็อกนั้น ไม่ใช่เปิด unsafe-inline เพราะขี้เกียจ
style-src ผ่อนกว่า script-src ตัวแก้ไข Gutenberg และธีมหลายตัวจะส่ง style อินไลน์ออกมา ถ้า style-src เปิด unsafe-inline ไว้ชั่วคราวก็ไม่ใช่เรื่องใหญ่
แต่ script-src ห้ามเปิด unsafe-inline เด็ดขาด เพราะนี่คือแนวป้องกันหลักของ CSP ในการกัน XSS มาตรฐานความเข้มงวดของสองคำสั่งนี้ต้องแยกกัน
สาม ความเสี่ยงในการถูกหลบเลี่ยง อย่าให้ CSP เป็นแค่ป้าย
ตั้งค่า CSP ผิดก็เหมือนไม่ได้ตั้ง สามรูปแบบต่อไปนี้ทำให้กำแพงนี้ใช้ไม่ได้ทันที
แบบแรกคือเปิด unsafe-inline มันอนุญาตให้สคริปต์อินไลน์ใด ๆ ทำงานได้ สคริปต์อันตรายที่ถูกฉีดผ่าน XSS ก็รันได้ แนวป้องกันหลักของ CSP ก็พัง แบบที่สองคือเปิดไวลด์การ์ด * เท่ากับไม่จำกัดแหล่งที่มา แบบที่สามคือลืม object-src เบราว์เซอร์เก่ายังหลบผ่าน Flash หรือปลั๊กอินได้
ตัวอย่างสถานการณ์ที่สอง ขี้เกียจเลยเขียน ‘unsafe-inline’ ลงใน script-src
สองเดือนต่อมา เว็บไซต์ถูกฝังสคริปต์ฟิชชิงแบบอินไลน์ และ CSP ก็ไม่สามารถบล็อกได้เลย
วิธีที่ถูกต้องคือใช้ nonce สุ่มค่าใหม่ทุกครั้งที่ร้องขอ อนุญาตเฉพาะสคริปต์อินไลน์ที่มีค่านี้
| วิธีเขียนที่ผิด | ความเสี่ยง | ทางเลือกทดแทน |
|---|---|---|
| script-src * | สคริปต์บุคคลที่สามใด ๆ โหลดได้ | ระบุโดเมนในไวท์ลิสต์ทีละราย |
| script-src ‘unsafe-inline’ | สคริปต์ที่ฉีดผ่าน XSS ยังรันตามปกติ | ย้ายออกเป็นไฟล์ภายนอกหรือใช้ nonce/แฮช |
| ไม่เขียน object-src | ปลั๊กอินเก่าถูกใช้ในทางที่ผิดได้ | object-src ‘none’ |
| ไม่ตั้งค่า frame-ancestors | หน้าถูกฝังใน iframe โดยไม่หวังดี | frame-ancestors ‘none’ |
CSP กับความสอดคล้องตามกฎหมายเป็นคนละเรื่อง การยินยอมคุกกี้ตาม GDPR เป็นอีกกลไกหนึ่ง ดูที่ความสอดคล้อง GDPR สำหรับเว็บไซต์อิสระ CSP ป้องกันการฉีดสคริปต์ ไม่ได้แก้ปัญหาการยินยอมคุกกี้ ส่วนการเข้าสู่ระบบและสิทธิ์ไฟล์ระดับเซิร์ฟเวอร์ดูที่การเสริมความปลอดภัยเว็บไซต์อิสระ。
เลือก nonce หรือแฮชอย่างไร สคริปต์อินไลน์แบบคงที่ใช้แฮช สคริปต์อินไลน์ที่สร้างแบบไดนามิกใช้ nonce โค้ดติดตามแบบคงที่ในธีม WordPress นับเป็นแฮชเพื่ออนุญาต บล็อกสคริปต์ที่เนื้อหาเปลี่ยนทุกครั้งที่ร้องขอใช้ nonce
ตำแหน่งที่ติดตั้งก็มีข้อควรพิจารณา ควรส่ง CSP response header จากชั้น Nginx หรือ CDN ไม่ควรพึ่งให้แอป PHP ส่งออก เพราะการส่งจากชั้นแอปจะหายไปเมื่อ PHP ทำงานผิดพลาด แต่การส่งจากชั้น CDN เสถียรกว่า
| ตำแหน่งติดตั้ง | ความเสถียร | เหมาะกับ |
|---|---|---|
| การตั้งค่า Nginx | สูง แอปล่มก็ยังอยู่ | เซิร์ฟเวอร์ที่สร้างเอง |
| กฎ CDN | สูง มีผลทั่วโลก | ใช้ Cloudflare เป็นต้น |
| ส่งออกจากแอป PHP | ต่ำ หายเมื่อเกิดข้อขัดข้อง | ทดสอบชั่วคราว |
ปลายทางรับรายงานก็ต้องสร้างแยกต่างหาก ที่อยู่ที่ report-uri ชี้ไปต้องไม่ถูก CSP บล็อกเอง ไม่งั้นรายงานก็ส่งออกไม่ได้
เว็บไซต์ standalone สามารถใช้บริการเก็บ report สำเร็จรูป หรือจะตั้ง log endpoint เฉพาะใน Nginx เพื่อรับ report แล้วเขียนลงไฟล์ log แยกต่างหาก ไม่ปนกับ log ทราฟฟิกของเว็บหลัก
เมื่อปริมาณ report เยอะขึ้นต้องทำ aggregation วันหนึ่งอาจได้ report หลายร้อยรายการ ดูทีละรายการไม่ไหว ให้จัดกลุ่มตาม directive ที่ถูกละเมิดและโดเมนที่ถูกบล็อก แล้วโฟกัสที่โดเมนใหม่ที่เพิ่มขึ้นในแต่ละวัน
มีความเข้าใจผิดที่พบบ่อย คือคิดว่าเปิด CSP แล้วจบเลย ความจริงทุกครั้งที่เพิ่มสคริปต์บุคคลที่สามใหม่ หรืออัปเดตธีม อาจมีแหล่งที่ละเมิดใหม่โผล่เข้ามาได้
ดังนั้นหลังจากเปลี่ยนเป็น Enforce แล้ว ห้ามปิด report-uri เพราะมันคือสัญญาณเดียวที่บอกคุณว่า “สคริปต์ที่เพิ่งเพิ่มลืมใส่ whitelist”
เรื่องการรองรับของเบราว์เซอร์ก็ต้องพูดถึง เบราว์เซอร์สมัยใหม่ (Chrome, Edge, Firefox, Safari เวอร์ชันสองปีล่าสุด) รองรับ CSP 3 ครบถ้วน ส่วน IE เก่าข้ามไปได้เลย เพราะผู้เข้าชมเว็บไซต์ standalone แบบ B2B แทบไม่ใช้ IE
ยังมีรายละเอียดตอน deploy อีกอย่าง คือ header CSP ต้องส่งทั้ง http และ https ถ้าส่งเฉพาะ https คำขอช่วง http ต้นทางจะไม่ได้รับการป้องกันจาก CSP
หลังจากเปลี่ยนเป็น Enforce แนะนำให้คง header Report-Only ไว้ทำงานคู่กันสักระยะ ทั้งบล็อกทั้งรายงาน เผื่อมีอะไรตกหล่นจะได้เห็นใน report
CSP ไม่ใช่โปรเจกต์ที่ทำครั้งเดียวจบ ทุกครั้งที่เพิ่มสคริปต์บุคคลที่สามใหม่ (แชท คุย วิดีโอ โฆษณา) ต้องดูก่อนในโหมด report ว่ามันโผล่มาในรายการละเมิด แล้วค่อยเพิ่มโดเมนนั้นเข้า whitelist จากนั้นจึงเปลี่ยนเป็น production
ใส่การตรวจ CSP เข้าไปในเช็กลิสต์ก่อนอัปสคริปต์ใหม่ พอเพิ่มสคริปต์ก็เพิ่ม whitelist ไปพร้อมกัน ไม่ต้องรอให้ผู้ใช้แจ้งว่า “ปุ่มไม่แสดง” หลังอัปแล้วค่อยนึกถึง CSP
เกณฑ์ตัดสินอย่างหนึ่ง คือถ้าสคริปต์บุคคลที่สามบางตัวแปลงเป็นไฟล์ภายนอกไม่ได้ และคำนวณ hash ก็ไม่ได้ ต้องกลับมาประเมินว่า บริการบุคคลที่สามนั้นคุ้มค่าที่จะใช้หรือไม่ บริการที่ต้องพึ่ง unsafe-inline ตัวมันเองก็คือหนี้ทางเทคนิคอยู่แล้ว
หลัง deploy เสร็จ ให้ใช้แผง Security ในคอนโซลเบราว์เซอร์สุ่มตรวจทีละหน้า ดูหน้าแรก หน้าสินค้า หน้าบล็อก หน้าแบบฟอร์ม ให้ครบทุกหน้า เพื่อยืนยันว่าไม่มีคำเตือนละเมิดสีแดง
หน้าที่สุ่มตรวจไม่ผ่าน ให้กลับไปหาโดเมนที่เกี่ยวข้องใน report เพิ่มเข้า whitelist แล้วเฝ้าดูอีกหนึ่งวัน วนแบบนี้สองสามรอบก็จะนิ่ง
CSP ไม่ใช่รายการความปลอดภัยที่ deploy ครั้งเดียวแล้วจบ แต่จะพัฒนาควบคู่ไปกับคลังสคริปต์บุคคลที่สามของคุณในระยะยาว ทุกครั้งที่เพิ่มสคริปต์ใหม่ ต้องอัปเดต whitelist ไปพร้อมกัน อย่าลืมตั้งค่าโดเมนสคริปต์บุคคลที่สามที่โผล่ใหม่ตัวใดตัวหนึ่งเด็ดขาด
| แหล่งที่มาของ report | ความหมาย | วิธีจัดการ |
|---|---|---|
| ละเมิด script-src | สคริปต์ใหม่ยังไม่ได้ใส่ whitelist | เพิ่มโดเมนหรือแยกเป็นไฟล์ภายนอก |
| ละเมิด img-src | รูปที่ลิงก์ภายนอกมาจากโดเมนใหม่ | เพิ่ม CDN หรือเปลี่ยนเป็น path สัมพัทธ์ |
| ละเมิด frame-src | มีการฝัง iframe ใหม่ | เพิ่มโดเมนวิดีโอหรือเอาออก |
| ละเมิด base-uri | หน้าถูก inject แท็ก base | ตรวจ XSS ทันที |
FAQ คำถามที่พบบ่อย
CSP ควรใช้โหมดบังคับตั้งแต่เปิดใช้งานเลยไหม?
สคริปต์ inline ใน WordPress theme ถูก CSP บล็อกทำอย่างไร?
CSP กับ HTTPS และ HSTS เกี่ยวข้องกันอย่างไร?
ทำไมรายงาน CSP ของฉันมีแต่การละเมิด?
CSP ป้องกัน SQL injection ได้ไหม?
ยังต้องเก็บ report-uri ไว้ไหม?
ทิ้งท้าย
จุดยากของ CSP ไม่ได้อยู่ที่การเขียนกฎ แต่อยู่ที่ความอดทนในการรันโหมดรายงาน ระยะสังเกตสองสัปดาห์แลกกับการควบคุมสคริปต์ได้อีกหลายปี คุ้มค่า
สามขั้นตอนลงมือทำ เริ่มจากเปิด Report-Only รัน 14 วัน จากรายงานนำโดเมนจริงมาใส่ whitelist ทีละโดเมน ใช้ hash แทน unsafe-inline พอการละเมิดลดลงเหลือหลักหน่วยค่อยสลับเป็นโหมดบังคับ พร้อมเก็บ report ไว้เฝ้าต่อ
ประโยคที่จำได้ง่ายๆ ระดับความปลอดภัยของ CSP ขึ้นอยู่กับว่า whitelist สะอาดแค่ไหน ถ้ายังเก็บ unsafe-inline และ wildcard ไว้ กำแพงนี้ก็เท่ากับไม่ได้ก่อ
