trusty — Website Trust Platform
Website Security

Content Security Policy (CSP) คืออะไร? คู่มือสำหรับธุรกิจ SaaS สตาร์ทอัพ และบริษัทเทคโนโลยี

Content Security Policy (CSP) คืออะไร สำหรับธุรกิจ SaaS สตาร์ทอัพ และบริษัทเทคโนโลยี — หลักการ ขั้นตอนตรวจสอบ และข้อผิดพลาดที่พบบ่อย สำหรับทีม Product, Engineering, Growth และ Privacy

📅 เผยแพร่ 16 กรกฎาคม 2569อัปเดตล่าสุด 12 สิงหาคม 2569✍️ เขียนโดย trusty Editorial Team⏱ อ่าน 9 นาที
Contemporary workspace featuring computers, coding screens, and office essentials in a tech environment.
ภาพโดย cottonbro studio จาก Pexels

💬 สรุปสั้น ๆ

สำหรับ SaaS ที่มีทั้ง marketing site, dashboard แบบ multi-tenant, widget ที่ลูกค้าฝังในเว็บของตัวเอง และสคริปต์ third-party จำนวนมาก (analytics, chat, billing) CSP ต้องถูกออกแบบเป็นหลาย policy แยกตามพื้นผิว ไม่ใช่ policy เดียวที่ใช้ทั้งระบบ และควรผูกเข้ากับ pipeline การ deploy เพื่อให้ nonce เปลี่ยนทุก build พร้อม frame-ancestors ที่รองรับทั้งฝั่งที่คุณ embed คนอื่น และฝั่งที่คนอื่น embed คุณ

ทีม Engineering ของ SaaS ส่วนใหญ่ไม่ได้กลัวว่า CSP จะตั้งค่ายาก แต่กลัวว่าจะตั้งแล้วฟีเจอร์บางอย่างพังโดยไม่รู้ตัว เพราะผลิตภัณฑ์ SaaS ทั่วไปไม่ได้มีแค่เว็บเดียว มันมี marketing site หนึ่งชุด มี dashboard ที่ลูกค้าล็อกอินเข้ามาใช้งานจริงอีกชุด บางบริษัทยังมี widget หรือ embed script ที่ลูกค้าเอาไปวางในเว็บของตัวเองอีกชั้นหนึ่ง แต่ละพื้นผิวเหล่านี้โหลดสคริปต์จากแหล่งที่ต่างกันโดยสิ้นเชิง

บทความนี้เขียนสำหรับทีม Product, Engineering, Growth และ Privacy ที่ต้องตัดสินใจร่วมกันว่าจะวาง CSP อย่างไรให้ตามทันความเร็วของการ ship ฟีเจอร์ใหม่ทุกสัปดาห์ โดยไม่กลายเป็นกำแพงที่ทีม Growth ต้องขอยกเว้นทุกครั้งที่อยากติดตั้งเครื่องมือใหม่

สแตกเครื่องมือของ SaaS ที่ทำให้ CSP ซับซ้อนกว่าเว็บทั่วไป

เว็บ SaaS ทั่วไปมักผูกอยู่กับเครื่องมือหลายชั้นพร้อมกัน เช่น product analytics อย่าง Segment, Amplitude หรือ PostHog ที่ยิง event จากทั้งหน้า marketing และหน้า dashboard, chat widget อย่าง Intercom หรือ Zendesk ที่ฝัง iframe ของตัวเอง, billing SDK อย่าง Stripe.js ที่ต้องโหลดจาก domain ของ Stripe โดยตรงตามเงื่อนไข PCI, และ CDN ที่เสิร์ฟ JS chunk จาก build ล่าสุดซึ่งชื่อไฟล์เปลี่ยนทุกครั้งที่ deploy

ถ้าเขียน policy แบบเปิดกว้างไว้ก่อนเพื่อความสะดวก เช่น อนุญาต script-src * ก็เท่ากับไม่มี CSP อยู่ดี แต่ถ้าล็อกแน่นเกินไปโดยไม่สำรวจให้ครบ ฟีเจอร์ chat หรือ billing อาจใช้งานไม่ได้ตั้งแต่วันแรกที่ deploy โดยไม่มีใครสังเกตจนลูกค้าแจ้งเข้ามา

  • แยกรายการ domain ตามหมวด: analytics, chat/support, payment, CDN ของตัวเอง, OAuth/SSO provider
  • ทำ inventory นี้เป็นเอกสารกลางที่ทีม Growth ต้องอัปเดตก่อนติดตั้งเครื่องมือใหม่ทุกครั้ง ไม่ใช่ให้ Engineering ไล่หาเองทีหลัง

Nonce ที่ผูกกับ CI/CD และปัญหาของ Feature Flag

Directiveค่าที่แนะนำสำหรับ SaaSเหตุผล
script-src'self' 'nonce-{build-id}' https://js.stripe.comnonce ต้องสุ่มใหม่ทุก build ไม่ใช่ค่าคงที่ที่ hardcode ไว้ใน template
connect-src'self' https://api.segment.io https://*.amplitude.comครอบคลุม endpoint ที่ SDK วิเคราะห์ข้อมูลยิงออกไปแบบ async
frame-src'self' https://js.stripe.com https://*.intercom.iowidget ที่ฝัง iframe ต้องเปิดเฉพาะ domain ที่ตรวจสอบแล้ว
frame-ancestorsขึ้นกับว่าเป็นหน้าไหน (ดูหัวข้อถัดไป)ต่างจาก 3 directive บนเพราะควบคุมทิศทางตรงข้าม

ปัญหาที่ SaaS เจอบ่อยกว่าธุรกิจทั่วไปคือ feature flag ผลิตภัณฑ์ใหม่มักมาพร้อมสคริปต์ทดลองจากผู้ให้บริการภายนอกที่ทีม Growth เปิดใช้เองผ่าน flag โดยไม่ผ่าน Engineering ก่อน วิธีที่ใช้ได้จริงคือกำหนดว่า flag ใดก็ตามที่โหลดสคริปต์จาก domain ใหม่ ต้องผ่านการเพิ่มเข้า policy ก่อนเปิด flag จริง ไม่ใช่เปิดแล้วค่อยแก้ทีหลัง

Multi-tenant และ Subdomain-per-customer: policy เดียวใช้ไม่ได้กับทุกที่

SaaS จำนวนมากให้ลูกค้าแต่ละรายมี subdomain ของตัวเอง เช่น customer-a.yourapp.com เพื่อแยกข้อมูลและปรับแต่ง branding บางส่วน ถ้าลูกค้าองค์กรบางรายอัปโหลดโลโก้หรือ custom CSS ของตัวเองผ่านหน้า settings ต้องตัดสินใจให้ชัดว่าไฟล์เหล่านั้นจะถูกโหลดจาก domain ของคุณเองหลังผ่านการตรวจสอบ หรือปล่อยให้ลูกค้าใส่ URL ภายนอกได้อย่างอิสระ เพราะตัวเลือกหลังทำให้ policy ที่ตั้งไว้อย่างรัดกุมเปิดช่องกว้างขึ้นทันทีในระดับ tenant เดียว

แนวทางที่ทีมส่วนใหญ่ใช้คือ upload asset ของลูกค้าผ่าน pipeline ที่ตรวจสอบไฟล์แล้วเสิร์ฟกลับผ่าน CDN ของตัวเองเสมอ เพื่อให้ policy หลักยังอ้างอิงเฉพาะ domain ที่ควบคุมได้ ไม่ต้องเปิดกว้างให้ URL ภายนอกที่ผู้ใช้กรอกเอง

เมื่อลูกค้าฝัง Widget ของคุณในเว็บของเขา

ผลิตภัณฑ์ SaaS สาย embeddable เช่น chat widget, booking widget หรือ analytics badge ที่ลูกค้าคัดลอกโค้ดไปวางในเว็บของตัวเอง ต้องคิด CSP สองทิศทางพร้อมกัน ทิศแรกคือ policy ของหน้า dashboard ที่ทีมคุณควบคุมเอง ทิศที่สองคือ frame-ancestors ของหน้า widget ที่จะถูกฝัง ซึ่งต้องอนุญาตให้ domain ของลูกค้าทุกรายฝังได้ แต่ยังกันไม่ให้เว็บอื่นที่ไม่เกี่ยวข้องฝัง widget ไปใช้ในทางที่ผิดวัตถุประสงค์ เช่น phishing page ที่ปลอมหน้าตาให้เหมือนใช้บริการจริง

ถ้า widget รองรับ custom domain ของลูกค้าองค์กร ควรมีระบบ allowlist domain ต่อ account แทนการเปิด frame-ancestors แบบกว้าง เพราะ CSP อย่างเดียวตรวจสอบแค่ต้นทาง ไม่ได้ยืนยันตัวตนบัญชีที่ฝังอยู่

Rollout แบบ Progressive: Report-Only บน Staging ก่อน Enforce บน Production

  1. เปิด Content-Security-Policy-Report-Only บนสภาพแวดล้อม staging และ preview deploy ของทุก pull request ก่อน
  2. ตั้ง report-uri หรือ report-to ให้ส่งเข้าระบบ log กลาง แล้วให้ทีม Engineering ไล่ดู violation ที่เกิดจาก third-party จริง ไม่ใช่ false positive จาก extension ของเบราว์เซอร์ผู้ทดสอบ
  3. เมื่อ violation จากสคริปต์ที่รู้จักหมดแล้ว ค่อยเปลี่ยนเป็น Content-Security-Policy แบบบังคับใช้จริงบน production โดยเริ่มจาก tenant กลุ่มเล็กก่อนขยายเต็มระบบ
  4. ผูกขั้นตอนนี้เข้ากับ CI/CD ให้ nonce เปลี่ยนอัตโนมัติทุก build และ fail การ deploy ถ้า policy syntax ผิดพลาด

ข้อดีของการแยก staging กับ production ออกจากกันคือทีม Growth ทดลองเครื่องมือใหม่บน staging ได้โดยไม่กระทบผู้ใช้จริง และทีม Engineering เห็น violation ล่วงหน้าก่อนที่ลูกค้าจะเจอปัญหา

ตัวอย่างสถานการณ์จริง: เมื่อ Dashboard พังหลัง Deploy เวอร์ชันใหม่

ทีม Engineering ของ SaaS รายหนึ่งย้ายระบบ build จาก webpack เวอร์ชันเก่าไปเป็น Vite เพื่อลดเวลา build จาก 8 นาทีเหลือ 90 วินาที ผลข้างเคียงที่ไม่มีใครคาดคิดคือชื่อไฟล์ JS chunk เปลี่ยนรูปแบบใหม่ทั้งหมด และ inline style ที่ Vite แทรกเข้ามาระหว่าง build ไม่ตรงกับ nonce เดิมที่ policy อ้างอิงอยู่ ผลคือหน้า dashboard โหลดไม่ขึ้นเลยทันทีที่ deploy เวอร์ชันใหม่ขึ้น production เพราะ CSP บล็อกทั้ง script chunk และ style ที่ inject มาใหม่

สิ่งที่ทำให้ทีมนี้แก้ปัญหาได้เร็วภายในหลักนาทีแทนที่จะเป็นหลักชั่วโมง คือมี dashboard เฝ้าดู CSP violation report แบบ real-time อยู่แล้ว ทำให้เห็นทันทีว่า violation เกิดจาก style-src ไม่ใช่ script-src ตามที่คาดไว้ตอนแรก และสามารถ rollback หรือ patch policy ให้รองรับ style nonce แบบใหม่ได้โดยไม่ต้องไล่ debug ทีละหน้า บทเรียนสำคัญคือทุกครั้งที่เปลี่ยน build tool หรือ bundler ต้องตรวจ CSP ควบคู่ไปกับการทดสอบฟังก์ชันอื่น ไม่ใช่แยกกันทดสอบทีหลัง

พร้อมตรวจสอบความน่าเชื่อถือของเว็บไซต์คุณหรือยัง?

ทดลองใช้งาน trusty ฟรี ไม่ต้องใช้บัตรเครดิต เริ่มสแกนได้ทันที

ทดลองใช้งานระบบฟรี

Audit CSP ในระบบที่มีหลาย Service และหลายทีมดูแล

SaaS ที่เติบโตเกินระดับสตาร์ทอัพเล็กมักแตกระบบเป็นหลาย service ที่แต่ละทีมดูแลเอง เช่น ทีม Billing ดูแลหน้า checkout แยกจากทีม Core Product ที่ดูแล dashboard หลัก และทีม Marketing ดูแล landing page แยกอีกชุด ถ้าไม่มีเจ้าภาพกลางที่ดู CSP ของทุก service พร้อมกัน แต่ละทีมมักตั้งค่ากันเองแบบไม่ประสานกัน บางทีมเปิดกว้างเกินไปเพราะรีบ ship ฟีเจอร์ บางทีมล็อกแน่นเกินจนขัดขวางเครื่องมือ tracking ที่บริษัทใช้ร่วมกันทั้งองค์กร

แนวทางที่ใช้ได้จริงคือกำหนด baseline policy กลางที่ทุก service ต้องมีอย่างน้อย (เช่น block object-src และ base-uri เป็น 'self' เสมอ) แล้วให้แต่ละทีมเพิ่มเติมเฉพาะส่วนที่ตัวเองต้องใช้ พร้อมทำ audit รอบไตรมาสโดยดึง policy จริงจากทุก service มาเทียบกับ baseline เพื่อจับ config ที่หลุดจากมาตรฐานตั้งแต่เนิ่น ๆ ก่อนที่จะกลายเป็นช่องโหว่ที่ไม่มีใครสังเกต

ความสัมพันธ์ระหว่าง CSP กับ OAuth/SSO และหน้า Login ของ Enterprise Customer

ลูกค้าองค์กรขนาดใหญ่ของ SaaS มักขอให้รองรับ SSO ผ่าน SAML หรือ OAuth ของผู้ให้บริการที่ตัวเองใช้อยู่แล้ว เช่น Okta, Azure AD หรือ Google Workspace ซึ่งหมายความว่าหน้า login ต้อง redirect ไปมาระหว่าง domain ของคุณกับ domain ของผู้ให้บริการ identity เหล่านั้น การตั้ง connect-src และ form-action ให้ครอบคลุมทุก provider ที่ลูกค้าอาจใช้ล่วงหน้าไม่ได้เสมอไป เพราะลูกค้าองค์กรใหม่แต่ละรายอาจมาพร้อม identity provider ที่ต่างกัน

วิธีที่ทีม Engineering หลายแห่งใช้คือแยก policy ของหน้า login/SSO ออกจากหน้า dashboard หลัก แล้วเปิด form-action ให้ครอบคลุม pattern ของ identity provider ที่พบบ่อยไว้ล่วงหน้า พร้อมมีกระบวนการเพิ่ม provider ใหม่เข้า policy ภายในเวลาที่กำหนดเมื่อฝ่ายขายปิดดีลกับลูกค้าองค์กรที่ใช้ provider ที่ยังไม่รองรับ เพื่อไม่ให้กระบวนการ onboarding ลูกค้ารายใหญ่ต้องหยุดรอทีม Engineering

เช็กลิสต์ปฏิบัติ

  • ทำ inventory เครื่องมือ third-party แยกตามหมวด analytics, chat, payment, CDN, OAuth และอัปเดตทุกครั้งที่ทีม Growth ติดตั้งใหม่
  • ผูก nonce เข้ากับ build id ใน CI/CD ให้เปลี่ยนอัตโนมัติทุก deploy ไม่ hardcode ไว้ใน template
  • แยก policy ของ marketing site, dashboard และหน้า widget ที่ถูกฝัง ออกจากกันตามพื้นผิวการใช้งานจริง
  • เสิร์ฟไฟล์ที่ลูกค้าอัปโหลด (โลโก้ CSS ที่ปรับแต่งได้) ผ่าน CDN ของตัวเองเสมอ แทนการอนุญาต URL ภายนอกที่ผู้ใช้กรอก
  • เปิด Report-Only บน staging และ preview deploy ก่อนบังคับใช้จริงบน production ทุกครั้งที่แก้ policy
  • กำหนดกระบวนการว่า feature flag ที่โหลดสคริปต์จาก domain ใหม่ ต้องเพิ่มเข้า policy ก่อนเปิดใช้งานจริง

ข้อผิดพลาดที่พบบ่อย

  • ทีม Growth เปิด feature flag ที่โหลดสคริปต์จาก vendor ใหม่โดยไม่แจ้ง Engineering ทำให้ฟีเจอร์นั้นถูก CSP บล็อกเงียบ ๆ จนลูกค้าแจ้งปัญหาเข้ามาก่อน
  • ใช้ nonce ค่าเดียวกันซ้ำในหลาย build เพราะ hardcode ไว้ในโค้ด ทำให้ผู้โจมตีที่หาค่านั้นเจอสามารถนำไปใช้ข้าม request ได้
  • เปิด frame-ancestors กว้างเกินไปสำหรับหน้า widget โดยไม่มี allowlist ต่อ account ทำให้เว็บอื่นฝัง widget ไปใช้นอกเหนือวัตถุประสงค์
  • อนุญาตให้ลูกค้าใส่ URL ภายนอกสำหรับโลโก้หรือ custom CSS ได้อย่างอิสระ จนกลายเป็นช่องโหว่เฉพาะ tenant ที่ policy หลักมองไม่เห็น

สรุป

CSP สำหรับ SaaS ไม่ใช่ policy เดียวที่ตั้งครั้งเดียวจบ แต่เป็นระบบที่ต้องแยกตามพื้นผิว (marketing, dashboard, widget) ผูกเข้ากับ pipeline การ deploy และมีกระบวนการให้ทีม Growth กับ Engineering คุยกันก่อนติดตั้งเครื่องมือใหม่ทุกครั้ง เริ่มจาก Report-Only บน staging เสมอ แล้วค่อยขยายไป production เมื่อมั่นใจว่า violation ที่เห็นเป็นของจริง ไม่ใช่ผลจากสคริปต์ที่จำเป็นต่อการทำงาน

ทีม Product, Engineering, Growth และ Privacy ที่ทำงานร่วมกันตั้งแต่ขั้นตอนวางแผน มักตั้ง CSP ได้ทันความเร็วของการ ship ฟีเจอร์ใหม่ทุกสัปดาห์ โดยไม่ต้องขอยกเว้น policy กันเป็นรายเครื่องมือทุกครั้งที่มีของใหม่เข้ามา

ดูภาพรวมเรื่องความปลอดภัยเว็บไซต์เพิ่มเติมได้ที่ ศูนย์ความรู้ด้าน Website Security หรือเปรียบเทียบแนวทางกับองค์กรที่มีความเสี่ยงสูงกว่าได้ที่ คู่มือ CSP สำหรับองค์กรการเงิน และหากต้องการตรวจสอบสถานะความปลอดภัยเว็บไซต์ของคุณตอนนี้ ลองใช้ เครื่องมือ Website Trust Scan

แหล่งข้อมูลอ้างอิง

คำถามที่พบบ่อย

ทำไมเว็บ SaaS ต้องแยก CSP เป็นหลาย policy แทนที่จะใช้ policy เดียว

เพราะ marketing site, dashboard และหน้า widget ที่ลูกค้าฝัง โหลดสคริปต์จากแหล่งต่างกันโดยสิ้นเชิง การใช้ policy เดียวมักบังคับให้เปิดกว้างเกินจำเป็นในบางพื้นผิวเพื่อไม่ให้อีกพื้นผิวพัง

nonce ควรผูกกับอะไรใน CI/CD

ควรผูกกับ build id หรือ deploy id ที่สุ่มใหม่ทุกครั้ง แทนการ hardcode ค่าคงที่ไว้ใน template เพราะ nonce ที่ใช้ซ้ำจะลดประโยชน์ของการป้องกันลงอย่างมาก

ควรอนุญาตให้ลูกค้าใส่ URL โลโก้หรือ custom CSS ของตัวเองไหม

ควรเสิร์ฟผ่าน CDN ของตัวเองหลังผ่านการตรวจสอบไฟล์ แทนการอนุญาต URL ภายนอกที่ลูกค้ากรอกเอง เพราะเปิดช่องโหว่เฉพาะ tenant ที่ policy หลักมองไม่เห็น

widget ที่ลูกค้าฝังในเว็บของเขาต้องตั้ง frame-ancestors อย่างไร

ควรมี allowlist domain ต่อ account แทนการเปิดกว้างให้ทุกเว็บฝังได้ เพราะ CSP อย่างเดียวตรวจสอบแค่ต้นทาง ไม่ได้ยืนยันตัวตนบัญชีที่ฝังอยู่

feature flag เกี่ยวข้องกับ CSP อย่างไร

feature flag ที่เปิดใช้สคริปต์จาก vendor ใหม่โดยไม่ผ่าน Engineering ก่อน มักถูก CSP บล็อกโดยไม่มีใครสังเกตทันที ควรกำหนดให้เพิ่มเข้า policy ก่อนเปิด flag จริงเสมอ

เนื้อหานี้จัดทำขึ้นเพื่อให้ข้อมูลทั่วไปเท่านั้น ไม่ถือเป็นคำแนะนำทางกฎหมาย กรุณาปรึกษาผู้เชี่ยวชาญด้านกฎหมายหรือ Data Protection Officer ของหน่วยงานท่านก่อนนำไปปฏิบัติจริง

พร้อมตรวจสอบความน่าเชื่อถือของเว็บไซต์คุณหรือยัง?

ทดลองใช้งาน trusty ฟรี ไม่ต้องใช้บัตรเครดิต เริ่มสแกนได้ทันที