เข้าใจ Web Security: จัดเก็บ JWT ไว้ใน local storage หรือ cookies ดี?

Nuttavut Thongjor

เป็นธรรมดาของระบบทั่วไปที่ต้องมีการตรวจสอบผู้ใช้งานระบบว่าผู้ใช้งานปัจจุบันคือใคร (Authentication) เมื่อทราบแล้วว่าใครคือผู้เข้าใช้ระบบ จึงทำการตรวจสอบสิทธิ์ว่าเขาผู้นั้นสามารถทำอะไรกับทรัพยากรในระบบได้บ้าง (Authorization)

สำหรับการสร้าง API เรามักนิยมใช้ Stateless Token เช่น JWT ในการทำ Authentication (Token-based authentication) โดย token ประเภทนี้จะไม่มีการจัดเก็บในฝั่งเซิฟเวอร์ แต่ยังจำเป็นต้องจัดเก็บทางฝั่งไคลเอ็นต์ เพื่อใช้ส่งไปกับรีเควสให้ทางเซิฟเวอร์ทราบว่าเราเป็นใคร

เมื่อ access token นั้นยังต้องจับเก็บทางฝั่งไคลเอ็นต์ (browser) คำถามจึงเกิดขึ้นว่าเราควรจัดเก็บไว้ใน local storage / session storage หรือ cookies ดีกว่ากัน?

จัดเก็บ token ด้วย local storage

แรกเริ่มเราต้องลอคอินเข้าสู่ระบบก่อนเพื่อยืนยันว่าเราคือใคร ภายหลังการลอคอินระบบจะส่ง access-token กลับมาให้ เพื่อให้ฝั่งไคลเอ็นต์ใช้จัดเก็บและส่งกลับมาหาเซิฟเวอร์อีกครั้ง เมื่อต้องการทำอะไรซักอย่างกับระบบในนามของผู้ใช้คนนั้น

Code
1HTTP/1.1 200 OK
2Content-Type: application/json
3{
4 "accessToken": "eyJz11a...k23aZWy",
5 "tokenType": "Bearer",
6 "expiresIn": 86400
7}

หลังจากเราได้รับ response กลับมาแล้ว เราจึงทำการจัดเก็บ access token ไว้ภายใต้ HTML5 Web Storage เช่น localStorage เป็นต้น เมื่อเราทำการร้องขอต่อ API ในครั้งถัดไป หากเราต้องการระบุว่าเราคือใครเราต้องใช้ JavaScript เพื่ออ่าน access token ที่จัดเก็บใน HTML5 Web Storage แล้วทำการส่งไปพร้อมกับรีเควสนั้นๆ

Code
1HTTP/1.1
2
3POST /blog/new
4Host: galaxies.com
5Authorization: Bearer eyJz11a...k23aZWy

ตัวอย่างข้างต้น เรานำส่ง access token ไปพร้อมกับรีเควสผ่านทาง Authorization Header นั่นเอง

เผชิญหน้ากับ Cross-Site Scripting Attacks

เราสามารถเข้าถึง access token ภายใต้ localStorage ได้ด้วยคำสั่งจาก JavaScript หากแฮกเกอร์สามารถออกคำสั่ง JavaScript บนเว็บเราได้ละ จะเกิดอะไรขึ้น?

สมมติเว็บของเราอนุญาตให้ผู้ใช้โพสต์ข้อความอะไรก็ได้ในช่องสนทนา ผู้ใช้หัวหมอรายหนึ่งจึงใส่โค้ด HTML พร้อมแปะสคริปต์ JavaScript เก๋ๆ ดังนี้

HTML
1<img src="http://file.not.exist"
2onerror=alert(localStorage.getItem('access-token'));>

เพราะเราอนุญาตให้ผู้ใช้ใส่อะไรก็ได้ โค้ดดังกล่าวจึงได้รับการประมวลผลในฐานะที่เป็นอีลีเมนต์หนึ่งของ HTML เนื่องจากไฟล์รูปภาพไม่มีอยู่จริง onerror จึงได้รับการทำงาน เป็นผลให้เผย access token ที่จัดเก็บไว้ใต้ localStorage ออกมาด้วย

แน่นอนว่าถ้าคุณเป็นแฮกเกอร์ คุณย่อมไม่เพียงแค่แสดง access token ออกมา แต่คุณคงปรารถนาที่จะทำอะไรพิศดารกว่านั้น เช่นการสำเนา access token ไว้แอบอ้างว่าเป็นผู้ใช้ในครั้งถัดไป

เทคนิคในการแอบฝังโค้ดแปลกปลอมเข้าไป แล้วรอให้เหยื่อมาเปิดหน้าเพจที่มีโค้ดอัปรีย์เช่นนี้ เราเรียกว่าการโจมตีแบบ Cross-Site Scripting (XSS)

การที่ localStorage เข้าถึงได้จาก JavaScript โดยตรงเช่นนี้ จึงมีโอกาสเสี่ยงต่อการโจมตีแบบ XSS นั่นเอง ด้วยเหตุนี้ OWASP องค์กรไม่แสวงหากำไรด้านความปลอดภัย จึงได้ให้คำแนะนำว่าเราไม่ควรจัดเก็บข้อมูลที่เป็นความลับภายใต้ Web Storage

แล้วถ้าเราเปลี่ยนไปจัดเก็บ access token ผ่าน cookies แทนละจะช่วยแก้ปัญหานี้ได้หรือไม่?

รู้จัก HttpOnly Cookies

การจัดเก็บ access token บน cookie ด้วยวิธีธรรมดาก็ไม่ต่างอะไรกับการใช้ localStorage เพราะเรายังคงเข้าถึงได้ผ่าน JavaScript อยู่ดี จึงยังคงเสี่ยงต่อการโจมตีด้วย XSS

cookies นั้นเป็นคนหลายบุคลิกครับ นอกจากจะมีตัวตนแบบธรรมดาเรียบง่ายแล้ว ยังมี secure cookie ที่ใช้ควบคู่กับ HTTPS แต่นางเอกของงานนี้ที่เราจะพูดถึงคือ HttpOnly cookies

หากการเข้าถึง access token ได้จาก JavaScript เป็นปัญหา ก็ตัดปัญหาไม่ให้ JavaScript เข้าถึง access token ภายใต้ cookies ได้ซะซิ เท่านี้ปัญหาก็จบ และจากประโยคบอกเล่าข้างต้นนี้ HttpOnly cookies จึงเข้ามามีบทบาทสำคัญ a HttpOnly cookies เป็นคุกกี้ประเภทที่ป้องกันไม่ให้ JavaScript สามารถเข้าถึงข้อมูลมันได้ผ่าน document.cookie เราจึงมั่นใจได้ว่าข้อมูลจากคุกกี้ประเภทนี้ จะปลอดภัยจากการใช้ XSS เพื่อขโมย access token

เมื่อเราเปลี่ยนการจัดเก็บ access token จาก localStorage มาเป็น HttpOnly cookies สิ่งที่เราต้องทำก็คือการสื่อสารระหว่างเซิฟเวอร์และไคลเอ็นต์ที่เปลี่ยนมาใช้ cookie ประเภทนี้แทน

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

หากเรามองให้ดี ไม่ว่าจะเป็นการส่ง access token ผ่าน Authorization Header แบบที่นำเสนอข้างต้น หรือการส่ง access token ผ่าน cookies ทั้งสองล้วนส่งข้อมูลผ่าน Header อยู่ดี นั่นเพราะ cookies ถูกจัดส่งผ่าน Header ที่ชื่อว่า Set-Token นั่นเอง

Code
1Set-Cookie: access-token=eyJz11a...k23aZWy; Secure; HttpOnly

การจัดส่ง access token ผ่านคุกกี้จึงไม่ได้ลำบากต่อการแงะข้อมูลกลับคืนมาแต่อย่างใด

Cookies และ Cross-Site Request Forgery

ถ้าสมมติเพื่อนๆเป็นแฮกเกอร์ เมื่อเราไม่สามารถขโมย access token ได้แล้ว เราจะโจรกรรมด้วยวิธีไหนต่อไปดี?

เราขโมย access token เพื่อแอบเอาค่านี้ไปใช้บอกระบบว่าเราคือเหยื่อ แต่เมื่อเราแอบขโมย token ไม่ได้ เราก็ให้เหยื่อเป็นคนสร้างธุรกรรมที่เราต้องการด้วยตัวเขาเองซะเลย เช่น เราต้องการให้เหยื่อโอนเงินให้เรา ในเมื่อเราขโมย token เพื่อใช้ทำการโอนในนามของเหยื่อไม่ได้ เราก็แอบล่อให้เหยื่อโอนเงินให้เราโดยไม่ให้เหยื่อรู้ตัว

สมมติเว็บของธนาคารที่เราใช้อยู่ประจำคือ mybank.com แฮกเกอร์อาจสร้างไซต์ใหม่ชื่อ mybamk.com แล้วล่อลวงให้เราเข้าใช้บริการไซต์นี้

ตอนนี้คุณถูกล่อลวงเรียบร้อยแล้ว คุณต้องการโอนเงินคุณจึงไปหน้าเพจสำหรับการโอนเงินคือ /transfer ตามปกติ แต่ที่ไม่ปกติคือคุณถูกล่อลวงมาที่เพจที่ลอกเลียนแบบของจริงอยู่

โดยทั่วไปการโอนเงินของไซต์ที่แท้ทรู จะใช้ฟอร์มแบบ POST พร้อมส่ง accountID ปลายทางและจำนวนเงินไปยังเซิฟเวอร์ แฮกเกอร์แสนฉลาดตอนนี้ก็ได้เลียนแบบฟอร์มดังกล่าวมาไว้ในหน้าเว็บตน

HTML
1<form action="https://mybank.com/transfer" method="POST">
2 <input type="text" name="accountID" value="HACKER ID" />
3 <input type="text" name="amount" value="AMOUNT" />
4</form>

จะสังเกตเห็นว่าแบบฟอร์มนี้จะยิงกลับไปที่เว็บต้นฉบับพร้อมกับส่ง accountID เป็นไอดีของแฮกเกอร์แทน

การที่เราจะโอนเงินให้ใครซักคนหนึ่ง จำเป็นที่ระบบต้องทราบว่าต้นทางการโอนคือใคร access token จึงเป็นสิ่งสำคัญที่ไคลเอ็นต์ต้องส่งให้กับเซิฟเวอร์เพื่อบอกว่าเราคือใครและมีสิทธิ์ในการโอนหรือไม่

เมื่อแบบฟอร์มนี้อ้างอิงว่าเป็นการส่งรีเควสไปหา mybank.com คุกกี้ที่จัดเก็บไว้ใช้กับ mybank.com จึงถูกส่งไปด้วย เพียงแค่ผู้ใช้ระบบถูกล่อลวงให้ทำธุรกรรมผ่านไซต์ปลอม access token ของเขาก็จะถูกนำไปใช้ในนามของตัวเขาเอง โดยที่แฮกเกอร์ไม่ต้องเปลืองแรงในการขโมยเลย

การโจมตีที่บังคับ (เรียกว่าขืนใจดีกว่า) ให้ผู้ใช้งานที่ลอคอินเข้าสู่ระบบแล้ว ทำอะไรซักอย่างที่เขาไม่ได้ต้องการทำ โดยหมายให้ระบบเข้าใจว่าเป็นการกระทำของผู้นั้นเอง เมื่อเหยื่อเข้าสู่เว็บไซต์จึงล่อลวงให้เกิดการทำธุรกรรมไปยังไซต์เป้าหมาย การโจมตีเช่นนี้เราเรียกว่า Cross-Site Request Forgery (CSRF)

ป้องกันการโจมตีแบบ CSRF ด้วย Origin และ X-Requested-With

การป้องกันการโจมตีแบบ CSRF นั้นสามารถทำได้หลายกระบวนท่า ในหัวข้อนี้ผมจะนำเสนอวิธีหนึ่งแบบคร่าวๆ โดยไม่อธิบายอะไรเป็นพิเศษ เพราะเราจะไปเน้นการแก้ปัญหา CSRF ด้วยอีกสองวิธีในหัวข้อถัดไปแทน

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

หาก API ของเราอนุญาตให้เข้าถึงได้ผ่าน AJAX เท่านั้น เราสามารถทำการการเพิ่ม header พิเศษ เช่น X-Requested-With: XMLHttpRequest ส่งไปพร้อมกับรีเควส เมื่อเซิฟเวอร์ได้รับก็จะทำการตรวจสอบ header ดังกล่าวว่ามีหรือไม่และมีค่าตามที่กำหนดไหม วิธีการนี้จะช่วยป้องกันไม่ให้ส่งรีเควสจากฟอร์มได้ นั่นเพราะการส่งข้อมูลผ่านฟอร์มจะส่ง header ตามแต่ใจเรากำหนดไม่ได้

วิธีการใช้ Origin ก็ฟังดูแปลก เพราะ API Server ต้องคอยกังวลและรับรู้ว่าไคลเอ็นต์คือใคร ส่วนวิธี X-Requested-With ก็จะยิ่งแปลกเมื่อเราออกแบบ API เราไม่ควรจำกัดให้ใช้งานได้กับ AJAX เท่านั้น

รู้จักการป้องกัน CSRF ด้วย Double Submit Cookie

เซิฟเวอร์นั้นอาจแยกแยะไม่ได้ว่ารีเควสนั้นมาจากความตั้งใจของผู้ใช้งานระบบเองหรือเป็นการแฝงมาจากผู้ไม่ประสงค์ดี ในกรณีของการหลอกล่อด้วยการสร้างฟอร์มบนไซต์อื่น แฮกเกอร์จะไม่สามารถเข้าถึงคุกกี้จากเซิฟเวอร์ใต้ไซต์ต้นฉบับได้เนื่องจากติด same-origin policy

ไซต์ต้นฉบับคือ mybank.com จะส่งคุกกี้เช่นไรจากเซิฟเวอร์ แต่ไซต์ต้มตุ๋นที่ชื่อ mybamk.com แม้กระสันอยากได้ข้อมูลใต้คุกกี้ดังกล่าวก็มิอาจทำได้ นั่นเพราะทั้งสองไซต์อยู่ข้าม origin กัน (คนละ protocol, port หรือ host)

อาศัยความจริงข้อนี้เราจึงแอบส่งค่าบางอย่างที่ได้จากการสุ่มมั่วมากับคุกกี้ พร้อมทั้งทำการฝังค่าตัวนี้ไว้กับฟอร์ม เช่น

HTML
1<form action="https://mybank.com/transfer" method="POST">
2 <input type="hidden" name="csrfToken" value="CjihIlUp...YQmNcgNb3SSMDn" />
3 <input type="text" name="accountID" value="ID" />
4 <input type="text" name="amount" value="AMOUNT" />
5</form>

จากตัวอย่างข้างต้น เราสุ่มค่ามั่วขึ้นมาคือ CjihIlUp...YQmNcgNb3SSMDn พร้อมส่งค่านี้มากับคุกกี้และส่งค่าเดียวกันนี้มากับฟอร์ม เมื่อใดก็ตามที่รีเควสถูกส่งไป เซิฟเวอร์จะต้องทำการเทียบค่า ว่าค่าของ csrfToken ในคุกกี้เป็นค่าเดียวกันกับในฟอร์มหรือไม่

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

ด้วยการใช้ HttpOnly และการทำ Double Submit Cookie จึงปกป้องการโจมตีทั้งแบบ XSS (เฉพาะรูปแบบที่หมายจะขโมย access token) และ CSRF ได้นั่นเอง

ป้องกัน CSRF ด้วย Same-Site Cookies

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

Same-site cookies เป็นคุกกี้ที่สามารถป้องกันการโจมตีแบบ CSRF ได้ เนื่องจากคุกกี้ประเภทนี้จะไม่ถูกส่งหากมาจากต่างไซต์กัน

Same-site cookies มีสองแบบครับ คือ Strict และ Lax

Strict ตามชื่อเลย เข้มงวดสุดๆ คุกกี้ที่ตั้งค่านี้จะไม่ถูกส่งมายังไซต์ของเรา หากรีเควสนั้นมาจากไซต์อื่น ตรงจุดนี้อาจมีผลต่อความรู้สึกของผู้ใช้ได้ เช่น หากเราเป็น Facebook และตั้งค่าคุกกี้เพื่อจัดเก็บ access token ไว้ด้วย Strict สิ่งที่เกิดขึ้นก็คือ เมื่อมีใครเอาลิงค์เฟสบุคไปแชร์ในไซต์ของเรา เราจะจิ้มเพื่อเข้าไปดูได้แต่เราจะเห็นว่าตัวเราเองยังไม่ได้ลอคอินในเฟสบุค (ทั้งๆ ที่ลอคอินแล้ว) นั่นเพราะรีเควสที่ยิงไปหาเฟสบุคมาจากไซต์อื่น Same-site cookies แบบ Strict จึงไม่ถูกส่งไปด้วย เฟสบุคจึงไม่ทราบว่าเราคือใคร

โดยหลักแล้วเราออกแบบให้เข้าถึงทรัพยากรในเซิฟเวอร์ของเราผ่าน HTTP GET เมื่อการร้องขอนั้นไม่ส่งผลกระทบต่อทรัพยากรในเซิฟเวอร์ หากการร้องขอทำให้เกิดการเปลี่ยนแปลง เช่น สร้างทรัพยากรใหม่ในระบบ (สร้างบทความใหม่ สร้างผู้ใช้งานคนใหม่) เราจะเปลี่ยนไปใช้ HTTP VERBS อื่นแทน เช่น POST

อาศัยความจริงข้างต้น เราจึงกล่าวได้ว่าการเข้าถึงแบบ GET ค่อนข้างปลอดภัยกว่า ด้วยความที่เราก็อยากให้ผู้อื่นแชร์ลิงก์ไซต์เราไปไว้ในเพจเขาได้ด้วย การใช้ Strict จึงไม่ตอบโจทย์มากนั้น ทางเลือกของเราจึงเป็น Same-site cookies แบบ Lax

Lax อนุญาตให้ส่งคุกกี้นี้ได้เมื่อทำการร้องขอจากไซต์อื่น เพียงแต่การร้องขอนี้ต้องเกิดขึ้นบน HTTP GET เช่น จิ้มลิงก์ เท่านั้น โดยเงื่อนไขสำคัญของการทำงานคือการกระทำนี้ต้องทำให้เกิดการเปลี่ยน URL บน address bar (top-level navigation) ความหมายคืออะไร? นั่นคือการร้องขอแบบ GET ผ่าน iframe หรือ AJAX จะทำไม่ได้นั่นเอง เพราะการใช้ AJAX หรือ iframe นั้น URL บน address bar จะไม่เปลี่ยน

Code
1Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Lax;
2Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Strict;

ด้วยการผสานระหว่าง HttpOnly, Secure และ SameSite การโจมตีแบบ XSS และ CSRF ก็เหมือนแมลงสาบโดนฉีดไบกอน... แม้อาจไม่ตายซะทีเดียว แต่ก็คงดิ้นกระแด่วๆคาขวดเขียว

Code
1Set-Cookie: <cookie-name>=<cookie-value>; Secure; HttpOnly; SameSite=Strict;

ช่างน่าเสียดายนักที่ Same-site cookies ยังไม่สามารถใช้งานได้ในทุกเบราเซอร์... บาย

เข้าใจวิถีแห่ง Cookies

HTTP นั้นเป็น stateless protocal อารมณ์ว่าสมองกลวงไม่เคยจำอะไรเลย แต่บางครั้งเราก็อยากให้การสื่อสารระหว่าง client และ server นั้นรู้เรื่อง ไม่ใช่ต้องมาลอคอินใหม่ทุกครั้งของการใช้งาน

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

หลายครั้งที่เราต้องการใช้ข้อมูลจาก Cookies ในไซต์อื่น เช่น การฝังโฆษณาลงบนเว็บของเรา เมื่อผู้ใช้เว็บเราทำการคลิกโฆษณาต้องทำการส่งรีเควสไปผู้ให้บริการ ถ้าไม่ทำเช่นนี้เราก็อดได้ตังค่าโฆษณาใช่ไหมละ ฉะนั้นแล้วคุกกี้ซึ่งจัดเก็บ ID ของเรา (ต้นทางที่อนุญาตให้ลงโฆษณา) ต้องถูกส่งกลับไปที่เว็บโฆษณาด้วย ตรงจุดนี้จึงกล่าวได้ว่า Cookie ออกแบบมาเพื่อให้ไม่ปลอดภัยตั้งแต่แรก

Cookies นั้นเชื่อถือได้ยาก การตั้งค่าทั่วไปอาจทำให้ต่าง subdomain เข้าถึงคุกกี้ของกันและกันได้ นอกจากนี้คุกกี้บน https://example.com ยังสามารถเข้าถึงจาก http://example.com อีกด้วย จึงกล่าวได้ว่าคุกกี้ไม่ได้ตามหลักการ Same Origin Policy แบบที่ localStorage เป็น เมื่อเป็นเช่นนี้เราจึงต้องมี Domain, Path, Secure และ Http-Only flags เพื่อทำให้คุกกี้ปลอดภัยมากขึ้น

ฟังๆดูแล้วคุกกี้ดูมีแต่รูที่เราปะจนกลับมาปลอดภัย แล้วถ้าเราหันกลับมาใช้ Web Storage แบบ localStorage หละ มันจะดีขึ้นไหม?

HTML Sanitizer

localStorage นั้นป้องกัน CSRF ได้อย่างสมบูรณ์ นั่นเพราะค่าต่างๆที่เก็บอยู่ภายใต้ localStorage จะไม่ถูกส่งไปกับทุกรีเควสเช่นเดียวกับคุกกี้ นอกจากนี้ค่าของ localStorage ยังไม่สามารถเข้าถึงได้จากไซต์ต่าง origin (same-origin policy)

ทว่า localStorage อาศัยการอ่านและเขียนค่าด้วย JavaScript เมื่อแฮกเกอร์ได้สิทธิ์ของการใช้ JavaScript เขาจึงเขาถึงค่า access token ภายใต้ localStorage ได้

มีความเป็นไปได้ที่แฮกเกอร์จะแอบฝังโค้ดไว้ในเพจของเราผ่านช่องทางต่างๆ เช่น ช่องสนทนา

HTML
1<img src="http://file.not.exist"
2onerror=alert(localStorage.getItem('access-token'));>

เพื่อดับฝันของแฮกเกอร์ เราจึงควรแปลงโค้ดอัปรีย์ (sanitize) นี้เสียก่อนทำการบรรจุหีบห่อลงเว็บของเรา หลังการแปลงผลลัพธ์ที่ได้อ่านเป็นเช่นนี้

HTML
1&lt;img src=&quot;http://file.not.exist&quot;
2onerror=alert(localStorage.getItem(&#039;access-token&#039;));&gt;

ช่างโชคดีจนน้ำตาไหล frontend libs สมัยใหม่ไม่ว่าจะเป็น Angular หรือ React ต่างก็ป้องกัน XSS ด้วยการ sanitize ให้กับเราอยู่แล้ว

ในความโชคดีก็ยังมีฝันร้าย การโจมตีแบบ XSS ไม่ได้มาในรูปแบบของการแปะโค้ดลงช่องสนทนาเท่านั้น หากแต่การโจมตีดังกล่าวอาจฝังอยู่ใน lib ที่เรานำมาใช้ร่วมด้วยก็ได้ โดยเฉพาะอย่างยิ่งกับการใช้ CDN

Content Security Policy

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

สมมติหน้าเว็บเราต้องการมีปุ่ม Like ของเฟสบุค หากใครชอบใจหน้าเว็บนี้ย่อมสามารถกดปุ่ม Like ได้ ด้วยเหตุนี้เราจึงต้องใช้สคริปต์จากเฟสบุคคือ https://connect.facebook.net/en_US/sdk.js#xfbml=1 ในเพจเรา

เรามั่นใจว่าเพจเราใช้แต่สคริปต์ที่กำหนดเท่านั้น เราจึงไม่ต้องการให้แฮกเกอร์หาช่องโหว่แล้วนำสคริปต์จาก https://connect.evil.net มาวางเพื่อทำงาน นี่คือเป้าหมายที่เราต้องการ

Content-Security-Policy เป็น HTTP header ที่กำหนดมาจากเซิฟเวอร์เลยว่าสิ่งใดคือสิ่งที่เราเชื่อถือได้บ้าง (whitelist) ด้วยการใช้งาน CSP บราวเซอร์จะทำการประมวลผลเฉพาะสคริปต์ใน whitelist ที่เรากำหนดเท่านั้น สคริปต์ประหลาดที่แฮกเกอร์แฝงมากไม่ได้อยู่ใน whitelist ก็จะไม่ถูกประมวลผล ทำให้เราปลอดภัยจาก XSS ได้ระดับนึง

Code
1Content-Security-Policy: script-src 'self' https://connect.facebook.net

หมายเหตุ: ไม่ใช่เฉพาะ script-src เท่านั้นที่สามารถกำหนดได้ ยังมีค่าอื่นๆอีกมาก เช่น connect-src font-src img-src เป็นต้น ที่เพื่อนๆสามารถลองตั้งค่าได้ครับ

เก็บ access token ใน cookies หรือ localStorage ดีกว่ากัน?

แม้ในปี 2017 OWASP จะจัดให้การโจมตีแบบ XSS (Server XSS + Client XSS) อยู่ลำดับสาม และการโจมตีแบบ CSRF อยู่ลำดับแปด แต่ถ้าเพื่อนๆเป็นแฮกเกอร์ จะใช้วิธีใดในการโจมตี?

หากเราเลือกโจมตีแบบ XSS (เพื่อเข้าถึง access token) เพื่อนๆอาจได้ access-token ไปก็จริง แต่โดยปกติแล้วเรามักตั้งให้ token ประเภทนี้มีอายุสั้นมาก ถึงเราได้มาก็ไม่แน่ว่าจะใช้งานได้ เพราะ token อาจหมดอายุไปเสียก่อน นอกจากนี้การโจมตีแบบ XSS ยังต้องรอเหยื่อมาตกหลุมพลางอีก

เมื่อเป็นเช่นนี้ เพื่อนๆจึงคิดว่างั้นโจมตีแบบ CSRF ดีกว่าเพราะได้ผลที่แน่นอนกว่า ด้วยการปลอมแปลงนิดหน่อยเหยื่อก็ทำธุรกรรมให้กับเราแล้ว

หากอาศัยจากสมมติฐานนี้ เราอาจมองว่า CSRF ควรได้รับการป้องกันระดับสูง ในขณะที่ XSS ก็ต้องได้รับการป้องกันเช่นกัน เราจึงใช้ localStorage ที่ป้องกัน CSRF ได้แน่นอน แล้วทำ CSP + Sanitize เพื่อป้องกัน XSS ระดับ 99.99% (เป็ดโปรยังฆ่าเชื้อได้ 99.99% เลย)

คุกกี้ก็เป็นอีกหนึ่งทางออกที่สามารถเลือกใช้ได้ เพียงแต่การจัดเก็บ access token บนคุกกี้ เพื่อนๆควรใช้คู่กับ Secure + HttpOnly พร้อมทำ Double Submit Cookie (ไม่แนะนำ Same-Site cookies เพราะยังไม่รองรับอย่างกว้างขวาง)

XSS ไม่จำกัดอยู่แค่การขโมย token

XSS นั้นไม่เพียงแต่เป็นรูปแบบการโจมตีเพื่อหมายขโมย access token เท่านั้น เพจสอนแฮกเว็บแบบแมวๆ - เพจดังด้านความปลอดภัย ได้ขยายความรูปแบบการโจมตีด้วย XSS ไว้ว่า

การโจมตีเว็บแบบ XSS เราไม่ได้ใช้โจมตีแต่การขโมย session token (ไม่ว่าจะอยู่ใน cookie/local storage) อย่างเดียว แฮกเกอร์สามารถใช้ทำอย่างอื่นได้ เช่นใช้ JavaScript ดักคีย์บอร์ด หรืออ่านค่าใน textfield เช่นเลขบัตรเครดิต หรือแก้ไขหน้าตาเว็บ แก้เลขบัญชีเว็บใน HTML หรือถ้าเป็น XSS ใส่เว็บที่ user สิทธิ์สูงรัน command ผ่านฟีเจอร์เว็บได้ก็อาจจะได้ RCE ฯลฯ

เมื่อพิจารณาจากรูปแบบการโจมตีที่เกิดขึ้นได้ XSS จึงดูร้ายแรงและเป็นอันตรายมากกว่า CSRF โดยแอดมิน เพจสอนแฮกเว็บแบบแมวๆ ได้ให้เหตุผลไว้ว่า

ถ้าเว็บใด ๆ มีช่องโหว่ XSS แล้วละก็ การป้องกัน CSRF เกือบทุกอย่างที่เขียนมา 99% ไร้ความหมายทันทีเพราะ

  • ถ้าทำ XSS สร้าง form แล้ว submit ค่าทำ CSRF (จริง ๆ ควรชื่อ Same-Site Request Forgery มากกว่าในเคสนี้) เราสามารถใช้ JavaScript อ่านค่า csrf token ใน textfield ได้ (bypass ในหัวข้อ double submit cookie)

  • ถ้าใช้ XSS ทำ CSRF ค่า Origin ที่ติดมาเป็นค่าเหมือนเว็บเดิม Same-Origin Policy ช่วยไม่ได้ (bypass ในหัวข้อ ป้องกันการโจมตีแบบ CSRF ด้วย Origin)

มุมมองของผู้เขียน

สำหรับผม - ผู้เขียน เลือกการใช้ localStorage เพราะเป็นวิธีที่นิยมใช้กันมากกว่า เราเชื่อมั่นในโค้ดที่เราเขียนและสคริปต์ที่เราใช้ เราใช้ frontend เฟรมเวิร์กที่ดีและอัพเดทสม่ำเสมอ อีกทั้งเรามั่นใจว่าปัญหาจากการใช้ localStorage จะยุ่งยากน้อยกว่าคุกกี้

ผู้เขียนเชื่อว่าไม่มีระบบใดจะสมบูรณ์ 100% จึงอย่าไว้ใจช่องโหว่ที่อาจเกิดขึ้นได้ สิ่งสำคัญของการใช้ access token คือช่วงอายุที่สั้น และเราต้องทำให้มั่นใจว่าธุรกรรมที่สำคัญควรได้รับการยืนยันเสมอ เช่น ให้กรอกพาสเวิร์ดอีกครั้ง นอกจากนี้อะไรที่เป็นพื้นฐานที่ดีก็อย่าละเลย เช่น sanitize ข้อมูลทุกครั้งก่อนจัดเก็บ เป็นต้น

เมื่อ Local Storage คือความยุ่งยากของการทำ Server-Side Rendering

localstorage นั้นจำเป็นต้องใช้ JavaScript ในการเข้าถึง หากเราต้องการทำ Server-Side Rendering (SSR) ด้วยการเลือกแสดงผลข้อมูลอิงกับผู้ใช้งานในขณะนั้นจึงเป็นไปไม่ได้ นั่นเพราะทุกครั้งของการทำ SSR เราไม่ได้ส่งข้อมูล access-token ที่เก็บอยู่ภายใต้ localstorage ไปด้วยนั่นเอง แต่สำหรับการจัดเก็บผ่าน cookie นั้นไม่เป็นปัญหา เพราะ cookie จะถูกจัดส่งไปพร้อมกับทุกการร้องขอ เป็นผลให้ access-token ได้รับการจัดส่งตามไปด้วย เมื่อมองในมุมของการทำ SSR cookie จึงมีภาษีที่เหนือกว่าเยอะ

ของแถม - ป้องกัน Clickjacking ด้วย X-Frame-Options

แม้เราจะป้องกันการโจมตีด้วย CSRF เป็นอย่างดี แต่การโจมตีที่เรียกว่า Clickjacking ก็อาจทำให้คุณหงายเงิบไปเลย

สมมติเราอยากล่อให้คนกดถูกใจเพจในเฟสบุคเราจะทำไงดี?

แน่นอนว่าถ้าเราสร้างเว็บแล้วแปะปุ่มให้เขากดไลค์ ถ้าเขาไม่อยากกด เขาก็คงไม่มากดหลอก จริงไหมครับ? แล้วถ้าเป็นแบบนี้หละ เราสร้างเว็บไซต์ขึ้นมา แล้วแสดงข้อความว่า "ฟรี iPhone X คลิกเลย" พร้อมทั้งแอบซ่อน iframe แบบโปร่งใส (มองไม่เห็น) ไว้ด้านหลัง iframe ดังกล่าวให้ทำการโหลดหน้าเพจเฟสบุคของเรา โดยกำหนดให้ตำแหน่งของคำว่า "ฟรี iPhone X คลิกเลย" ตรงกับปุ่ม Like Page

เมื่อเหยื่อเห็นคำว่าฟรี ต่อมกระสันก็จะทำงาน พร้อมพลาดเอามือไปกดปุ่มดังกล่าว ป๊ะเข้าให้ ปุ่ม Like Page ก็จะถูกกดตามไปด้วย เพราะมันซ่อนอยู่ข้างหลัง ฉลาดไหมละ?

และนี่หละครับคือการโจมตีแบบ Clickjacking ซึ่งเป็นวิธีการโจมตีที่ล่อให้เหยื่อคลิกอะไรซักอย่างแล้วไปทำอีกอย่างที่ซ่อนอยู่โดยที่เหยื่อไม่ตั้งใจ

แน่นอนว่าต่อให้คุณป้องกัน CSRF มาเป็นอย่างดี ก็ยังไม่รอดพ้นการโจมตีเช่นนี้ นั่นเพราะการคลิกเกิดขึ้นโดยตรงบนหน้าเพจของคุณที่ถูกฝังอยู่กับ iframe ที่ซ่อนอยู่

ถ้าคุณเป็นเฟสบุค คุณคิดว่าจะป้องกัน Clickjacking อย่างไรดี? ใช่แล้วครับ ถ้าไม่อยากให้เพจของเราซ่อนอยู่เบื้องหลังด้วย iframe ก็อย่าอนุญาตให้เพจเราแสดงเนื้อหาใน iframe ได้ซะซิ!

สิ่งที่เราควรทำจึงเป็นการตั้งค่า response ของเพจให้มี X-Frame-Options header เป็น DENY ด้วยวิธีนี้จะเป็นการป้องกันไม่ให้เพจของเราถูกฝังบน iframe ใดๆได้ แต่หากเราต้องการให้ฝังเพจได้เฉพาะกับ iframe ในไซต์เดียวกัน ก็ให้ตั้งค่า header ตัวนี้เป็น SAMEORIGIN แทน

เอกสารอ้างอิง

Abhinav Sejpal (2014). Why am I anxious about Clickjacking?. Retrieved December, 5, 2017, from https://www.linkedin.com/pulse/20141202104842-120953718-why-am-i-anxious-about-clickjacking

Mike West and Joseph Medley. Content Security Policy. Retrieved December, 5, 2017, from https://developers.google.com/web/fundamentals/security/csp/

James Kettle (2016). Web Storage: the lesser evil for session tokens. Retrieved December, 5, 2017, from http://blog.portswigger.net/2016/05/web-storage-lesser-evil-for-session.html

OWASP. Top 10 2017-A3-Cross-Site Scripting (XSS). Retrieved December, 5, 2017, from https://www.owasp.org/index.php/Top_10_2017-A3-Cross-Site_Scripting_(XSS)

สารบัญ

สารบัญ

  • จัดเก็บ token ด้วย local storage
  • เผชิญหน้ากับ Cross-Site Scripting Attacks
  • รู้จัก HttpOnly Cookies
  • Cookies และ Cross-Site Request Forgery
  • ป้องกันการโจมตีแบบ CSRF ด้วย Origin และ X-Requested-With
  • รู้จักการป้องกัน CSRF ด้วย Double Submit Cookie
  • ป้องกัน CSRF ด้วย Same-Site Cookies
  • เข้าใจวิถีแห่ง Cookies
  • HTML Sanitizer
  • Content Security Policy
  • เก็บ access token ใน cookies หรือ localStorage ดีกว่ากัน?
  • XSS ไม่จำกัดอยู่แค่การขโมย token
  • มุมมองของผู้เขียน
  • เมื่อ Local Storage คือความยุ่งยากของการทำ Server-Side Rendering
  • ของแถม - ป้องกัน Clickjacking ด้วย X-Frame-Options
  • เอกสารอ้างอิง