guest@websec ~ % man 07-csrf
07 · Cross-Site Request Forgery (CSRF)
CSRF หลอกให้เบราว์เซอร์ที่ล็อกอินอยู่ส่ง request ที่คุณไม่ได้ตั้งใจ คุกกี้ของคุณติดไปด้วย แอปจึงคิดว่าคุณเป็นคนกด
ทำงานอย่างไร
เบราว์เซอร์ส่งคุกกี้ไปกับทุก request ของเว็บนั้น ไม่ว่าหน้าไหนจะเป็นคนสั่ง นี่คือสิ่งที่ทำให้คุณล็อกอินค้างได้ และ CSRF ก็ใช้จุดนี้
ผู้โจมตีไม่เห็นผลลัพธ์ แค่ทำให้บางอย่างเกิดขึ้น: เปลี่ยนอีเมล โอนเงิน เพิ่ม admin
ลองดู
Alice ล็อกอินธนาคารอยู่ ตั้งค่าการป้องกันของธนาคาร แล้วโจมตีจากหน้าเว็บร้าย
การตั้งค่า bank.test
ยอดเงินของ Alice: $5,000
หน้าเว็บ evil.test
7.1 วิธีป้องกัน
ใช้ GET และ POST ให้ถูกต้อง
กฎข้อแรก: GET มีไว้อ่านอย่างเดียว อะไรที่เปลี่ยนข้อมูลใช้ POST, PATCH หรือ DELETE การป้องกัน CSRF ส่วนใหญ่ตั้งใจข้าม GET ดังนั้น GET ที่ลบข้อมูลได้คือประตูที่เปิดอ้า
บังคับ token ลับใน request ที่เปลี่ยนสถานะ
ทางแก้คลาสสิก: ใส่ token ลับในทุกฟอร์ม เว็บร้ายสั่งให้เบราว์เซอร์ส่งคุกกี้ได้ แต่อ่านหน้าของเราไม่ได้ จึงไม่รู้ token
csrf_meta_tags ครอบคลุม Turbo กับ fetch ระวัง: controller แบบ API-only ไม่มีการป้องกัน CSRF และทุกที่ที่มี skip_forgery_protection ควรตรวจซ้ำclass ApplicationController < ActionController::Base # on by default since Rails 5.2; being explicit makes failures loud protect_from_forgery with: :exception rescue_from ActionController::InvalidAuthenticityToken do terminate_session if respond_to?(:terminate_session, true) # generator helper cookies.delete(:remember_token) redirect_to new_session_path, alert: "Please sign in again." end end <%# app/views/layouts/application.html.erb %> <%= csrf_meta_tags %> <%# token for Turbo / JS %> <%= form_with model: @transfer do |f| %> <%# hidden authenticity_token %> // your own fetch() calls fetch("/transfers", { method: "POST", headers: { "X-CSRF-Token": document.querySelector("meta[name=csrf-token]").content }, body })
// npm install csrf-sync (synchronizer token pattern) const { csrfSynchronisedProtection, generateToken } = require("csrf-sync")({ getTokenFromRequest: req => req.body._csrf || req.get("x-csrf-token"), }); app.use(session(/* … */)); app.use(csrfSynchronisedProtection); // checks POST/PUT/PATCH/DELETE app.get("/transfer", (req, res) => res.render("transfer", { csrfToken: generateToken(req) })); <!-- template --> <meta name="csrf-token" content="<%= csrfToken %>"> <form method="post" action="/transfer"> <input type="hidden" name="_csrf" value="<%= csrfToken %>"> … // fetch from your own JS fetch("/transfer", { method: "POST", headers: { "X-CSRF-Token": document.querySelector("meta[name=csrf-token]").content }, body });
# Django: CsrfViewMiddleware is enabled by default # <form method="post">{% csrf_token %} … </form> # JS: send the csrftoken cookie value in the X-CSRFToken header # Flask: pip install flask-wtf from flask_wtf.csrf import CSRFProtect csrf = CSRFProtect(app) # checks POST/PUT/PATCH/DELETE # <meta name="csrf-token" content="{{ csrf_token() }}"> # <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> # fetch(url, {method:"POST", headers:{"X-CSRFToken": token}})
เสริมด้วย SameSite cookie และการตรวจ origin
มีอีกสองชั้นที่เพิ่มได้ง่าย:
- คุกกี้
SameSite=Laxไม่ถูกส่งไปกับ POST หรือรูปภาพจากเว็บอื่น แต่ยังส่งเมื่อกดลิงก์ปกติ จึงช่วย GET อันตรายไม่ได้ - ตรวจ Origin: ปฏิเสธ POST ที่บอกว่ามาจากเว็บอื่น
SameSite=Lax ให้คุกกี้โดยค่าเริ่มต้น (ตั้งแต่ 6.1) ตรวจว่าแอปเก่าไม่ได้ปิดไว้อย่าให้เว็บอื่นโหลดข้อมูลของคุณเป็นสคริปต์
ถ้า URL คืนข้อมูลส่วนตัวเป็น JavaScript เว็บอื่นโหลดผ่านแท็ก <script> แล้วอ่านได้ ให้คืนเป็น JSON และอย่าใช้ JSONP
.jsล้างคุกกี้ที่อยู่ถาวรเมื่อพบการปลอม request
เมื่อจับ request ปลอมได้ ให้ล้างคุกกี้ "จดจำฉัน" ด้วย ไม่อย่างนั้นผู้ใช้ก็ล็อกอินกลับเข้ามาเองทันที
rescue_from ActionController::InvalidAuthenticityToken ที่จบ session และลบคุกกี้เหล่านั้น