ทำความเข้าใจกับ CSRF (Cross Site Request Forgery) ตอนที่ 2

ใครยังไม่ได้อ่านตอนแรก อ่านได้ที่นี่นะครับ > (ตอนที่ 1)

3. การป้องกัน CSRF ที่เป็นที่นิยม

ในการป้องกัน CSRF คำถามที่สำคัญคือ จะทำอย่างไรให้เรารู้ว่า “Request ที่ส่งมา มาจาก User จริงๆ” ไม่ได้มาจากเว็บอื่นหรือที่อื่น (เช่น HTML email ที่ user เปิดขึ้นมาพอดี)

คำถามนี้อาจจะตอบได้ยาก เลยขอเปลี่ยนคำถามว่า จะทำอย่างไรให้เรารู้ว่า “Request ที่ส่งมา มาจาก interaction จาก หน้าเว็บของเรา” ไม่ได้มาจากเว็บอื่นหรือที่อื่น

มาดูกันดีกว่าว่ามีวิธีอะไรบ้างที่สามารถตอบโจทย์นี้ได้

3.1 Hidden input field

วิธีที่คลาสสิคที่สุดเลย ก็คือการฝัง token ใน hidden input field หน้าตาก็ประมาณนี้



<form>
<input type="hidden" value="aSecretTokenRandomlyGeneratedForThisSession" />
<input type="password" />
</form>


ตอนที่เราสร้าง HTML ทางฝั่งเซอร์เวอร์จะต้องสร้าง random token ที่ผู้โจมตีเดาไม่ได้ง่ายๆ (จะใส่ Salt, Hash, หรือทำเพิ่มอะไรก็ตามใจ แต่หลักการคือ อย่าให้คนอื่นสามารถเดา token นี้ได้) แล้วก็ใส่ input นี้เข้าไปใน HTML  พอเวลาเซอร์เวอร์รับ Request กลับมา ก็เช็คดูว่า Token ตรงกับ session หรือไม่ expire หรือยัง ถ้าตรง ก็ถือว่าปลอดภัยและทำงานต่อได้

วิธีนี้เป็นวิธีที่ค่อนข้างเก่า และมีความไม่สะดวกอยู่หลายอย่าง

  1. เว็บสมัยใหม่ใช้ Ajax ซึ่งเราต้องมีโค้ดมาโหลดเจ้า hidden input ตัวนี้อีกจาก HTML element ที่มี id ตามที่กำหนดไว้   แล้วถ้าเรามีหลายฟอร์มให้ submit เราจะเลือก token อันไหนดี (ซึงแก้ได้ด้วยการให้มีแค่ hidden input เดียวกันทั้ง page กรณีนั้นเราก็จะต้อง submit ทุกอย่างผ่าน Ajax อย่างเดียว) มองในแง่การเขียนโปรแกรม ผมถือว่าเรื่องนี้เป็นเรื่องใหญ่ที่สุดเพราะคุณต้องเอา View มาผูกกับ Security functionality เลยทีเดียว ถ้ามีแบบนี้เยอะๆก็ maintain กันอ้วกแตก
  2. HTML ต้องถูก autogenerate ถ้าเว็บต้องรองรับผู้ใช้เยอะมากๆจะทำให้รับโหลดได้น้อยลงมาก รวมไปถึงว่าวิธีนี้ใช้ไม่ได้ถ้า HTML page ไม่ได้ถูกสร้างจากฝั่งเซอร์เวอร์ก็ใช้ไม่ได้ (เช่น Back-end เป็น REST API แล้ว Front-end Fat JavaScript Client ไม่ได้)
  3. เราต้องเก็บค่า Token ที่สุ่มขึ้นมาไว้กับ Session เปลืองเวลาในการค้นหาเพิ่มขึ้นอีก
  4. หากเราต้องการ Application Server หลายๆตัว เราจะมั่นใจได้อย่างไรว่า Request ถัดไปจะถูกส่งไปยังเซอร์เวอร์ตัวเดิมที่เก็บ Token กับ Session ไว้ได้อย่างไร   (ซึ่งแก้ได้ด้วยการให้มี Session Server ตัวเดียว แล้ว Application Server ต้องมาเช็ค Session ที่นี่ก่อน แต่ก็จะช้า แล้วก็มี Single Point of Failure)

แม้จะมีความลำบาก แต่ก็ปลอดภัย สมัยก่อนที่ Ajax จะได้รับความนิยมและการประมวลผลส่วนใหญ่อยู่ฝั่งเซอร์เวอร์อยู่แล้ว (PHP/JSP) วิธีนี้ก็เป็นวิธีที่ใช้กันแพร่หลาย

3.2 รับ Token ผ่านทาง Cookie แล้วส่งกลับทาง Request Header

เว็บปัจจุบันส่วนใหญ่ใช้ Ajax อยู่แล้ว ซึ่งสามารถอ่าน Cookie แล้วก็แก้ Request Header ได้   เลยทำให้เรามีอีกทางเลือกหนึ่ง คือส่ง Token ผ่านทาง Cookie แทน

วิธีการนี้เลือกที่จะเชื่อใจว่าเว็บไซต์อื่นๆไม่สามารถอ่าน Cookie ของเว็บเราได้ (ซึ่งเว็บบราวเซอร์ส่วนใหญ่ก็เป็นอย่างนี้อยู่แล้ว) ทำให้เรามั่นใจได้ว่า Token ที่อยู่ใน Request Header จะมาจากในเว็บของเราเท่านั้น

วิธีนี้ช่วยแก้ข้อเสีย 1 กับ 2 ของ วิธี 3.1 ได้  ในมุมมองของคนเขียนโปรแกรม เราสารถเขียนโค้ดแนว Interceptor ก่อนส่ง Request ให้อ่านค่า คุ้กกี้แล้วใส่ลง Header ได้ ทำให้ไม่ต้องเอา logic ของส่วน security ไปผูกกับ element id ในวิธี 3.1

3.3 Double Submit

วิธีนี้คล้ายกับ 3.2 แต่เปลี่ยนเป็นไม่ต้องเก็บ Session กับ Token แม่งเลย

เวลารับ Request มาก็แค่เช็คว่าค่าของ Token ใน Cookie กับ Request Header ที่ส่งมา มันตรงกัน ถ้าตรงก็ปลอดภัย ไปต่อได้

ฟังแล้วอาจจะดูทะแม่งๆแล้วไม่ปลอดภัย บางคนบอกว่าผู้โจมตีอาจจะเซ็ต Cookie token ให้เป็น1 และ Request Header token ให้เป็น1 ทั้งคู่ เราก็ป้องกันอะไรไม่ได้สิ

แต่จริงๆแล้วปลอดภัยครับ วิธีนี้ตั้งอยู่บนความจริงที่ว่าเรามี Same Origin Policy ที่ไม่อนุญาติให้เว็บอื่น (Origin อื่น) เปลี่ยนหรืออ่านค่าใน Cookie ได้ ดังนั้น การที่ Request Header จะมี Token ที่ถูกต้องได้นั้น จะต้องมาจาก Origin เดียวกันที่สามารถอ่านค่า Cookie ได้

ข้อดีที่ได้ตามมาคือแก้ข้อเสีย 3 กับ 4 ได้ เพราะเราไม่ต้องเก็บ Session แล้ว ในเว็บที่ต้องรับผู้ใช้จำนวนมากๆๆ วิธีนี้ช่วยในเรื่องของ performance + horizontal scaling (ด้วยการเพิ่ม application server หลายๆตัว) ได้ดีมาก

3.4 การป้องกันแบบอื่นๆ

จะเห็นว่าวิธีการที่ผ่านมา ยึดหลักการรับส่ง Token เป็นหลัก จริงๆแล้วยังมีวิธีอื่นๆอีก เช่น Referrer header (ซึ่งไม่ปลอดภัย 100%), Origin Header, หรือ Challenge-response (captcha หรือ OTP นั่นเอง)

หากใครสนใจลองไปอ่านต่อได้ที่นี่ https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29_Prevention_Cheat_Sheet#Checking_The_Referer_Header

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

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

  1. Leave a comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: