วิธีการตั้งค่า Content Security Policy (CSP) ให้เว็บ ฉบับมือโปร

TLDR

  • Content Security Policy (CSP) คือ HTTP Response Header ที่เจ้าของเว็บสามารถกำหนดไว้ที่เว็บเซิร์ฟเวอร์ได้ ซึ่งจะช่วยในการลดความเสี่ยงการถูกโจมตีด้วยช่องโหว่แนว Client Side อย่าง XSS
  • เมื่อเว็บเบราว์เซอร์เปิดหน้าเว็บนั้นที่มีการใช้ CSP จะทำให้เว็บเบราว์เซอร์ทำตามนโยบาย ที่กำหนดเอาไว้ว่าให้หน้าเว็บนั้น ยอมดาวน์โหลดเนื้อหาต่าง ๆ ไม่ว่าจะเป็นรูป หรือโค้ด JavaScript (และอื่น ๆ อีกมากมาย) ใด ๆ มาจาก URL ไหนได้บ้าง รวมถึงการป้องกันการใช้ inline JavaScript หรือการยอมให้ใช้แต่มีการตรวจสอบค่า ว่าไม่ได้ถูกแก้ไขมาโดยแฮกเกอร์
  • ตัวอย่างค่า CSP: 
    Content-Security-Policy: default-src https://example.com
  • สิ่งสำคัญคือ CSP ไม่ควรถูกใช้เพื่อป้องกันช่องโหว่ XSS เพียงอย่างเดียว แต่ควรป้องกัน XSS โดยการตรวจสอบค่าที่มีการรับมาจากผู้ใช้งาน และการตรวจสอบค่าก่อนนำไปแสดงผลบนหน้าเว็บไซต์ เพราะ CSP มีไว้สำหรับช่วยลดความเสี่ยงเฉย ๆ เนื่องจากการแก้ไขช่องโหว่ XSS ทุกจุดให้ได้ 100% เป็นไปได้ยาก ดังนั้น จึงควรมี CSP เป็นการป้องกันชั้นที่ 2 เผื่อการป้องกันชั้นแรกของเว็บไซต์โดนเจาะ
  • นอกจากช่วยลดความเสี่ยงการถูกโจมตีด้วย XSS จริง ๆ CSP มีประโยชน์อื่น ๆ อีก เช่น การช่วยแจ้งเตือนว่าเว็บไซต์มีช่องโหว่ XSS รวมถึงการกำหนดนโยบายของเนื้อหาบนเว็บให้มาจากแหล่งที่มา ที่เรากำหนดไว้เท่านั้น

CSP คืออะไร? ทำไมควรมี?

Content Security Policy (CSP) คือ HTTP Response Header ที่เจ้าของเว็บสามารถกำหนดไว้ที่เว็บเซิร์ฟเวอร์ได้ ซึ่งจะช่วยในการลดความเสี่ยงการถูกโจมตีด้วยช่องโหว่แนว Client Side อย่าง XSS

ทุกคนที่หลุดเข้ามาในบทความนี้ อาจจะเคยเขียนเว็บไซต์กันมาบ้างแล้วใช่ไหม ถ้าใช่ ! คุณมาถูกที่แล้ว สำหรับคนที่ยังไม่เคยเขียนก็อ่านได้ด้วยนะ มันเป็นความรู้ที่อาจจะเข้าใจยากหน่อย แต่ไม่ยากเกินความสามารถคุณหรอก คืองี้ ๆๆ นะ ทุกวันนี้เราทุกคนคงเห็นข่าวแฮกเกอร์โจมตีเว็บโน้น ขโมยข้อมูลเว็บนี้เยอะมาก ถ้าอยู่ ๆ วันหนึ่งเว็บที่ถูกโจมตี ดันเป็นเว็บของเราที่ถูกโจมตีล่ะ…

มันก็ไม่ดีใช่ไหมล่ะ นี่เลย เราขอเสนอ ทีวีไดเร็ค (ล้อเล่น)

เราจะมาเรียนรู้กันกับการลดความเสี่ยงการโจมตีผู้ใช้งานเว็บไซต์ของเรา จากช่องโหว่ยอดฮิตที่มุ่งหมายจะโจมตีผู้ใช้งานเว็บ

ก่อนอื่นต้องขอแยกก่อนว่าช่องโหว่เว็บอาจแบ่งออกได้เป็น 2 ประเภท

  1. ช่องโหว่ที่เน้นโจมตีเว็บไซต์เอง เรียกว่าช่องโหว่ฝั่ง Server Side เช่น SQL Injection, Broken Access Control และ Insecure Deserialization
  2. ช่องโหว่ที่เน้นโจมตี “ผู้ใช้งาน” เว็บไซต์ที่เปิดหน้าเว็บเบราว์เซอร์ขึ้นมา เรียกว่าช่องโหว่ฝั่ง Client Side เช่น Clickjacking และ Cross-Site Scripting (XSS) ซึ่งอยู่ใน OWASP Top 10 ปี 2021ในหัวข้อ Injection (ไม่ได้แยกออกมาเหมือนปี 2017 แล้ว)

สำหรับคนที่ไม่ทราบว่า XSS มันคืออะไรน่ะเหรอ มันก็คือ… การโจมตีผู้ใช้งานเว็บ โดยการฝังโค้ดภาษา JavaScript ลงไปหน้าเว็บเพจ ตัวอย่างเช่น ที่ช่องค้นหา หรือ ช่องกรอกข้อมูล ที่มีการนำค่าจากผู้ใช้งานกลับมาแสดง ก็ไม่รอดนะ) !!! จุดประสงค์ก็เพื่อหลอกให้คนที่เข้ามาถูกพาไปหน้าเว็บของแฮกเกอร์ หรือแอบขโมยข้อมูลของเราไปทำในสิ่งที่ละเมิดต่อตัวเราเอง หรือ บังคับให้เหยื่อเข้าไปกระทำการใด ๆ ที่เหยื่อไม่ต้องการ อย่างการเปลี่ยนรหัสผ่าน หรือ เพิ่มข้อมูลของแฮกเกอร์เข้าไปในระบบด้วยสิทธิ์ของบัญชีเหยื่อ 

โดยในบทความอาจจะไม่กล่าวถึงการโจมตีรูปแบบนี้มากนักแต่จะมาลดความเสี่ยงพวกแฮกเกอร์ใจร้ายด้วยวิธีการการสร้างข้อกำหนดที่เรียกว่า Content-Security-Policy (CSP) ขึ้นมาเป็นมาตรการอยู่บนเว็บไซต์ของเรา เหมือนการฉีดซิโนแวคมาเป็นมาตรการมาลดความเสี่ยงการติดโควิดนั่นเอง !

บอกเอาไว้ก่อนเลยนะ CSP ในหัวข้อนี้ไม่ใช่ วิธีการแก้ไขช่องโหว่อย่างถูกต้อง 100% เช่น ถ้าเว็บมีช่องโหว่ XSS ก็ควรมีการตรวจสอบข้อมูลก่อนจะนำมาแสดงผล แก้ไขโค้ดให้ถูกต้อง แต่ CSP จะมาช่วยเป็น “ส่วนเสริม” เพิ่มเติม ที่จะช่วยลดความเสี่ยงในอีกขั้น ถ้าหาก มีแฮกเกอร์ค้นพบช่องโหว่ XSS ที่เราไม่รู้มาก่อนและพยายามโจมตี CSP จะทำให้ แฮกเกอร์ที่เจอช่องโหว่ XSS นั้น แฮกสำเร็จได้ยากมากขึ้น เพราะเรามี CSP มาช่วยลดความเสี่ยงเอาไว้อยู่นั่นเอง

อ่านแล้วงง ๆ อยู่จะดูความหมายก็ยิ่งปวดหัว งั้นเราจะมาดูตัวอย่างง่าย ๆ กัน

จากภาพประกอบจะเห็นได้ว่ามี 4 ขั้นตอนดังนี้

  1. เมื่อผู้ใช้งานเว็บ เข้าสู่เว็บไซต์ https://example.com/index.html ในหน้าเว็บจะมีการดาวน์โหลดข้อมูลเพิ่มเติม (Resource) ไม่ว่าจะเป็น รูปประกอบเว็บ (.jpg, .png หรือ .gif) หรือไฟล์กำหนดหน้าตาเว็บ (CSS) ไฟล์โค้ด Javascript ที่ใช้ในเว็บ เพิ่มเติม
  2. แต่ช้าก่อน.. ถ้าหากมีการกำหนดค่า CSP เอาไว้ใน HTTP Response Header ของเว็บไซต์ จะมีการตอบค่านั้นกลับมา ตัวอย่างเช่น
HTTP/2 200 OK
Accept-Ranges: bytes
Vary: Accept-Encoding
Content-Type: text/javascript; charset=UTF-8
Content-Security-Policy: default-src https://example.com
  • จากนั้น ถ้าหากเว็บไซต์จะไปดาวน์โหลด Resource ที่ได้รับอนุญาตจาก CSP มานั้น ก็จะดาวน์โหลดได้ปกติ อย่างกรณีนี้ CSP กำหนดว่าให้ src (Source) หรือรูปแบบต้นทางของ Resource มาจากเว็บไซต์เดียวกันเท่านั้น (https://example.com) ทำให้ URL ดังต่อไปนี้ถูกดาวน์โหลดมาทำงานบนหน้าเว็บได้ปกติ
  • https://example.com/assets/css/file.css
  • https://example.com/assets/js/file.js
  • แต่ถ้าหากมีเอกสารหน้าเว็บนั้น มีการพยายามจะดาวน์โหลด Resource ที่ไม่ได้รับอนุญาตจาก CSP อย่างเช่น มาจาก URL ต้นทางทางไม่ได้กำหนดไว้ จะไม่สามารถดาวน์โหลดมาใช้งานบนเว็บได้ เพราะขัดต่อนโยบายของ CSP
  • https://hacker.sth.sh/assets/js/xss.js

รวมถึงในค่า default-src ในตัวอย่างนี้ จะไม่ยอมให้เว็บไซต์มีการใช้คำสั่ง JavaScript ในเอกสาร HTML เรียกว่า Inline JavaScript ที่ไม่ได้ถูกดาวน์โหลดมาจาก URL ที่กำหนดไว้อีกด้วย (ยกเว้นเราจะกำหนดใส่เพิ่มเติมว่า unsafe-inline)

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

โดยสรุปค่าของ CSP จะมีชื่อ Directive และ รูปแบบของต้นทางข้อมูล และคั่นด้วยเว้นวรรค

สำหรับวิธีการตั้งค่าเพื่อนำ CSP ไปใช้ คือการใส่ HTTP Response Header ซึ่งสามารถเลือกใส่ได้หลายจุดที่ทำให้เว็บไซต์ตอบ CSP กลับมาไม่ว่าจะเป็น

  • ใช้โค้ดของแอปพลิเคชัน ในการกำหนดค่า HTTP Response Header
  • ใช้โค้ดของแอปพลิเคชัน ในการกำหนด HTML tag ชื่อ meta และใส่ CSP เข้าไป เช่น <meta http-equiv=”Content-Security-Policy” content=”default-src ‘self'”>
  • ใช้ Load Balancer หรือ Reverse Proxy (Cloudflare Worker ก็ใส่ได้) 
  • ใช้การตั้งค่าของเว็บเซิร์ฟเวอร์เช่น Apache หรือ Nginx 

ตัวอย่างการกำหนดนโยบาย CSP ด้วยแก้นำไปแก้ไขในไฟล์การตั้งค่าของ Nginx

File: /etc/nginx/sites-enabled/sth.sh.conf

add_header Content-Security-Policy "default-src 'none'; script-src 'self' d.line-scdn.net www.google-analytics.com ajax.cloudflare.com static.cloudflareinsights.com www.googletagmanager.com; style-src 'self' 'unsafe-hashes' 'sha256-kx1mmB/yg4CpwLkky3QWbHeUFdZTsCtEdCcnNFIhY1w=' d.line-scdn.net fonts.googleapis.com; img-src 'self' d.line-scdn.net www.google-analytics.com; font-src 'self' fonts.gstatic.com; frame-src social-plugins.line.me; manifest-src 'self'; connect-src 'self'; require-trusted-types-for 'script';";

เมื่อเว็บไซต์พยายามทำอะไรที่ขัดต่อนโยบาย CSP เช่นการยิง AJAX ไปดึงข้อมูลจากเว็บ https://www.cyfence.com/ ที่ไม่ได้รับอนุญาต (เราอนุญาต connect-src เป็น self ในตัวอย่างด้านบน) ผ่านฟังก์ชันง่าย ๆ อย่าง fetch() ก็จะมีข้อความแสดงข้อผิดพลาดข้อใน Console ของเว็บเบราว์เซอร์ และการยิง AJAX นั้นจะไม่สำเร็จ

ตัวอย่างการตั้งค่า CSP ของเว็บดัง ๆ

จากด้านบนที่เราคุยกันไปว่า CSP คืออะไร ทีนี้เราจะมาลองดูตัวอย่างที่มีการใช้งานจริง ๆ จากเว็บไซต์ดัง ๆ ที่เราน่าจะรู้จักอย่างแน่นอน โดยตัวอย่างที่เราจะนำมาแสดงให้ทุกคนดูนั้น มาจากการตอบกลับของทางเซิร์ฟเวอร์ (HTTP Response) แต่การมานั่งอ่านเองมันคงยากใช่ไหม เรามีตัวช่วยนั่นคือ…

CSP Evaluator https://csp-evaluator.withgoogle.com/

โดยเจ้า CSP Evaluator จะทำการแปลค่า CSP ที่ถูกใช้งานออกมาทำให้เราทำความเข้าใจได้มากขึ้น พร้อมทั้งบอกความเสี่ยงจากการตั้งค่า CSP ที่ไม่รัดกุมพอ (ถ้ามี) ที่อาจจะสามารถปรับปรุงให้ปลอดภัยเพิ่มขึ้นได้ ไปเริ่มดูตัวอย่างแรกกันเลย

Twitter.com

เมื่อเข้าสู่เว็บไซต์ https://www.twitter.com และดักอ่านค่า HTTP Response จะพบ CSP ดังนี้

(ใช้ Google Chrome กด F12 ไปที่ Network > Doc > Headers > Response Headers)

เมื่อนำออกมาเป็นข้อความบางส่วนจะได้ค่า CSP ดังนี้

HTTP/2 200 OK
[...]
Content-Security-Policy: connect-src 'self' blob: https://*.giphy.com https://*.pscp.tv https://*.video.pscp.tv https://*.twimg.com https://api.twitter.com https://api-stream.twitter.com https://ads-api.twitter.com https://aa.twitter.com https://caps.twitter.com 
[...]
object-src 'none'; script-src 'self' 'unsafe-inline' https://*.twimg.com https://recaptcha.net/recaptcha/ https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.google-analytics.com https://twitter.com https://app.link https://accounts.google.com/gsi/client https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js  'nonce-MmI4YzMzNmYtMzY2YS00NjM3LTg5NGYtNGI3NWQwMTcwNDcw'; style-src 'self' 'unsafe-inline' https://accounts.google.com/gsi/style https://*.twimg.com; worker-src 'self' blob:; [...]

เมื่อนำค่า CSP จากเว็บ Twitter.com ไปทดสอบที่เว็บ CSP Evaluator และกดปุ่ม “CHECK CSP” จะได้ผลออกมาดังนี้

จากตัวอย่างของ Twitter.com ตามภาพด้านบนเราจะมาพาลงรายละเอียดในแต่ละคำสั่งกัน

เมื่อเรานำ Directive บางส่วนมาจัดลงในตารางเพื่อให้มองได้ง่ายขึ้นจะเป็นแบบนี้

DirectiveValueSourceคำอธิบาย
default-src‘self’นโยบายของ CSP จะมีแยกย่อยหลาย Directive ถ้าหากอันไหนไม่ได้ถูกกำหนดไว้ จะมาอ้างอิงจากนโยบายของ default-src เป็นค่าเริ่มต้น (ถ้าถูกกำหนดไว้ก็จะใช้ของแต่ละอันนั้น ๆ ไปแทน)

โดยค่าเริ่มต้นของนโยบายในตัวอย่างนี้คือ self แปลว่า ยอมให้นำเนื้อหาเฉพาะจากเว็บเดียวกันเข้ามาใช้งานได้
connect-src‘self’ blob:https://api.twitter.com
[…]
จำกัดให้สามารถเรียกใช้งาน API ภายนอกได้เฉพาะจาก URL รูปแบบที่กำหนดเท่านั้น เช่น https://api.twitter.com และ เนื้อหารองรับรูปแบบ Blob
form-action‘self’https://twitter.com https://*.twitter.com;สามารถใช้ HTML tag อย่าง <form> ในการส่งข้อมูลไปที่เว็บเดียวกันและเว็บที่เป็น Subdomain เท่านั้นตามรูปแบบ *.twitter.com
frame-src‘self’https://twitter.com https://mobile.twitter.com 
[…]
https://www.gstatic.com/recaptcha/;
สามารถนำเว็บอื่นมาแสดงผลผ่าน HTML tag  <iframe> จาก Domain ที่กำหนดเท่านั้น เช่น https://mobile.twitter.com ถ้าหากเว็บไซต์พยายามจะนำเว็บอื่นนอกเหนือจากที่กำหนดมาแสดงผลผ่าน <iframe> จะไม่สามารถทำได้
img-src‘self’ blob: data:https://*.cdn.twitter.com
[…]
https://imgix.revue.co;
สามารถนำ URL ของรูปจาก Domain ที่กำหนดไว้มาแสดงได้ในเว็บไซต์เท่านั้น เช่น https://demo.cdn.twitter.com/demo.jpg จะสามารถนำมาแสดงได้แต่ https://*.cdn.hacker.com/hack.jpg จะไม่สามารถนำมาแสดงบนเว็บได้
script-src‘self’ ‘unsafe-inline’https://*.twimg.com
[…]
‘nonce-MmI4YzMzNmYtMzY2YS00Nj
M3LTg5NGYtNGI3NWQwMTcwNDcw’;
กำหนดให้เว็บไซต์สามารถรันคำสั่ง JavaScript ในรูปแบบไหน และจากที่ไหนได้บ้าง จากตัวอย่างของ Twitter.com คือ

ยอมให้รัน JavaScript แบบ Inline (อยู่ในเอกสาร HTML) ได้
แต่ต้องมีการสุ่มค่า (Nonce) แบบใช้ครั้งเดียว ใส่ในแต่ละ HTTP Request กับใน <script> เช่น <script nonce=”${RANDOM}”>[…]</script> และตรวจสอบว่าเป็นค่าเดียวกันเสมอ
สามารถรัน JavaScript จากลิงก์ภายนอกได้แต่ต้องมาจาก URL ที่กำหนดเท่านั้นเช่น https://cdn.twimg.com/file.js

พอจัดแบบนี้จะทำให้เรามองและแบ่งการทำงานได้ง่ายขึ้นก็จะมองไม่ยากแล้วล่ะ ที่สีแดงตรงคำว่า base-uri และมี [missing] เพราะมันไม่มีการใช้แต่เจ้า CSP Evaluator นี้มันแนะนำให้ว่าควรมี งั้นตัวอย่างต่อไป

Stackoverflow.com

HTTP/2 302 Found
[...]
Content-Security-Policy: upgrade-insecure-requests; frame-ancestors 'self' https://stackexchange.com
[...]

ในส่วนของ stackoverflow.com เว็บที่เป็นที่พึ่งสำคัญของเราชาวโปรแกรมเมอร์  มาดูสิว่ามีการใช้ CSP ทำอะไรกันบ้าง

DirectiveValueSourceคำอธิบาย
upgrade-insecure-requestsจะทำการเปลี่ยนการดาวน์โหลดเนื้อหา จาก HTTP เป็น HTTPS ที่มีการใช้การเข้ารหัสระหว่างส่งข้อมูล
frame-ancestors‘self’https://stackexchange.comอนุญาตให้ใช้ <iframe> <object> <embed> … ได้เฉพาะเนื้อหาที่มาจาก URL ของเว็บ https://stackexchange.com

จากด้านบน จะดูเหมือนว่าจะมีการระบุนโยบายของ CSP เพียงแค่ 2 รายการ 

หลังจากได้แนะนำมาหลายตัวแล้วหลาย ๆ คนอาจจะพอจับทางเกี่ยวกับการใช้ CSP ได้แล้ว เรายังเหลืออีกสองตัวอย่างให้ทุกคนได้ลองอ่าน แล้วทำความเข้าใจเตรียมสร้างของตัวเองได้แล้ว !!

Tiktok.com

HTTP/2 200 OK
[...]
Content-Security-Policy: script-src 'unsafe-inline' https: 'strict-dynamic' 'nonce-3BNT2xu0Gl-9UG06nKXfD' 'unsafe-eval';frame-src *.tiktok.com accounts.google.com;report-uri https://mon-va.byteoversea.com/log/sentry/v2/api/slardar/main/?ev_type=csp&bid=tiktok_web
[...]

Google.com

HTTP/2 200 OK
[...]
Content-Security-Policy: object-src 'none';base-uri 'self';script-src 'nonce-CR9WTLZQaU8wzESI85y93A==' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/cdt1
[...]

จะเห็นว่าจาก 2 ตัวอย่างนี้มีการใช้ Directive ชื่อ report-uri และระบุ URL เพื่อใช้ในการแจ้งเตือน ในกรณีที่มีผู้ใช้งานเว็บไซต์ พบว่ามีการละเมิดข้ออนุญาตของ CSP ซึ่งอาจจะหมายความว่า กำลังถูกโจมตีด้วยช่องโหว่ XSS นั่นเอง

ตัวอย่าง HTTP Request ที่จะแจ้งมา จากการใส่ Directive ชื่อ report-uri และมีการละเมิดนโยบาย CSP

POST / HTTP/1.1
Host: example.com
...
Content-Type: application/reports+json

[{
  "type": "security-violation",
  "age": 10,
  "url": "https://example.com/vulnerable-page/",
  "user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0",
  "body": {
    "blocked": "https://evil.com/evil.js",
    "policy": "bad-behavior 'none'",
    "status": 200,
    "referrer": "https://evil.com/"
  }
}, {

เราสามารถตรวจสอบได้นะว่าเจ้าเบราว์เซอร์ของผู้ใช้งานเว็บเรามีการรองรับ CSP จาก URL ด้านล่างนี้เลย  https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP#browser_compatibility

สรุปง่าย ๆ คือทุกเว็บเบราว์เซอร์รองรับ CSP ยกเว้น Internet Explorer (IE) คือสามารถตั้งค่า CSP แต่ IE จะไม่นำมาบังคับใช้งานนั่นเอง

เริ่มต้นการตั้งค่า CSP สำหรับผู้ดูแลเว็บไซต์ และสิ่งที่ควรระวังการตั้งค่า

จากหลาย ๆ ตัวอย่างที่เราที่เราได้ชมกันไป ทีนี้เราจะมาลองตั้งค่าด้วยตัวเองกัน ! เริ่มจากการรวบรวมรายชื่อ Domain ที่เว็บเราจะสามารถเชื่อใจให้มันสามารถดึงเนื้อหา รูปภาพ หรือ โค้ด เข้ามาใช้ได้ 

ตัวอย่างเช่น 

  • cdn.example.com
  • js.example.com
  • css.example.com
  • img.example.com
  • อื่น ๆ .example.com

ในส่วนของขั้นตอนตั้งค่านั้น เราสามารถตั้งค่าการทำงานของมันได้ 2 รูปแบบ ด้วยกัน ได้แก่ 

  • รูปแบบ  Blocking หรือ Enforcing – โดยเจ้ารูปแบบนี้จะทำการบังคับใช้นโยบายที่ถูกระบุอยู่ในค่าของ CSP ที่เรากำหนดไว้ และจะทำการแจ้งไปยัง report-uri (ถ้ามี)

ตัวอย่าง

Content-Security-Policy: default-src 'self';
  • รูปแบบ Report only – รูปแบบนี้เองจะแตกต่างจากรูปแบบที่แล้วคือเราจะไม่บังคับใช้นโยบาย CSP ที่เรากำหนดไว้ แต่ถ้าหาก การใช้งานเว็บไซต์ มีการทำผิดนโยบาย CSP จะทำการแจ้งไปที่ report-uri เพื่อให้ เจ้าของเว็บ ที่เพิ่งทดลองตั้งค่า CSP สามารถค่อย ๆ ปรับค่า CSP จนไม่กระทบต่อการใช้งานปกติของเว็บไซต์ได้นั่นเอง

ตัวอย่าง

Content-Security-Policy-Report-Only: default-src 'self';

ต่อมาจะเป็นตัวอย่างของ Directive ที่เราสามารถนำไปเป็นตัวเลือกในการตั้งค่าได้

Directiveคำอธิบายตัวอย่างการใช้งาน
default-srcนโยบายของ CSP จะมีแยกย่อยหลาย Directive ถ้าหากอันไหน ไม่ได้ถูกกำหนดไว้ จะมาอ้างอิงจากนโยบายของ default-src เป็นค่าเริ่มต้น (ถ้าถูกกำหนดไว้ก็จะใช้ของแต่ละอันนั้น ๆ ไปแทน)‘self’ https://*.example.com
script-srcกำหนดว่าเว็บไซต์จะสามารถรันคำสั่ง JavaScript ที่มาจาก URL ใดที่เรากำหนดได้เท่านั้นผ่าน HTML tag <script>‘self’ https://js.example.com https://ajax.example.com
style-srcกำหนดว่าเว็บไซต์จะสามารถนำคำสั่ง CSS จาก URL ของเว็บใดมาใช้งานได้บ้าง‘self’ https://css.example.com
object-srcกำหนดว่าเว็บไซต์จะสามารถนำไฟล์จาก URL ใดเข้ามาแสดงในเว็บไซต์ได้บ้าง เช่นการยอมให้เปิดไฟล์ PDF ของเว็บผ่านเว็บเบราว์เซอร์ ก็จะมาอ้างอิง object-src‘self’
img-srcกำหนด URL ที่ยอมให้ HTML tag <img> สามารถดาวน์โหลดรูปมาแสดงบนเว็บไซต์ได้‘self’ https://img.example.com
form-actionกำหนด URL ที่สามารถใช้ HTML tag อย่าง <form> ในการส่งข้อมูลไปที่เว็บปลายทางใด ๆ ได้เท่านั้น‘self’
report-uriกำหนดให้เบราว์เซอร์ ส่งรายงานความผิดพลาดของนโยบาย CSP ที่กำหนดไปยัง URI ของ Directive นี้ โดยสามารถใช้ Content-Security-Policy-Report-Only เป็น HTTP Request Header เพื่อสั่งให้เบราว์เซอร์ส่งเฉพาะรายงานเท่านั้น (ไม่ได้บังคับปิดกั้นการใช้งานที่ผิดนโยบาย)https://csp.example.com/csp
connect-srcใช้กับ XMLHttpRequest (AJAX), WebSocket, fetch() หรือ EventSource หากเว็บปลายทางไม่ได้รับอนุญาตใน Directive connect-src เว็บเบราว์เซอร์จะตอบกลับสถานะเป็น HTTP 400 และไม่ยอมให้อ่านข้อมูลมาใช้งาน‘self’

และตัวอย่าง Directive อื่น ๆ สามารถไปอ่านเพิ่มเติมเพื่อนำมาปรับใช้ ได้ตาม URL ด้านล่างนี้เลย https://content-security-policy.com/  

ส่วนถัดไปจะเป็นส่วนของตัวอย่างการกำหนด ค่า Value ของแต่ละ Directive

Valueคำอธิบายตัวอย่างการใช้งาน
*เป็น Wildcard โดยจะอนุญาต URL ทั้งหมดยกเว้น data: blob: filesystem: และ schemes:img-src *
‘none’ป้องกันการนำ เนื้อหา รูปภาพหรือโค้ด จากทุก URLobject-src ‘none’
‘self’อนุญาตให้นำ เนื้อหา รูปภาพหรือโค้ดจาก URL ต้นทางเท่านั้น (จาก host, scheme เดียวกัน หรือ port)script-src ‘self’
‘unsafe-inline’อนุญาตให้ HTML Tag หรือ JavaScript Tag ได้เท่านั้นdefault-src ‘unsafe-inline’
‘nonce-<base64 value>’การป้องกัน Inline Script Tag ด้วยการสุ่มค่าที่เข้ารหัสแบบใช้ครั้งเดียวเพื่อยืนยันว่าไม่ใช่ Inject Script (รองรับเบราว์เซอร์แบบใหม่)script-src ‘nonce-dGVzdA==’
data:อนุญาตให้นำ เนื้อหา รูปภาพหรือโค้ดที่เข้ารหัสเท่านั้น (Base 64)img-src ‘self’ data:
blob:อนุญาตแสดงข้อมูลที่นำมา ในรูปแบบข้อมูลที่ไม่เป็นข้อความภาษาอังกฤษปกติ รวมถึงข้อมูลที่อาจไม่สามารถแสดงเป็น ASCII ได้ เช่นเป็น Binaryconnect-src ‘self’ blob:
domain.example.comอนุญาตให้นำ เนื้อหา รูปภาพหรือโค้ดจาก Domain ที่กำหนดไว้เท่านั้นimg-src domain.example.com
*.example.comอนุญาตให้นำ เนื้อหา รูปภาพหรือโค้ดจาก Subdomain นี้เท่านั้นimg-src *.example.com
https://cdn.comอนุญาตให้นำ เนื้อหา รูปภาพหรือโค้ดจาก URL ที่กำหนดไว้และเป็น Https Protocol เท่านั้นimg-src https://cdn.com
https:อนุญาตให้นำ เนื้อหา รูปภาพหรือโค้ดจาก URL ใดก็ได้ที่เป็น Https Protocol เท่านั้นimg-src https:

และตัวอย่าง Value อื่น ๆ สามารถไปอ่านเพิ่มเติมเพื่อมาปรับใช้ ได้ตาม URL ด้านล่างนี้เลย

https://content-security-policy.com ในหัวข้อ Source List Reference

แต่การสร้างเจ้า CSP จากที่อ่านมาบางคนอาจจะคิดว่า… เออแล้วจะใช้อะไรบ้างดีล่ะ เราก็มีตัวช่วยด้วยเช่นกัน แท่นแท๊น

https://github.com/fcsonline/autocsp

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

ทีนี้มาในส่วนที่ต้องระวังกันบ้าง

สิ่งที่ควรระวัง

  • การนำ CSP ไปตั้งค่าทันที ถ้าหากตั้งค่าผิดพลาด อาจจะทำให้เว็บไซต์ไม่สามารถใช้งานได้ เช่นมีการนำ JavaScript จาก URL ภายนอกมาใช้ แต่ URL นั้นไม่ได้ถูกอนุญาตโดย CSP ที่กำหนดไว้ ดังนั้น ควรทำการทดสอบแบบ แจ้งเตือน report-uri ด้วย HTTP Response Header ชื่อ Content-Security-Policy-Report-Only ก่อนเริ่มใช้งาน Content-Security-Policy
  • ถ้าหากเว็บไซต์มีการใช้ Inline JavaScript ไม่ควรกำหนด CSP เป็น ‘unsafe-inline’ โดยไม่มี Nonce เพราะจะทำให้การป้องกันของ CSP แทบไม่มีประโยชน์ใด ๆ
  • อย่ากำหนด Nonce แบบคงที่ ต้องใช้เป็นค่าสุ่มเสมอ
  • ควรมีการใช้ Directive default-src เพื่อเป็นค่าเริ่มต้น เพราะอาจจะลืมตั้งค่าบาง Directive
  • ถ้าหากมีการพัฒนาเว็บไซต์ใหม่ ควรมีการกำหนดให้เลี่ยงการใช้งาน Inline JavaScript โดยใช้การนำมาจาก URL เป็น .js ที่กำหนดใน script-src แทน เพื่อความปลอดภัยสูงสุด 
  • การใช้ Value * คือ Wildcard ถ้าเรามี Domain ที่กำหนดอยู่ และมี Wildcard ด้วย อาจกลายเป็นว่าเรายอมรับ Domain อะไรก็ได้ขัดกับเงื่อนไข Domain ที่เรากำหนดอย่างเจาะจงลงไปก่อนหน้านั้น
  • ตอนนี้ CSP รองรับเบราว์เซอร์เกือบทุกรุ่นแล้ว ยกเว้น IE ถ้าหากผู้ใช้งานหลักของเว็บไซต์เป็น IE ควรพิจารณาว่า การลดความเสี่ยงที่เพิ่มด้วย CSP อาจไม่ได้ผลอย่างที่คิด

ตัวอย่างการแฮกเว็บ ที่ CSP สามารถป้องกันได้

มา ๆ หลังจากลองตั้งค่าการป้องกันเว็บตัวเองเรียบร้อยแล้ว เดี๋ยวเราจะมาลองดูเปรียบเทียบกันระหว่าง การป้องกันกับไม่ป้องกันมันแตกต่างกันอย่างไร !

ตัวอย่างไฟล์ HTML ที่มีช่องโหว่ XSS เนื่องจากรับค่า HTTP Parameter ชื่อ q นำมาแสดงเป็นข้อความบนเว็บผ่านฟังก์ชัน innerHTML()

File: index.html

<html>
<head>
	<title>Cross-Site Scripting (XSS)</title>
</head>
<body>
	<form action="" method="GET">
		<input type="text" name="q">
		<input type="submit" value="Search">
	</form>
	<div id="results"></div>
	<script>
		document.addEventListener('DOMContentLoaded', function() {
			var q = getQueryParameter('q');
			if (q) {
				search(q, function(error, results) {
					showQueryAndResults(q, results);
				});
			}
		});
		function search(q, callback) {
			var results = [
				'Result #1',
				'Result #2',
				'Result #3'
			];
			callback(null, results);
		}
		function showQueryAndResults(q, results) {
			var resultsEl = document.querySelector('#results');
			var html = '';
			html += '<p>Your search query:</p>';
			html += '<pre>' + q + '</pre>';
			html += '<ul>';
			for (var index = 0; index < results.length; index++) {
				html += '<li>' + results[index] + '</li>';
			}
			html += '</ul>';
			resultsEl.innerHTML = html;
		}
		function getQueryParameter(name) {
			var pairs = window.location.search.substring(1).split('&');
			var pair;
			for (var index = 0; index < pairs.length; index++) {
				pair = pairs[index].split('=');
				if (decodeURIComponent(pair[0]) === name) {
					return decodeURIComponent(pair[1]);
				}
			}
			return false;
		}
	</script>
</body>
</html>

ลองใส่คำสั่ง HTML ที่พยายามจะรันคำสั่ง JavaScript ผ่าน Event ชื่อ onerror เมื่อหารูปไม่เจอ เพื่อจำลองสถานการณ์การโจมตีด้วย XSS

<img src="does-not-exist" onerror=alert(/XSS/)> 

เข้าไปที่ URL ดังนี้

http://เว็บไซต์จำลองช่องโหว่/?q=%3Cimg%20src%3D%22does-not-exist%22%20onerror%3Dalert(/XSS/)%3E

จะพบว่าเมื่อไม่มีการป้องกันช่องโหว่ XSS และไม่มี CSP จะทำให้คำสั่ง JavaScript ที่ถูกใส่เข้ามาถูกสั่งให้ทำงาน ในตัวอย่างนี้ จะเป็นเพียง กล่องข้อความธรรมดา แต่ในสถานการณ์โจมตีจริง แฮกเกอร์สามารถใส่คำสั่ง JavaScript ที่บังคับให้เหยื่อ ทำการเปลี่ยนรหัสผ่าน หรือเพิ่มบัญชี เพิ่มข้อมูลแปลกปลอมเข้าไปในระบบด้วย Session Token ของเหยื่อที่เข้าสู่ระบบและโดนบังคับให้รันคำสั่ง JavaScript นั้น ๆ ได้

ต่อมาเมื่อเราลองเพิ่ม CSP เข้าไป ในตัวอย่างนี้จะเป็นผ่าน HTML tag ชื่อ <meta> แต่จะมีผลคล้ายกับการใส่ผ่าน HTTP Response Header นั่นเอง

<meta http-equiv="Content-Security-Policy"content="default-src 'none';">

จำได้ไหมครับว่ามันหมายถึงอะไร ติ๊กต่อก ติ๊กต่อก … มันคือการที่เราจะไม่อนุญาตให้รัน JavaScript จาก URL ใด ๆ เลยรวมถึง Inline JavaScript งั้นลอง ทดสอบการโจมตีด้วย XSS อีกครั้ง จากการเข้าไปที่ URL เดิมแล้วมาดูผลกัน

จะเห็นว่าคำสั่ง JavaScript ที่ถูกใส่เข้าไป ขณะโจมตีช่องโหว่ XSS ไม่ทำให้ถูกทำงานแล้ว และยังมีการแจ้งเตือนใน Console ของเว็บเบราว์เซอร์ (กด F12) ว่าการรันคำสั่ง JavaScript ดังกล่าว ผิดนโยบายของ CSP

Refused to execute inline script because it violates the following Content Security Policy directive: "default-src 'none'". Either the 'unsafe-inline' keyword, a hash ('sha256-rnzwOP3Njo2xrQ/8bm4Jwn2Lv+xyEbWKZGM8Xfr2pEc='), or a nonce ('nonce-...') is required to enable inline execution. Note also that 'script-src' was not explicitly set, so 'default-src' is used as a fallback.

งั้นมาลองแบบอื่นกัน คราวนี้เรากำหนด CSP แต่เป็นนโยบายที่ไม่ปลอดภัย ยอมให้รันคำสั่ง สั่งการ Inline JavaScript ได้

<meta http-equiv="Content-Security-Policy"content="default-src 'unsafe-inline';">

จะเห็นว่าคำสั่ง XSS ที่ถูกแฮกเกอร์ใส่เข้ามายังสามารถทำงานได้แม้จะมีการตั้งค่า CSP (ที่ไม่ปลอดภัย) แล้วก็ตาม ดังนั้น การแก้ไขช่องโหว่ Client Side ต่าง ๆ ยังควรแก้ไขตามปกติ แต่การตั้งค่า CSP เป็นส่วนเสริมที่จะมาช่วยลดความเสี่ยงให้ช่องโหว่ที่ถ้าหากมีการค้นพบในอนาคต 

ทีนี้มาวิเคราะห์กันว่า เอ๊ะ ถ้าเราจะเอา CSP ที่ป้องกัน XSS จากตัวอย่างก่อนหน้านี้มาใช้

<meta http-equiv="Content-Security-Policy"content="default-src 'none';">

เราจะต้องไม่ยอมให้เว็บของเรารันคำสั่ง JavaScript ใด ๆ เลย (default-src เป็น fallback กรณีที่เราไม่ได้กำหนด script-src) แต่ในความเป็นจริง เราอาจจะต้องปรับให้ยอมรันคำสั่ง JavaScript หรือนำรูปหรือนำข้อมูลจากไฟล์ ภายนอกในส่วนที่น่าเชื่อถือได้มาใช้งานในเว็บด้วย อย่างกรณีนี้่เราสามารถปรับใหม่ได้เป็น

<meta http-equiv="Content-Security-Policy"content="default-src 'none'; script-src 'self';">

จากนั้นถ้าหากมีการใช้งานคำสั่ง JavaScript ก็ให้สร้างเป็นไฟล์ .js และเก็บไว้ในเว็บ (Origin) เดียวกัน (self ตามค่าใน script-src) เช่น

<script src="/main.js"></script>

แท่นแท๊นน เราสามารถป้องกันการใช้งาน Inline JavaScript และการโจมตีช่องโหว่ XSS ในกรณีนี้ได้แล้ว

นี่ก็เป็นตัวอย่างความสามารถในการป้องกันคร่าว ๆ ของ CSP ทุกคนคงเห็นภาพการทำงาน แล้วใช่ไหม ถ้างั้นเรามาลองดูอีกมุมดูว่าถ้าหากป้องกันแล้ว แต่ก็ยังโดนโจมตีได้จะเป็นอย่างไรในหัวข้อถัดไป

กรณีศึกษา การตั้งค่า CSP แล้วที่ยังสามารถโดนแฮกได้

การป้องกันด้วยการทำ CSP เป็นการป้องกันการเกิดช่องโหว่ XSS ได้บางส่วน แต่ ทุกระบบความปลอดภัยอาจมีช่องโหว่ที่เราไม่คาดคิดมาก่อนได้

หลายคนอาจสงสัย “อ้าว ถ้าป้องกันแล้วยังโดนมันจะปลอดภัยได้อย่างไรล่ะ !!!” แต่เดี๋ยวฟังก่อน ทุกการป้องกันย่อมดีกว่าการไม่ป้องกันอยู่แล้ว อย่างน้อยก็ลดโอกาสที่จะเกิดได้มากขึ้นไงล่ะ ตัวอย่างเช่น ถ้าบ้านหลังที่ 1 มีรั้วกับมี รปภ. คอยดูแลอยู่กับบ้านหลังที่ 2 ไม่มีการป้องกันใด ๆ เลย ถ้าคุณอยากจะไปลองงัดแงะ จะไปบ้านหลังไหน…ติ๊กต่อก…ติ๊กต่อก…โอเคเห็นภาพแล้วใช่ไหม ! งั้นไปลองดูกัน

ทีนี้ในหัวข้อนี้เราจะมานำเสนอตัวอย่างการป้องกันด้วย CSP ที่เหมือนจะทำถูกต้องแล้ว แต่ยังสามารถถูกแฮก เพื่อข้ามผ่านหรือหลบหลีกการป้องกัน (Bypass) ไปได้อยู่เป็นกรณีศึกษา สำหรับการโจมตี และเพื่อปรับปรุงการป้องกันให้ปลอดภัยยิ่งขึ้น ! 

มาลองดูกันกับตัวอย่างเว็บด้านล่างนี้ จากการแข่งขัน BSidesSF 2021 CTF ที่รับค่าจากผู้ใช้งานและนำมาแสดงผลทันที ซึ่งเรียกได้ว่ามีช่องโหว่ XSS นั้นเอง แต่ เจ้าของเว็บก็ได้ลดความเสี่ยงของการถูกโจมตีเอาไว้

ตัวอย่างหน้าเว็บ

โดยการกำหนดค่า CSP ดังนี้ 

HTTP Response Header:

HTTP/1.1 200 OK
Date: Tue, 28 Dec 2021 11:58:55 GMT
Server: Apache
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Security-Policy: script-src 'self' cdnjs.cloudflare.com 'unsafe-eval';img-src http://i.imgur.com; default-src 'self' 'unsafe-inline' ; connect-src *; report-uri /csp_report
Vary: Accept-Encoding
Content-Length: 505
Connection: close
Content-Type: text/html; charset=UTF-8

จากนั้นมาลองโจมตี XSS ด้วยการใส่คำสั่ง JavaScript ปกติกัน ด้วยการใส่ค่าต่อไปนี้เข้าไปในช่องค้นหา

<script>alert('/XSS/')</script>

จะเป็นการเข้าไปที่ URL จำลองสำหรับการทดสอบดังต่อไปนี้ (ให้สังเกตที่ค่า HTTP Parameter ชื่อ xss)

http://เว็บไซต์จำลองช่องโหว่

จะพบว่าเราไม่สามารถรันคำสั่ง JavaScript ได้เพราะว่า script-src กำหนดไว้ว่ายอมให้รัน JavaScript ได้เฉพาะ

  • ‘self’ คือจาก Origin เดียวกัน
  • cdnjs.cloudflare.com คือจากเซิร์ฟเวอร์ของ Cloudflare ที่เราควบคุมไม่ได้
  • ‘unsafe-eval’ ยอมให้รันคำสั่ง Inline JavaScript ฟังก์ชัน eval() หรือที่คล้ายกันได้ (ภายใต้เงื่อนไข 2 ข้อแรก)

แล้วเราจะต้องทำอย่างไร? หรือเราจะต้องแฮกเซิร์ฟเวอร์ Cloudflare เข้าไปวางไฟล์ JavaScript ในเว็บ cdnjs.cloudflare.com เพื่อที่จะโจมตีช่องโหว่ XSS หรือเปล่า?

แนวคิดวิธีการแฮกครั้งนี้คือ 

  • เนื่องจาก Cloudflare ให้บริการรับฝากไฟล์ JavaScript ให้เว็บอื่นใด ๆ เข้ามาเรียกไปใช้งานได้
  • ในไฟล์ JavaScript Framework ที่ Cloudflare เรียกใช้นั้น อาจจะมีคำสั่ง JavaScript ที่แฮกเกอร์ใช้ประโยชน์ในการโจมตีช่องโหว่ XSS ได้
  • โดยในกรณีนี้เราจะมาอาศัยประโยชน์จาก Angular Framework รุ่น 1.0.8 ที่ยอมให้เราสามารถใช้ HTML Tag อย่าง <div> ในการรันคำสั่ง JavaScript ได้ และไม่ติดข้อจำกัดของ CSP นั้นเอง!

แฮกเกอร์ สามารถที่จะใช้โค้ด HTML ดังต่อไปนี้เพื่อ

  • นำไฟล์ JavaScript 2 ไฟล์คือ Angular กับ Prototype จาก cdnjs.cloudflare.com ที่ CSP อนุญาต เข้ามาใช้งาน
  • ใช้ Angular Template ในการรันคำสั่ง JavaScript ด้วยเครื่องหมาย {{ }} ใน HTML Tag <div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.0.8/angular.js" /></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prototype/1.7.2/prototype.js" /></script>
<div ng-app ng-csp>
  {{$on.curry.call().alert('/XSS/')}}
</div>

เมื่อใส่คำสั่งโจมตีช่องโหว่ XSS นี้เข้าไป ก็จะทำให้สามารถรันโค้ด JavaScript ได้แม้ว่าจะมีการป้องกันช่องโหว่ XSS ด้วยการใส่ CSP แล้วก็ตาม !!

สรุป

ก็จบไปแล้วนะครับสำหรับบทความเรื่อง CSP หรือ Content-Security-Policy ที่จะช่วยลดความเสี่ยงต่อการถูกโจมตีให้กับเว็บไซต์ของทุกคนให้ปลอดภัยจาก หลาย ๆ ช่องโหว่ฝั่ง Client Side  หวังว่าทุกคนจะได้นำความรู้ตรงนี้ไปปรับใช้ เพื่อลดความเสี่ยงจากการถูกโจมตีจากแฮกเกอร์หรือผู้ไม่ประสงค์ดีต่าง ๆ 

เมื่อช่วงที่ผ่านมานี้มีเหตุการณ์ที่แฮกเกอร์ได้โจมตีล้วงข้อมูล เป็นจำนวนมากขึ้นเรื่อย ๆ มีช่องโหว่ใหม่ ๆ เกิดขึ้นเยอะแยะมากมาย CSP ก็เป็นหนึ่งในตัวเลือกที่ควรพิจารณาปรับใช้กัน เอาล่ะ ทีนี้ก็ได้เวลาที่ทุกคนจะเอาความรู้ไปปรับใช้กับเว็บไซต์ของตัวเอง เพื่อให้ปลอดภัยมากขึ้นจากพวกแฮกเกอร์ได้แล้วนะครับ