10.12. CORS และ CSRF#

CORS และ CSRF คือการป้องกันฝั่งเบราว์เซอร์สองอย่างที่กล้องที่เชื่อมต่ออินเทอร์เน็ตสาธารณะต้องมีควบคู่กับ HTTPS และระบบล็อกอิน แต่ละอย่างตั้งค่าด้วยโค้ดไม่กี่บรรทัด ส่วนต่างๆ ด้านล่างนิยามคำศัพท์และแสดงการเชื่อมต่อกับ microdot

10.12.1. CORS คืออะไร#

Cross-Origin Resource Sharing (CORS) คือกลไกของเบราว์เซอร์ที่ให้เซิร์ฟเวอร์เลือกอนุญาตให้ origin อื่นๆ อ่าน response ของตนได้ same-origin policy ค่าเริ่มต้นของเบราว์เซอร์จะบล็อกการอ่านนั้น: JavaScript บน https://example.com ไม่สามารถอ่าน response จาก https://yard-cam.example.com ได้ เพราะ host ที่ต่างกันถือเป็น origin ที่ต่างกัน CORS คือวิธีฝั่งเซิร์ฟเวอร์ในการอนุญาตข้อยกเว้นสำหรับ peer ที่เลือก

หากแดชบอร์ดเสิร์ฟจากกล้องเอง ทุก request จะเป็น same-origin และ CORS ไม่มีผลอะไร การตั้งค่านี้สำคัญเมื่อแดชบอร์ดอยู่ที่อื่น — URL สาธารณะอย่าง https://app.example.com ที่คุยกับกล้องที่ https://yard-cam.example.com:

from microdot.cors import CORS

cors = CORS(
    app,
    allowed_origins=['https://app.example.com'],
    allow_credentials=True,
    max_age=86400,
)

allowed_origins คือรายชื่อ origin ที่อนุญาตให้อ่าน response ของกล้อง ระบุเฉพาะ origin ของแดชบอร์ดเท่านั้น ไม่ใช่ * — เพื่อป้องกันไม่ให้เว็บไซต์ third-party อ่าน response ของกล้องโดยไม่ตั้งใจ

allow_credentials=True อนุญาตให้ cross-origin request รวม session cookie ซึ่งแดชบอร์ดต้องการเพื่อคงสถานะล็อกอินข้าม origin boundary

max_age=86400 บอกเบราว์เซอร์ว่าสามารถ cache ผล preflight ได้หนึ่งวัน เบราว์เซอร์จะส่ง OPTIONS request พิเศษก่อนการเรียก cross-origin ใดๆ ที่ใช้ method นอกเหนือจาก GET/HEAD/POST หรือส่ง custom header max_age ลดค่าใช้จ่ายนั้นเหลือ preflight หนึ่งครั้งต่อวันต่อ route

10.12.2. CSRF คืออะไร#

Cross-Site Request Forgery (CSRF) คือการโจมตีที่หน้าเว็บอันตรายทำให้เบราว์เซอร์ของผู้ใช้ส่ง request ที่ผ่านการยืนยันตัวตนไปยังเซิร์ฟเวอร์ที่เชื่อถือได้ แม้ว่า CORS จะเปิดใช้งาน แต่ <form> ที่ซ่อนอยู่บน evil.com ที่ POST ไปยัง https://yard-cam.example.com/config จะเข้าถึงกล้องได้ และเบราว์เซอร์จะแนบ session cookie ของกล้องไปด้วย — cookie ตาม destination host ไม่ใช่ origin ของหน้าที่ส่ง request — ดังนั้นกล้องจึงประมวลผล POST นั้นเหมือนกับว่าเจ้าของเป็นคนทำ

การป้องกัน CSRF ปฏิเสธ request เหล่านั้น microdot.csrf.CSRF เพิ่ม middleware ที่ตรวจสอบ header Sec-Fetch-Site ในทุก request ที่เปลี่ยนสถานะและปฏิเสธสิ่งที่ไม่ได้ติดป้าย same-origin (หรือมาจาก origin ที่ CORS อนุญาต):

from microdot.csrf import CSRF

CSRF(app, cors=cors)

การส่ง instance cors ให้ middleware ยกเว้น origin ที่อนุญาตของแดชบอร์ด — กล้องยังคงรับ POST จากแดชบอร์ดแม้ว่าจะเป็น cross-origin

Sec-Fetch-Site ถูกเซ็ตโดยเบราว์เซอร์สมัยใหม่โดยอัตโนมัติ กล้องไม่ต้องทำอะไรฝั่ง client สำหรับเบราว์เซอร์เก่าที่ไม่ส่ง header นั้น CORS allow-list จะเป็นการตรวจสอบสำรอง

10.12.3. การยกเว้น webhook#

หากกล้องต้องการ webhook endpoint เพื่อรับ POST จาก cloud service third-party — เช่น callback จากผู้ให้บริการเก็บถาวร — ให้ mark route ด้วย @csrf.exempt เพื่อให้ middleware ปล่อยผ่าน handler มีหน้าที่ตรวจสอบ request ด้วยวิธีอื่น — โดยทั่วไปคือ Hash-based Message Authentication Code (HMAC) เหนือ payload ที่คำนวณด้วย secret ที่กล้องและ third party แชร์กัน ซึ่งพิสูจน์ว่า request มาจากผู้ที่รู้ secret นั้น กล้องหลังบ้านไม่มีสิ่งเหล่านั้น แต่ decorator นี้พร้อมใช้เมื่อคุณต้องการ

10.12.4. baseline สี่บรรทัด#

เมื่อติดตั้ง HTTPS แล้ว stack ที่แนะนำสำหรับการ deploy กล้องที่หันหน้าสู่อินเทอร์เน็ตคือ:

Session(app, secret_key=SECRET,
        cookie_options={'http_only': True, 'secure': True})
login = Login()
cors = CORS(app, allowed_origins=[...], allow_credentials=True)
CSRF(app, cors=cors)

Session และ login ในบทก่อนหน้า CORS และ CSRF ในหัวข้อนี้ HTTPS จากหัวข้อก่อนหน้า ทั้งสี่ส่วนซ้อนกันและไม่ยุ่งกับ route แต่ละเส้นทาง

กล้องตอนนี้ปลอดภัยพอที่จะเชื่อมต่ออินเทอร์เน็ตสาธารณะแล้ว — HTTPS, login, CSRF, CORS