แนวทางการออกแบบเว็บ API ให้มีความปลอดภัยแบบแมว ๆ

22 พฤษภาคม 2018

พิชญะ โมริโมโต
พิชญะ โมริโมโตหัวหน้าทีมทดสอบเจาะแฮกระบบ (lead penetration tester) ของบริษัท สยามถนัดแฮก, เป็นที่ปรึกษาด้านความปลอดภัยให้หน่วยงานเอกชน, เป็นที่รู้จักกันในฐานะ หนึ่งในแอดมินกลุ่ม 2600 Thailand และเป็นหนึ่งในคนเขียนบทความลงเพจ สอนแฮกเว็บแบบแมว ๆ

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

1. ใช้การเชื่อมต่อเป็น HTTPS เสมอ

HTTPS คือการเข้ารหัสข้อมูลระหว่างผู้ใช้งานกับเว็บ server  เรื่องนี้น่าจะเป็นพื้นฐานความปลอดภัยที่นักพัฒนาทุกคนควรนำไปใช้ได้แล้ว เพราะข่าวร้ายคือ ตั้งแต่เดือนกรกฏาคม 2561 ที่จะถึงนี้ Chrome เวอร์ชั่น 68 จะแจ้งเตือนเว็บทุกเว็บที่ไม่ได้ใช้ HTTPS ว่าไม่ปลอดภัย “Not Secure” อีกทั้งจากประสบการณ์ส่วนตัวผมเคยแจ้งประเด็นนี้ให้ระบบที่นึงว่ามันไม่ปลอดภัย แต่เค้ายังไงก็ไม่ยอมแก้ ข้ออ้างมาเพียบแต่พอบอก สิ่งที่คนส่วนมากอาจยังไม่รู้และถ้ารู้จะรีบเปิดคอมฯเข้าไปแก้เว็บตัวเองให้เป็น HTTPS เลยทันทีคือ ถ้าเว็บของเรานั้นเป็น HTTPS แล้ว Google จะจัดอันดับ ranking จากผลการค้นหามาสู่เว็บเราให้ดีขึ้นด้วยนะ! นอกจากเหตุผลด้านความปลอดภัย ก็ยังอาจสร้างรายได้ให้ธุรกิจมากขึ้นจากการที่ได้ผลลัพธ์อันดับดีขึ้นด้วย เป็นการทำ SEO ไปในตัว โชคสองชั้นชัด ๆ

ในมุมมองแฮกเกอร์แล้ว ถ้าการเชื่อมต่อเว็บใด ๆ ไม่ใช่ HTTPS จะสามารถอ่านข้อมูลที่ถูกดักมากลางทางได้ ตัวอย่างง่าย ๆ เช่นถ้าเร้าเตอร์หรือ server ระหว่าง เว็บกับผู้ใช้งานโดนแฮก ก็จะโดนดักอ่านรหัสผ่านได้หมดเลย

Figure 1: ตัวอย่างการดักอ่านรหัสผ่านที่ไม่ได้เข้ารหัสขณะ login ด้วยโปรแกรม Wireshark

2. เก็บรหัสผ่านในรูปแบบที่ผ่านฟังก์ชันแฮชที่มีความปลอดภัยเสมอ

ถ้าเว็บเรามีระบบสมาชิก ให้คนเข้ามาสมัครได้ แน่นอนว่าเราจะต้องทำการเก็บ ชื่อผู้ใช้ กับ รหัสผ่าน แต่จะเกิดอะไรขึ้นถ้า ระบบของเราโดนแฮก หรือผู้ดูแลระบบ คนในองค์กรไม่หวังดี นำข้อมูลรหัสผ่านออกไป? หนึ่งในวิธีลดความเสี่ยงของปัญหานี้คือเราสามารถ แปลงรหัสผ่านโดยการใช้ฟังก์ชันแฮชที่มีความปลอดภัยเช่น bcrypt ไปเป็นรูปแบบที่ไม่สามารถย้อนกลับไปเป็นรหัสผ่านจริง ๆ ได้โดยง่าย ถ้าไม่เคยทำมาก่อน ฟังดูเหมือนซับซ้อนแต่ ถ้าคุณใช้ framework เขียนเว็บ มั่นใจได้ว่าเกือบทุกยี้ห้อมีฟังก์ชันการแฮชรหัสผ่านให้แน่นอน เปิดคู่มืออ่านโล้ด ถ้าไม่มีตัวภาษาที่ใช้เขียนก็มีความสามารถทำได้ และอีกอย่างคือควรใส่ค่าสุ่มที่เรียกว่า salt ลงไปก่อนจะทำการแฮชด้วย เพื่อป้องกันการแฮชค่าเดียวกันของต่างผู้ใช้งานแล้วได้แฮชค่าเดิม ซึ่งบางอัลกอริทึมเช่น bcrypt จะใส่ไว้ให้อัตโนมัติแล้วไม่ต้องทำเองให้ยุ่งยาก เพิ่มเติมข้อควรระวังคือ อย่าใช้ ฟังก์ชันแฮชที่มีความปลอดภัยต่ำเมื่อเทียบกับตัวเลือกอื่นในยุคนี้แล้วอย่าง MD5 หรือ SHA1 โดยเด็ดขาด เรื่องเล่าคือผมไปเจอระบบที่หัวหน้าทีมพัฒนาระบบเค้าป้องกันไม่ให้โปรแกรมเมอร์ใช้ MD5/SHA1 โดยการทำการสแกนโค้ดเพื่อหาสองฟังก์ชันนี้เสมอหลังจากมีการส่งโค้ดมา ใน code repository ขององค์กรด้วย บางที่เด็ดกว่านั้นตั้งชื่อฟังก์ชันใหม่จาก MD5() ทำ wrapper ครอบเป็น VeryInsecureMD5() เพื่อสร้างความตระหนักทีมพัฒนาให้เลือกใช้แฮชที่ปลอดภัย เวลาเขียนโปรแกรม

ความลับอีกอย่างของแฮกเกอร์ที่โปรแกรมเมอร์มักไม่รู้กันคือ นอกจากรหัสผ่านแล้ว สิ่งที่คุณควรแฮชด้วยคือพวก password reset token และ token ที่ใช้ authenticate/authorize ทั้งหลาย เพราะว่าถ้าระบบมีช่องโหว่ให้เข้าถึงฐานข้อมูลได้ แฮกเกอร์อาจจะไปดึง password reset token มา reset รหัสผ่านเหยื่อได้โดยง่ายกว่าการดึงรหัสผ่านมาแคร๊กตรง ๆ

ใครสนใจหาข้อมูลเพิ่มเติมเรื่องนี้ สามารถดูได้ที่ Password Storage Cheat Sheet และ Forgot Password Cheat Sheet จาก OWASP ครับ

ส่วนรหัสผ่านของระบบเองเช่นรหัสที่ใช้เชื่อมต่อเข้า ฐานข้อมูลหรือต่อเข้าบริการ บุคคลที่สามต่าง ๆ ถ้าเลือกได้ไม่ควรเก็บในโค้ดหรือไฟล์โดยตรงแต่เก็บใน environment variable แทน เพื่อเวลาโยนโค้ดขึ้น git server หรือระบบต่าง ๆ กันเช่นระบบสำหรับทดสอบกับระบบจริงบน production จะได้ไม่มีรหัสผ่านของที่ใดที่หนึ่งติดมาด้วย แฮกเกอร์เค้ามี tool ชื่อ GitRob, Repo-Scraper และ gittyleaks เอาไว้คุ้ยหารหัสผ่านที่โปรแกรมเมอร์ลืมทิ้งไว้ในโค้ดที่ code repository ต่าง ๆ เช่น GitHub ด้วยละ !

ข้อควรระวังอีกอย่างคือบางครั้งเราใช้เว็บ framework หรือเว็บสำเร็จรูปบางตัวมันมี รหัสผ่านติดมาด้วยอย่าลืมตรวจสอบและแก้ไขก่อนด้วยเช่น รหัสผ่านเริ่มต้น บางครั้งอาจจะรวมถึงรหัสผ่านที่เอาไว้เข้ารหัสในเว็บแอปพลิเคชันด้วย เช่น เว็บย่อยของ Instagram เคยโดนแฮกเพราะใช้เว็บสำเร็จรูปเขียนจากRuby On Rails ที่ไม่ได้แก้ secret key สำหรับเข้ารหัสค่าต่าง ๆ ในเว็บ แฮกเกอร์เลย ใช้กุญแจเดียวกันนี้โจมตีช่องโหว่ของตัว framework จนสามารถยึด server ได้ เอา token กระโดดเข้าไปต่อถึงข้อมูลบน Amazon Web Services ที่เก็บรูปผู้ใช้เกือบทั้งหมดในโลกด้วย!

3. ใช้การสร้างค่าสุ่มที่มีความปลอดภัย

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

http://example.com/reset_password/?user=longcat&token=[ค่าสุ่ม]

จะเกิดอะไรขึ้น ถ้าเราสร้าง [ค่าสุ่ม] โดยการสุ่มเลขขึ้นมา 5 ตัว สมมุติได้ 76412 ? สิ่งที่จะเกิดขึ้นตามมาแน่ ๆ คือแฮกเกอร์จะไปกดลืมรหัสผ่านของผู้ใช้งาน longcat และเขียนโปรแกรมขึ้นมาลองเดาสุ่มค่า token นี้เรื่อย ๆ จาก 00000 ถึง 99999 จนสามารถเข้า link ที่ถูกและไปแก้ไขรหัสผ่านของใครก็ได้ในระบบ

จากตัวอย่างง่าย ๆ นี้เราจะเห็นว่าการสร้างค่าสุ่มที่มีความปลอดภัยมีความสำคัญมาก เราควรจะกำหนดความยาวเพื่อไม่ให้ถูกเดาได้โดยง่ายเช่นตั้งแต่ 12 ตัวขึ้นไป พร้อมทั้งใช้ฟังก์ชันที่สร้างเลขสุ่มที่ปลอดภัยด้วยอย่างในภาษา Java ก็จะมี class ที่ใช้สร้าง object สำหรับสร้างค่าสุ่มคือ java.util.Random แต่หารู้ไม่ว่า ฟังก์ชันนี้ไม่ปลอดภัยเพียงพอสำหรับการสร้างค่าที่ต้องใช้เป็นความลับ ซึ่งจะมีฟังก์ชันที่เหมาะสมกว่าสำหรับการสร้างค่าสุ่มที่มีความปลอดภัยคือ java.security.SecureRandom ส่วนถ้าคุณใช้ภาษาอื่น หรือเว็บ framework ก็ปรึกษาคู่มือว่าจะสร้างเลขสุ่มที่ปลอดภัยได้อย่างไร หนึ่งในวิธีที่แนะนำคืออาจจะใช้ฟังก์ชันที่สร้างค่าสุ่มที่เรียกว่า UUID version 4 จะหน้าตาประมาณนี้

123e4567-e89b-12d3-a456-426655440000

ก็ได้ถือว่ามีความปลอดภัยเพียงพอและยากต่อการเดา และอีกอย่างคือค่า token ใด ๆ ที่ออกแบบมาไม่ได้ให้เพื่อใช้ซ้ำหลาย ๆ ครั้งควรถูกยกเลิก (invalidate) หรือมีวันหมดอายุติดมาตรวจสอบตอนก่อนจะใช้งานด้วยเสมอ

ค่าอย่างนึงที่บางคนเข้าใจผิดและชอบนำมาใช้แทนค่าสุ่มคือเวลา หรือ Unix timestampไม่ว่าจะใช้แทนโดยตรงหรือใช้เป็น seed ของการสุ่ม ซึ่งไม่ว่าจะแบบไหนก็ไม่ปลอดภัยเพราะมันอาจถูกเดาได้และอาจนำมาสร้างมาเป็นลำดับเลขสุ่มและอีกหลาย ๆ ค่า ปลายปีก่อนก็มีกรณีเกมแนว slot machine บน Ethereum blockchain ชื่อ Slotthereum โดนเดาเลขที่จะถูกรางวัล jackpot ได้เพราะใช้ค่าสุ่มจาก block number ใน blockchain ซึ่งสามารถถูกโกงได้ มีนักวิจัยด้านความปลอดภัยมาแจ้งช่องโหว่นี้แต่โดนนักพัฒนาท้าว่าถ้าโกงได้จริง ให้ลองถูกรางวัลดูสิ ผลคือโดนสอยเงินรางวัล jackpot ออกไป

4. ตัวอย่างการออกแบบชื่อ path ของเว็บ API ที่ช่วยลดโอกาสจะเกิดช่องโหว่

เชื่อผมเถอะ ผมแฮกมาเยอะละจริง ๆ ถ้าคุณจะออกแบบ RESTAPI หน้าตาประมาณนี้

http://example.com/profile/user/123

เพื่อดึงข้อมูลผู้ใช้งาน เป็น JSON โดยเลข 123 เป็น user ID ที่ใช้อ้างอิงกับ ผู้ใช้งานที่กำลัง login ใน session ปัจจุบัน ถ้าคุณกำลังทำแบบนี้ ผมบอกได้เลยว่า ทำไม่ผิดหรอก แต่มันมีวิธีที่ดีกว่า และลดโอกาสที่จะโดนแฮกได้ดีกว่ามากถ้าคุณจะออกแบบให้ path มันหน้าตาเป็นแบบนี้

http://example.com/profile/user/me

และไม่ว่าจะ user ID 123 หรือ 124 หรือ 125 เข้ามาทุกคนก็จะยิงเข้ามาที่ path เดียวกัน เหตุผลคือ ถ้าคุณไม่ได้มี /123 ที่เดียวคุณมีอีกหลายที่ ที่ใช้ค่าอ้างอิงไปที่ ID แต่ละอย่าง มันอาจเกิดขึ้นได้บ่อยมากที่คนเขียนโปรแกรมมักจะพลาดไม่ได้เช็คสิทธิ์ (authorization) จาก 100 จุด อาจจะลืมสัก 1 จุด ก็เป็นได้ ทำให้สมมุติ ผู้ใช้งาน user ID 123 ไปลองเข้า link หน้าตาแบบนี้

http://example.com/profile/user/124

จะสามารถไปดึง ไปอ่านข้อมูลของ user ID 124 แทนของตัวเองได้ ถ้าลืมเช็คสิทธิ์ไว้ ! ช่องโหว่นี้มีชื่อเรียกว่า Insecure Direct Object Referenceหรือย่อว่า IDOR (อยู่ในความเสี่ยง A5 Broken Access Control ตาม OWASP Top 10 ปี 2017) เกิดได้กับทุกฟังก์ชันที่คุณสร้างและใช้ค่ามาอ้างอิงถึงข้อมูลของแต่ละผู้ใช้งานแยกกัน ผมเคยแจ้งช่องโหว่นี้ในโครงการ Bug Bounty Program และเคลมเงินได้หลายแสนบาทและแจ้งมานับไม่ถ้วนในระบบลูกค้า จึงอยากจะแนะนำว่าถ้าคุณออกแบบแต่แรกให้ดีซะอย่างกรณีแรก จาก /124 เป็น /me แทน เพื่อลดการใช้ object reference ที่ไม่จำเป็นให้มากที่สุดที่จะมากได้ และเช็คสิทธิ์ซ้ำอีกที อีกตัวอย่างการออกแบบที่เน้นเรื่องความปลอดภัยคือ สมมุติระบบมีการเก็บ order ของผู้ใช้ที่เข้ามาซื้อของในเว็บ ในฐานข้อมูลเราอาจจะออกแบบ order ID มาเป็นเลขเรียง 1, 2, 3, 4, 5 เทคนิคนึงที่ควรใช้คือสมมุติ ผู้ใช้งาน longcat สร้าง order มา 2 ครั้ง เป็น order ID ในระบบเราภายในเก็บเป็น 456 กับ 567 เราอาจจะทำการ mapping แทนที่จะออกแบบ Web API หน้าตาแบบนี้

http://example.com/store/myorder/456
http://example.com/store/myorder/567

มันจะดีกว่ามากถ้าเราทำการ mapping โดยใช้เลข 1 ครั้งแรกของผู้ใช้คนนั้นแทนที่เป็น 456 ส่วนครั้งที่สองเป็น 2 สำหรับแทนที่ของ order ID ภายใน567 เวลาผู้ใช้งานมี session อยู่เข้าผ่านเลข 2 อย่างเช่น

http://example.com/store/myorder/1

ก็จะวิ่งไปที่ order ID เลข 456 แทน ประโยชน์คือ เพื่อป้องกัน internal state ของ order ID เปิดเผยออกมา และป้องกันช่องโหว่ในทุก ๆ จุดที่เราอาจจะลืมเช็คทำให้ ผู้งานเข้าไปดู order ของคนอื่นได้นั้นเอง และอย่าลืมว่าไม่ว่าคุณจะใช้ object reference แบบใด เวลาผู้ใช้งานเรียก ด้วย Id นั้น ๆ หรือฟังก์ชันใด ๆ ต้องมีการเช็คสิทธิ์ ก่อนอนุญาตให้เข้าถึงข้อมูลได้เสมอ วิธีการเขียนการตรวจสอบสิทธิ์ทั้ง การยืนยันตัวผู้ใช้และการยืนยันสิทธิ์ของผู้ใช้งาน ที่ดีคือใช้ framework หรือเทคนิคที่สามารถ รวมการเช็คสิทธิ์แบบศูนย์กลางที่เดียวกันได้ คือตั้งที่เดียวให้มันเช็คสิทธิ์ทั้งหมดทุกจุด อย่าพยายามแยกไปเช็คหลาย ๆ ที่ เพราะแยกแล้วโอกาสพลาดสูงกว่า

ตัวอย่างง่าย ๆ เช่น ถ้าเรามีตัวเลือกระหว่างเช็คว่า ผู้ใช้งาน login รึยัง ในไฟล์หน้าเว็บ 10 หน้า เราอาจจะเขียนไว้ที่ super class หรือ จุดที่ HTTP request วิ่งเข้าอย่างจะเป็น routing class ที่ก่อนจะวิ่งไป 10 หน้านั้นมันจะวิ่งมาตรงนี้ก่อนแน่ ๆ แล้วดักแก้มาเช็คว่าผู้ใช้งาน login รึยังตรงนี้แทน เพื่อลดโอกาสว่าในอนาคตมีโปรแกรมเมอร์เข้ามาใหม่ มาจากทีมอื่นมาเพิ่มหน้าที่ 11, 12 … จะได้ไม่ลืมเช็ครวมถึงการตรวจสอบสิทธิ์ต่าง ๆ ด้วย role-based access control เช็คว่า ผู้ใช้งานที่เข้ามาด้วย role ที่เรากำหนดเท่านั้นถึงจะใช้ฟังก์ชัน ถึงจะเข้าถึงข้อมูลบางอย่างได้ หรืออาจจะใช้อีกแนวทางคือ resource-based access control เช่นกำหนดว่า คนที่จะเข้าถึงฟังก์ชันนี้ได้จะต้อง มีสิทธิ์จะทำอะไร เช่น ถ้าผู้ใช้งานคนนึงจะเข้าถึง ฟังก์ชันสร้างห้องประชุมได้ จะต้องมีสิทธิ์ board Member แต่อาจจะมีมากกว่า 1 role ก็ได้ที่มีสิทธิ์นี้ ทำให้เวลาเราเขียนโค้ดเราสามารถแตกสิทธิ์ แตก role เพิ่มได้ลดได้โดยไม่จำเป็นต้องแก้โค้ดเก่า ถ้าเราออกแบบมาอย่างดีตั้งแต่ต้น ตัวอย่าง framework ที่ช่วยจัดการเรื่องนี้ได้เช่น Apache Shiro

Figure 2: ฟีเจอร์ของ Apache Shiro รูปจาก https://shiro.apache.org/introduction.html

5. การเก็บข้อมูลที่อัปโหลดจากผู้ใช้งานที่ปลอดภัย

ถ้าระบบเรายอมให้ผู้ใช้งานอัปโหลดรูปหรือเอกสาร ภายใต้เงื่อนไขข้อมูลเก็บบน cloud ได้มีงบและระบบสเกลขนาดกลางขึ้นไป ควรแยกไปไว้ระบบเก็บไฟล์ static โดยเฉพาะอย่างผู้ให้บริการ CDN เช่น AWS S3 ไม่ควรเก็บในระบบเดียวกันกับแอปพลิเคชัน เพราะมีความเสี่ยงหลายอย่างเช่น harddisk เต็มแล้วระบบจะล่มทั้งหมด หรือไฟล์ที่อัปโหลดเข้ามามีโค้ดอันตรายต่อแอปพลิเคชันของเรา มันจะดีกว่ามากถ้าเราแบ่งความเสี่ยงตรงนี้ไปที่ระบบเก็บฝากโดยเฉพาะเลย แต่ไม่ว่าเราจะเลือกทางนี้หรือไม่ แน่นอนว่าต้องตรวจสอบให้ละเอียดคือผู้ใช้งาน อัปโหลดไฟล์ที่เราต้องการเข้ามาจริง ๆ เช่นเช็คนามสกุลไฟล์ว่าเป็น .png .jpg .gif รึเปล่า มีเนื้อหาถูกต้องตามไฟล์ที่เราอยากได้มี magic numbersขึ้นต้นไฟล์ถูกต้อง ขนาดไม่ใหญ่เกิน จำนวนไฟล์ไม่มากเกิน ถ้านอกเหนือจากเงื่อนไขที่เรากำหนดเหล่านี้จะต้องให้ไม่ยอมให้อัปโหลด หรือดีกว่านั้น ในกรณีที่ยอมให้อัปโหลดแล้ว ควรลบข้อมูลพวก metadataออกจากรูปหรือเอกสารด้วย เพื่อปกป้องผู้งานเว็บเราจาก ข้อมูลส่วนตัวเปิดเผยออกมาจากข้อมูล EXIF รูปถ่ายหรือชื่อ username ชื่อเครื่องที่อาจติดมากับไฟล์เอกสารต่าง ๆ

จุดที่ต้องระวังอย่างนึงของการใช้บริการ public cloud ที่เป็น บุคคลที่สาม โดยเฉพาะ AWS S3 เพราะว่าตัว bucket สำหรับเก็บข้อมูล สามารถตั้งค่าให้ใครก็ได้ อ่าน/เขียน ข้อมูลที่เราเก็บไว้ได้ผลคือมีคนมักง่าย เปิดสิทธิ์ให้ใครก็ได้เข้าได้ เพื่อว่าจะได้ใช้งานง่าย ๆ หรือไม่เข้าใจว่าการตั้งแบบไหนไม่ปลอดภัย ผลคือโดนคนอื่นมาล้วงข้อมูลไว้ กันรัว ๆ ในช่วงหลายปีที่ผ่านมาทั้ง Uber ข้อมูลผู้ใช้งาน 57 ล้านรายรั่ว,ข้อมูลลูกค้าหลายร้อยกิ๊กของ Accenture และเคสอื่น ๆ อีกมากมาย ทั้งเป็นข่าวและไม่เป็นข่าว ข่าวดีคือมีโปรแกรมชื่อ S3 Inspector ช่วยตรวจสอบ AWS S3 bucket ที่เราเปิดมาใช้ได้ว่าตั้งค่าไม่ปลอดภัยรึเปล่า

 

Figure 3: ตัวอย่างการใช้งานโปรแกรม S3 Inspector รูปจาก https://github.com/kromtech/s3-inspector

6. นอกจาก API สำหรับผู้ใช้งานแล้ว API ที่คุยกันระหว่าง server ก็ต้องออกแบบให้ปลอดภัย

ระบบขนาดกลางถึงขนาดใหญ่ นอกจากจะมีการให้ ผู้ใช้งานฝั่ง end-user เรียก API เพื่อใช้ฟีเจอร์ต่าง ๆ แล้ว ตัวระบบใน API นั้น ๆ เอง มักจะต้องไปเรียก API ที่ server อื่นต่ออีกที เพื่อทำงานอะไรบางอย่างที่ทำเองในระบบตัวเองไม่ได้ เช่นการดึงข้อมูลจากระบบอื่นมาใช้ การคุยกันระหว่าง API สองตัวโดยไม่ผ่านผู้ใช้งานฝั่ง end-user เราเรียกว่า server-to-server communication

บ่อยครั้งในกระบวนการออกแบบซอฟต์แวร์ มีการออกแบบมาอย่างไม่ปลอดภัย โดยให้ผู้ใช้งานฝั่ง end-user ยิงเข้า API ตัวที่ควรทำเป็น server-to-server communication ได้โดยตรง ผลคือ การตรวจสอบค่าต่าง ๆ ที่ควรทำกลับไม่ได้ถูกตรวจสอบ

Figure 4: ตัวอย่างการออกแบบระบบที่ใช้ Web API 3 ตัวในฝั่ง backend

ตัวอย่างจากรูป diagram เหตุผลที่ปัญหานี้เกิดได้จากหลายสาเหตุ อย่างแรกคือ ตั้งแต่ตอนออกแบบ software architecture ทางผู้พัฒนา ไม่ได้เอาเครื่อง Web API #3 กับ เครื่อง Web API #2 ไปใส่ไว้ใน internal network ที่เข้าถึงได้เฉพาะเครื่อง Web API #1 หรือไม่ได้มีการทำ network access control ที่กำหนดว่าให้เครื่อง IP 1.2.3.5 กับ 1.2.3.5 เข้าถึงได้เฉพาะจาก IP 1.2.3.4 เท่านั้น เมื่อ ผู้ใช้งานยิง API ไปที่เครื่อง Web API #2 หรือ #3 โดยตรง ก็สามารถเข้าถึงข้อมูล หรือฟังก์ชันที่ ปกติแล้วจะมีการตรวจสอบความปลอดภัยจาก Web API #1 โดยไม่มีการป้องกันได้ ภายใต้เงื่อนไขว่า ผู้โจมตีระบบสามารถล่วงรู้ API Spec ของWeb API #3 ได้ ซึ่งในกรณีที่เกิดขึ้นจริง สามารถรู้ได้จากหลายสาเหตุเช่นผู้พัฒนาออกแบบ API หน้าตา pattern คล้ายกันหรือมี JavaScript กับแอปมือถือ ที่ฝังโค้ดยิงเข้า API เหล่านั้นอยู่สามารถถูกแกะมาดูได้

อย่างที่สองคือ ผู้พัฒนาโปรแกรม อาจไม่คิดว่าผู้ใช้งานทั่วไป จะสามารถแก้ไขค่าที่ส่งมาอัตโนมัติจากใน web browser ได้ และกำหนดให้หน้าเว็บฝั่ง frontend สำหรับ Web API #1 เก็บค่าบางอย่าง สมมุติชื่อ username ไว้ในฝั่ง client-side เช่น local storage หรือ cookie หรือในตัวแปร JavaScript แล้วจากนั้นใช้ JavaScript  เพื่อยิงค่าเหล่านั้นที่เก็บไว้ใน client-side กลับไปหา Web API #2 เมื่อแฮกเกอร์มาวิเคราะห์การออกแบบระบบแบบนี้แล้ว สิ่งที่จะเกิดขึ้นได้คือ ค่าที่ส่งจาก web browser สามารถถูกดักแก้ไขได้ด้วยการตั้งค่า proxy และใช้โปรแกรมมาดักแก้ไขกลางทาง แก้ไขค่า username จาก user1 เป็น user2 จากนั้นก็ส่งค่าเหล่านั้นกลับไปยัง Web API #2 เพื่อข้ามผ่าน การตรวจสอบ ณ จุดที่ Web API #1 ทั้งหมด ตัวอย่างของจริงในกรณีนี้ที่เคยเห็นก็มี เกมออนไลน์เกมนึง หน้าเว็บยอมให้ล็อคอินผ่าน Facebook ได้ พอคนเล่นเกมล็อคอินเสร็จ API ของ Facebook จะคืนค่า ID ที่ใช้อ้างอิงว่าผู้ใช้งานคนนั้นเป็นใครมา (ขอเรียกว่า FB ID ละกัน) เช่น 1234 เป็นนาย A ปรากฏว่าแอปใช้โค้ดฝั่ง client-side ยิงค่า FB ID 1234 นี้กลับไปที่ API อีกตัวนึง เพื่อว่า server เกมจะรู้ได้ว่าผู้ใช้งาน FB ID 1234 ล็อคอินเข้ามาเล่นเกมและยอมให้เข้าเกมได้ ปรากฏว่าแฮกเกอร์ ที่สามารถรู้ค่า FB ID ของเหยื่อได้ (ซึ่งได้จากหลายกรณีเช่นมีฟีเจอร์อื่นที่ดึง FB ID ออกมาได้ หรือค่า FB ID นั้นเป็นเลขที่สามารถเดาได้โดยง่าย) สามารถแฮกล็อกอินเข้าไปในเกม เป็นผู้ใช้งานที่ล็อคอินผ่าน Facebook คนอื่นได้ เป็นช่องโหว่ระดับร้ายแรงสูง

อีกตัวอย่างที่น่าสนใจ กลับด้านกันเลยคือ การทำงานบางอย่างก็ควรให้ ผู้ใช้งานส่งข้อมูลเข้า Web API ปลายทางโดยตรงดีกว่า อย่างถ้าต้องมีการส่งข้อมูลสำคัญ และเราในฐานะเจ้าของระบบ Web API #1 นั้นไม่มีความจำเป็นต้องรู้ข้อมูลเหล่านั้น ตัวอย่างชัดเจนที่สุดคือ ระบบจ่ายเงินตัดบัตรเครดิตผ่าน payment gateway ทางเลือกที่ดีกว่า คือให้ ผู้ใช้งานส่งข้อมูลไปจ่ายเงินกับ API ของ payment gateway เองโดยตรง เพื่อที่ข้อมูลที่ผู้ใช้งานอย่าง ตัวเลขบัตรเครดิต ไม่จำเป็นต้องส่งผ่านระบบเรา เป็นการโอนถ่ายความเสี่ยงในกรณีที่ถ้าระบบเราโดนแฮก ข้อมูลบัตรเครดิต หรือข้อมูลที่ไม่จำเป็นต้องเก็บหรือส่งผ่านในระบบเราจะได้ไม่โดนแฮกไปด้วย และเราก็เก็บข้อมูล ID ที่อ้างอิงถึงเลขบัตรเครดิตเหล่านั้นไว้ใช้งานแทน (เรียกว่า tokenized PAN)

สุดท้ายจะมีเทคนิคนึงที่จะนำมาใช้เพิ่มความปลอดภัยให้ การคุยกันระหว่าง server ได้ (ถ้าตามรูป diagram คือการคุยกันระหว่าง Web API #1 กับ Web API #2 และ #3) คือการใช้ JWT(JSON Web Token) หลายคนอาจจะเคยได้ยินหรือเคยใช้แล้ว สรุปง่าย ๆ ก็คือมันคือเทคนิคที่ช่วยทำให้ยืนยันว่าข้อมูลที่เราจะส่งปลายทางจะได้รับถูกต้อง หรือข้อมูลที่เราจะรับเป็นข้อมูลที่ต้นทางจะส่งมาจริง ๆ ด้วยการเอา กุญแจดิจิทัล มาทำลายเซ็นไว้แนบไปกับข้อมูลด้วย เรียกว่าการทำ message authentication หรืออาจจะมากกว่านั้นคือเข้ารหัสด้วยก็ได้ ซึ่งทำให้ server ทั้งฝั่งผู้รับและผู้ส่งข้อมูลมั่นใจได้ว่า ข้อมูลที่คุยกันไม่ได้ถูกแก้ไขปลอมแปลงระหว่างทางอ่านเพิ่มเติมเรื่อง JWT ได้ที่ บทความของคุณ @chaintng

Figure 5: การเพิ่มความปลอดภัยให้การคุยกันระหว่าง server ด้วย JWT

7. ควรทำการตรวจสอบค่าที่รับเข้ามาจากผู้ใช้งานเสมอ

ความเสี่ยงที่ระบบมักจะโดนแฮกมากที่สุด จัดอันดับโดย OWASP TOP 10 ปี 2017 คือ ความเสี่ยงของช่องโหว่ในตระกูล Injection ซึ่งมีตั้งแต่ SQL Injection, Command Injection, Code Injection และอื่น ๆ อีกมากมาย ตัวอย่างเช่นเว็บ API เรารับค่าเข้ามาแบบนี้

POST /concert/bnk48/edit/123
{"title": "First Concert 2018"}

เมื่อเรารับค่า title เข้ามาเป็น “First Concert 2018″สิ่งที่อาจจะเกิดขึ้นได้อย่างแรกคือ ถ้าเรานำไปเก็บลงฐานข้อมูล แต่ไม่ได้ใช้วิธีการที่ปลอดภัยเช่นนำค่าที่รับมาไปใส่ใน SQL query โดยตรง

update concert set title="First Concert 2018" where id=123;

อาจทำให้โดนแฮกได้ ถ้าแฮกเกอร์ส่งค่าเข้ามาเป็น

{"title":"Turtle Turtle!!\"; drop table concert --"}

ถ้าตัว database driver และ DBMS ที่ใช้รองรับการทำ stack query ก็จะทำให้แฮกเกอร์นอกจากจะอัพเดท title ของข้อมูลทุก record เป็น “Turtle Turtle!!” และยังทำการ ลบ (drop) ข้อมูลใน table ชื่อ concert ทั้งหมดต่ออีกด้วย คำสั่งใน SQL query ก็จะออกมาหน้าตาเป็นแบบนี้แทน

update concert set title="First Concert 2018"; drop table concert --" where id=123;

การโจมตีนี้ที่แฮกเกอร์ใส่คำสั่ง SQL เพิ่มเติมเข้ามา และถูกนำมาประมวลผลเป็น SQL query เราเรียกว่าช่องโหว่ SQL Injection ซึ่งป้องกันได้ด้วยการใช้ prepared statement อย่างถูกต้องหรือเรียกใช้ฟังก์ชันเชื่อมต่อฐานข้อมูลจากตัวเว็บ framework หรือ library ที่ไม่ได้เป็นการเอาค่าที่รับมาไปต่อข้างในค่า SQL queryโดยตรง ถ้าใช้อย่างถูกวิธี ก็จะป้องกันให้โดยอัตโนมัติแล้ว สรุปคือ อย่าเอา ข้อมูลจาก ผู้ใช้งาน ไปต่อ string ใน SQL query โดยตรงเด็ดขาด! รวมถึงใน การสั่งการคำสั่ง OS command ด้วยเช่นถ้าค่า title ไหลเข้าไปในฟังก์ชันของ Node.js ต่อไปนี้

const{ execSync } = require('child_process');
let stdout = execSync('java -jar concert.jar "'+title+'"');

แฮกเกอร์ก็สามารถที่จะแก้ค่า title เป็น

{"title":"Turtle Turtle!!\"; rm -rf — no-preserve-root /"}

เพื่อที่จะ inject คำสั่ง OS Command อื่น ๆ เข้ามาลบข้อมูลในเครื่องserver ของเราได้ ซึ่งพอแปลงตัวแปรใส่ไปในโค้ดเพื่อให้เห็นภาพง่ายขึ้นคำสั่งที่ได้ จะกลายเป็น

let stdout = execSync('java -jar concert.jar "Turtle Turtle!!"; rm -rf --no-preserve-root /');

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

ตัวอย่างเช่นชื่อคนก็ควรจะมีเฉพาะ ก-ฮ a-z A-Z แต่ถ้ามี เหล่า quote ต่าง ๆ ‘” โผล่มาเราก็ไม่ควรจะยอมรับแล้ว หรือเบอร์โทรศัพท์ควรมีแค่ 0–9 เราก็ไม่ควรยอมให้ผู้ใช้งานใส่ค่าที่ไม่ควรใส่เข้ามาได้ ได้ ซึ่งการตรวจสอบทั้งหมด สำคัญมากว่าจะต้องตรวจสอบฝั่ง server-side เพราะถ้าตรวจสอบผ่าน client-side โดยใช้ JavaScript สามารถถูกหลบเลี่ยง (bypass) ได้เสมอ ทางที่ดีควรใช้คู่กัน ใช้ JavaScript ตรวจก่อน 1 ชั้นเพื่อ UX ที่ดี ถ้าไม่ถูกจะได้ไม่ต้องส่งค่าไปแล้วรอ server ตอบกลับ จากนั้นเช็คฝั่ง server-side อีก 1 ชั้นเพื่อเช็คความปลอดภัยของค่าที่ส่งเข้ามาอย่างแท้จริง อีกเทคนิคนึงถ้าเราไม่อยากจะ ไม่ยอมรับค่าที่ผู้ใช้งานส่งเข้ามา แทนที่เราจะปฏิเสธ ตรง ๆ เราอาจทำการทำให้ค่าที่ถูกส่งเข้ามาปลอดภัยเอง (sanitize) และนำไปใช้ต่อเลย เช่นการลบอักขระอันตรายออกโดยอัตโนมัติ หรือมีการทำ encoding/encryption ที่เหมาะสมก่อนนำไปเก็บหรือไปใช้ ก็จะช่วยลดความเสี่ยง เพิ่มความปลอดภัยได้

โดย best practice ของการเขียนโค้ดที่ต้องตรวจสอบค่าของผู้ใช้งานนั้น ควรมีการเรียกใช้งาน utility class หรือ library ที่พิสูจน์และเป็นที่ยอมรับแล้วว่าปลอดภัย และทุก ๆ ครั้งที่จะทำการตรวจสอบในกรณีเดียวกันควรจะต้องเรียกใช้งานจากutility class นั้น ๆ ที่เดียว เพื่อป้องกันในทุกจุดในกรณีเดียวกันให้เหมือนกัน สมมุติถ้า มีช่องโหว่เกิดขึ้นใน utility class นั้นเราก็จะได้แก้จุดเดียว เพื่อว่าให้ง่ายต่อการ maintenance การเพิ่มฟีเจอร์ การ test การ integration กับระบบอื่นและลดข้อผิดพลาดจากกรณีว่า ถ้าเราเขียนไว้หลาย ๆ ที่จาก 100 ที่ อาจจะมีสัก 1 ที่ ๆ ลืมหรือเขียนผิด ถ้าเราบังคับให้ใช้จากที่เดียวเลยก็จะแก้ปัญหานี้ได้ดีขึ้น

8. คอยตามข่าวช่องโหว่ใน component ที่เราใช้งาน

เวลาเราจะสร้างเว็บ API หรือระบบขึ้นมาสักอย่าง มีโอกาสน้อยมากที่เราจะเขียนโค้ดเองทั้งหมด เรามักจะใช้เว็บ framework หรือ library ของคนอื่นเข้ามาในโค้ดด้วยเสมอ เช่นใช้เพื่อทำฟีเจอร์อ่าน QR code ใช้เพื่อทำ rich-text editor หรืออ่านค่ามาจากเอกสาร Excel ที่ผู้ใช้งานอัปโหลดมาบนเว็บเป็นต้น

และบ่อยครั้งแฮกเกอร์ก็มักจะพบว่าโปรแกรมเมอร์เขียนโค้ดระบบตัวเองถูกต้องปลอดภัย แต่ดันไปใช้ component ของคนอื่น ที่มีช่องโหว่ ตัวอย่างเช่นเว็บที่เขียนภายใต้เฟรมเวิร์คของ Apache Struts 2 วันดีคืนดีก็มีคนไปพบว่าสามารถยิงโค้ดอันตราย (exploit) เข้ามาและเจาะระบบยึดเครื่องได้ ซะอย่างงั้น เพราะฉะนั้นเวลาเราออกแบบ พัฒนาระบบ เราก็ควรจะใช้งานเวอร์ชั่นล่าสุดในขณะนั้นเสมอเพื่อความปลอดภัยสูงสุด พร้อมทั้ง ทำเอกสารบอกถึงรายชื่อและเวอร์ชันของ component ที่เราใช้ เพื่อว่าถ้าในอนาคตถ้า component ที่เราใช้มีช่องโหว่เราจะได้ ตรวจสอบได้ง่าย และนำเวอร์ชันใหม่มาสับเปลี่ยนกันได้สะดวกมากยิ่งขึ้น เพื่อความปลอดภัยนั้นเอง แน่นอนว่าระบบเราทำมาอายุอาจจะยาวนานเป็น 10 ปีในช่วงเวลานั้น component ต่าง ๆ ก็มีโอกาสมีคนไปเจอช่องโหว่ได้แน่นอนเคล็ดลับก็คือถ้าใครเขียนระบบด้วย Java หรือ C#  ที่ใช้ Maven/Ant/Gradle/Jenkins เป็นตัว build จะมี plugin ชื่อ OWASP Dependency Check ใช้ช่วยสแกนหา ช่องโหว่ใน component ที่ใช้งานในระบบได้ส่วนช่องโหว่ใน library ภาษา JavaScript ฝั่ง client side ก็สามารถใช้แอปอีกตัวคือ Retire.js สแกนหาช่องโหว่ได้

Figure 5: ตัวอย่างการแฮกช่องโหว่ใน Elasticsearch เวอร์ชั่นเก่า (CVE-2015-1427) แค่มี IP แฮกเกอร์ก็ยึดเครื่อง server ได้แล้ว

9. ควรหลีกเลี่ยงเทคนิคการเขียนโปรแกรมที่ต้องทำการ serialize ตัว object ใด ๆ

ปกติเราเขียนโปรแกรมมีฐานข้อมูล ถ้าเรามี ข้อมูลเช่น abc 123 เราสามารถเก็บค่า primitive type ง่าย ๆ เหล่านั้นลงในฐานข้อมูล หรือไฟล์ธรรมดาได้ แต่มันก็จะมีบางเหตุการณ์ ที่เราอาจจะออกแบบมา ด้วยเหตุผลอะไรไม่รู้แหละ อาจจะเพราะความง่ายก็ได้ อยากจะนำ object ที่เราสร้างมาจาก class อะไรสักอย่าง ซึ่งพ่วงด้วย property ของ object ต่าง ๆ เนี่ย เก็บลงไปใน ไฟล์หรือในฐานข้อมูล ซึ่งเทคนิคการแปลง object ไปเป็นค่าที่เก็บได้พวกนี้เราเรียกว่าการทำ serialize และการแปลงกลับคือการทำ deserialize

ล่าสุดผมได้มีโอกาสไปสำรวจแอปพลิเคชันตัวนึงเขียนด้วย Ruby On Rails และปรากฏว่ามีฟีเจอร์ export/backup ข้อมูล พอเปิดข้อมูลออกมากลายเป็น serialized data ชื่อว่า Marshal ซึ่งมีในหลายภาษาโปรแกรม ซึ่งพอแกะดูโดยละเอียดแล้วเป็น object ของ class ๆ นึงเวลา เรา import กลับเข้าไป ตัวแอปพลิเคชันก็จะเอาค่า object นั้นแปลงกลับไปใช้งาน ผลปรากฏว่าด้วยเทคนิคเล็ก ๆ น้อย ๆ แฮกเกอร์ก็สามารถเปลี่ยนค่าใน object นั้นได้ตามใจชอบเช่นสมมุติถ้าเป็น Bitcoin Wallet object บอกว่าเรามีเงิน 10 BTC เราก็แก้เป็น 100,000 BTC แล้ว import กลับเข้าไปโกงค่าเงินที่เรามีได้เป็นต้น หรือร้ายแรงกว่านั้นสามารถสับเปลี่ยน object เป็นโค้ดอันตรายเพื่อยึดเครื่อง server  เจาะทะลวงเข้าไปในระบบปฏิบัติการณ์ได้ทันที

วิธีการที่ใช้แทนการ serialize คือเก็บค่าแบบ primitive type แบบเดิมเป็น list / array แล้วแต่ภาษาและแปลงออกมาเป็น JSON มาเก็บแทน เวลาอยาก import คืนก็เอาค่าเหล่านั้นมา assign ให้ property ต่าง ๆสำหรับ object ที่เราต้องการ หรือหาทางออกแบบวิธีอื่นที่ไม่ได้ deserialize ค่าที่รับมาโดยตรงถ้าเลี่ยงไม่ได้จริง ๆ ใช้ทำการ whitelist เฉพาะ object ที่ต้องการเท่านั้นอย่างระมัดระวังสุด ๆ และการลดความเสี่ยงแบบสุดท้ายคือการทำ blacklist ในส่วนของค่าที่ไม่ปลอดภัย ตัวอย่างเช่นการ deserialization ใน Java มี library ชื่อ SerialKiller ช่วยทำ blacklist/whitelist ได้

ในแต่ละภาษาโปรแกรมอื่น ๆ ไม่ว่าจะเป็น PHP Ruby Python C# .NET ก็จะมีเทคนิคคล้าย ๆ กันนี้ในการ serialize/deserialize ค่า Object ซึ่งวิธีการแฮกแตกต่างกันและค่อนข้างซับซ้อน คุณอาจไม่จำเป็นต้องรู้ก็ได้ แต่สิ่งที่ต้องรู้คือ ควรหลีกเลี่ยงการ serialize/deserialize ทุกชนิด โดยเฉพาะกับค่าที่รับมาจากผู้ใช้งานในระบบ ซึ่ง OWASP Top 10 ปี 2017 ได้เพิ่มช่องโหว่นี้เข้ามาอยู่ในอันดับ 8 เพราะว่าก่อนหน้านี้ช่องโหว่นี้แลดูลึกลับฝั่งนักพัฒนาโปรแกรมส่วนมากไม่รู้จักกัน ใครสนใจก็ตามไปอ่านเพิ่มเติมได้ที่ Top 10–2017 A8-Insecure Deserialization

Figure 7: การแฮกด้วยช่องโหว่ insecure deserialization ใน ruby on rails ทำให้แฮกเกอร์ยึดเครื่อง server ได้

10. การทำนโยบาย Cross-Origin Resource Sharing อย่างปลอดภัย

ปกติเวลาเราเขียนเว็บ API ขึ้นมา หลายครั้งเราอยากให้เว็บใน domain อื่นเว็บอื่น (ภาษาเทคนิคเรียกว่าต่าง origin) ของเราหรือของคนอื่น เรียกใช้ได้ด้วย AJAX ซึ่งตัว web browser เวลาต่างเว็บส่ง AJAX หากัน จะมีกระบวนการตรวจสอบความปลอดภัยว่า เว็บหนึ่งควรจะใช้ AJAX เข้าถึงข้อมูลอีกเว็บนึงได้รึเปล่า โดยใส่ HTTP Header ตัวนึงมาชื่อว่า Origin: ตามด้วยชื่อเว็บที่ยิง AJAX นั้นออกไปเพื่อบอกว่าคนมาขอข้อมูลเป็นใครเช่น

GET /api/v1/bookCollections/
Host: www.originA.com
Cookie: token=abc123
Referer: https://www.originB.com/somepage/
Origin: https://www.originB.com
Vary: Origin

และเว็บปลายทาง B ที่รับ AJAX ก็จะตอบกลับมาว่า ยอมหรือไม่ยอมให้อ่านข้อมูลเช่น

HTTP/1.1 200 OK
Server: Apache
Access-Control-Allow-Origin: https://www.originB.com

ซึ่งตรงนี้เป็นฉบับย่อของกระบวนการทำ Cross-Origin Resource Sharing หรือย่อว่า CORS ซึ่งเกิดจาก web browser เป็นคนกลางช่วยเช็คให้ว่าเว็บ A ควรจะยิง AJAX หา B เพื่อรับส่งข้อมูล (resource) อะไรมาใช้งานได้รึเปล่าเป็นเรื่องของ trust relationship ระหว่าง A กับ B ว่า อยากให้ดึงข้อมูลข้ามกันไปใช้ได้ไหม ซึ่งเวลาเราออกแบบเว็บ API เราควรจะต้องรู้และมี whitelist ว่าเว็บอะไร ที่เราอนุญาติให้มาดึงข้อมูลเราได้บ้างเอามาตอบใน

Access-Control-Allow-Origin: <ตรงนี้>

เหมือนในตัวอย่าง ซึ่งในทาง web application security จะมีช่องโหว่นึงชื่อว่า Overly Permissive CORS คือถ้าเรา ไปอนุญาตว่า ไม่ว่าเว็บอะไร Origin ไหนยิง AJAX มาเว็บเราให้เอา Referer ที่ติดมานั้นไปใส่ใน Access-Control-Allow-Origin header<ตรงนี้> (ขอเรียกย่อว่า ACAO ละกัน) ทันที จะทำให้แฮกเกอร์สามารถสร้างหน้าเว็บขึ้นมา ส่งให้เหยื่อเปิด พอเหยื่อเปิดแล้ว ยิง AJAX ไปอ่านข้อมูลเว็บที่เหยื่อ login ทิ้งไว้อยู่ ดึงข้อมูลมาให้แฮกเกอร์อ่านได้

ตัวอย่างเช่น สมมุติถ้า Facebook มีช่องโหว่นี้ แฮกเกอร์ก็สามารถสร้างเว็บอะไรก็ได้สมมุติ www.attacker.com ส่งให้เหยื่อเปิด พอเหยื่อเปิด เว็บนี้ก็จะยิง AJAX เข้าไปอ่านข้อมูล หน้า Facebook Messenger ว่าคุยอะไรกับใคร ดึงข้อความมา เก็บในตัวแปร JavaScript แล้วยิงค่านั้นอีกที กลับไปหา server ของแฮกเกอร์เพื่อ log ข้อมูลลับของเหยื่อเก็บไว้ได้ ซึ่งเหตุการณ์นี้เคยเกิดขึ้นจริงแล้ว Critical Issue Affects Privacy of 1-Billion Facebook Messenger Users จากตัวอย่างนี้เราจะเห็นว่า การตั้ง ACAO ไม่ปลอดภัยทำให้ผู้ใช้งานเว็บโดนแฮกได้ ควรกำหนดให้ปลอดภัย บางเว็บอาจใช้เป็น * แทนก็อาจจะได้ ถ้าข้อมูลที่จะให้เว็บอื่นดึงไม่ต้อง login เพราะ web browser จะไม่ส่ง cookie ไปถ้า ACAO เป็น * ทำให้ดึงได้แต่ public content ซึ่งส่วนมากไม่เป็นอันตราย แต่อย่าตอบเป็นชื่อเว็บทุกเว็บกลับไปหรือตอบเป็นค่า null กลับไปหาเว็บอะไรก็ไม่รู้เด็ดขาดอ่านเพิ่มเติมได้ที่ Exploiting CORS Misconfigurations for Bitcoins and Bounties

สิ่งสุดท้ายคือทำการทดสอบทางด้านความปลอดภัยให้ Web API ก่อนเปิดให้ใช้งาน

ผมเชื่อว่า ยากมากที่เราจะทำระบบใด ๆ ให้ปลอดภัย 100% ขนาด Google, Facebook และ Microsoft ที่มี software engineer ระดับโลก เค้ายังเขียนโค้ดมีช่องโหว่ โดนแฮกกันได้ เหตุผลเพราะอะไรนั้นต้องคุยกันยาว เช่นความซับซ้อนของระบบ, มนุษย์มีข้อผิดพลาดได้, เกิดจากการทำงานร่วมกันที่เข้าใจต่างกัน ฯลฯ ดังนั้นหลังจากเราทำซอฟต์แวร์เสร็จแล้ว ต่อให้เราใส่ใจเรื่องความปลอดภัยในกระบวนการพัฒนามากแค่ไหน ในSDLC ตอนก่อนจะเปิดให้ใช้งานจริง ควรมีการทดสอบด้านความปลอดภัยก่อนเสมอ โดยอย่างน้อยที่สุดก็อาจจะแค่ว่าใช้โปรแกรมเช่น OWASP ZAP หรือw3af มาร่วมautomate หรือ manual ทดสอบความปลอดภัยว่า

มีการ ตรวจสอบข้อมูลที่รับเข้าไปจริงไหม?
มีการ ตรวจสอบสิทธิ์ว่าระหว่างผู้ใช้งานเข้าถึงข้อมูลกันไม่ได้จริงไหม?
มีการ ตรวจสอบว่าฟังก์ชันของ admin คนที่เป็น ผู้ใช้งานจะเรียกใช้ไม่ได้จริงไหม?
มีการ ตรวจสอบว่า business logic เราทำถูกต้อง ผู้ใช้งานระบบจะโกงเราไม่ได้ใช่ไหม? หรือจะมีทางไหนไหมที่เราจะโดนโกงจากแอปพลิเคชันที่เราเขียนมา?

จบแล้วครับกับ 10 เทคนิค การออกแบบเว็บ API ให้มีความปลอดภัยแบบแมว ๆ หวังว่าผู้อ่านจะได้ประโยชน์ดี ๆ ทั้งในมุมมองแฮกเกอร์มันส์ ๆ และมุมมองนักพัฒนา นักทดสอบซอฟต์แวร์ว่าเราควรจะทำยังไงให้ แอปพลิเคชันและเว็บ API เรามีความปลอดภัยมากยิ่งขึ้นครับ ข่าวร้ายก็คือว่าภัยคุกคามและช่องโหว่มีมากมาย ซับซ้อน กว่าที่ผมกล่าวถึงในบทความนี้หลายเท่านัก แต่ผมเชื่อว่าถ้าทำตามนี้ก็จะช่วยลดช่องโหว่ได้ในระดับที่ปลอดภัยพอสมควรแล้ว 80-90% ครับเรื่อง security ก็เป็นหน้าที่ของนักพัฒนาซอฟต์แวร์ทุกคนที่ควรจะตระหนักถึงผู้ใช้งานและอัพเดทความรู้เกี่ยวกับภัยคุกคามและช่องโหว่ใหม่ ๆ อยู่เสมอครับ

บทความที่เกี่ยวข้อง