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