guest@websec: ~/websec/07-csrf/th — zsh — 100×40 เขียนโดย SaKKo ENTH โค้ด:

guest@websec ~ % man 07-csrf

07 · Cross-Site Request Forgery (CSRF)

CSRF หลอกให้เบราว์เซอร์ที่ล็อกอินอยู่ส่ง request ที่คุณไม่ได้ตั้งใจ คุกกี้ของคุณติดไปด้วย แอปจึงคิดว่าคุณเป็นคนกด

ทำงานอย่างไร

เบราว์เซอร์ส่งคุกกี้ไปกับทุก request ของเว็บนั้น ไม่ว่าหน้าไหนจะเป็นคนสั่ง นี่คือสิ่งที่ทำให้คุณล็อกอินค้างได้ และ CSRF ก็ใช้จุดนี้

ผู้โจมตีไม่เห็นผลลัพธ์ แค่ทำให้บางอย่างเกิดขึ้น: เปลี่ยนอีเมล โอนเงิน เพิ่ม admin

ลองดู

sim: bank.test ปะทะ evil.testตั้งการป้องกัน แล้วเริ่มโจมตี

Alice ล็อกอินธนาคารอยู่ ตั้งค่าการป้องกันของธนาคาร แล้วโจมตีจากหน้าเว็บร้าย

การตั้งค่า bank.test




ยอดเงินของ Alice: $5,000

หน้าเว็บ evil.test

7.1 วิธีป้องกัน

ใช้ GET และ POST ให้ถูกต้อง

กฎข้อแรก: GET มีไว้อ่านอย่างเดียว อะไรที่เปลี่ยนข้อมูลใช้ POST, PATCH หรือ DELETE การป้องกัน CSRF ส่วนใหญ่ตั้งใจข้าม GET ดังนั้น GET ที่ลบข้อมูลได้คือประตูที่เปิดอ้า

บังคับ token ลับใน request ที่เปลี่ยนสถานะ

ทางแก้คลาสสิก: ใส่ token ลับในทุกฟอร์ม เว็บร้ายสั่งให้เบราว์เซอร์ส่งคุกกี้ได้ แต่อ่านหน้าของเราไม่ได้ จึงไม่รู้ token

เปิดไว้แล้วโดยค่าเริ่มต้น ฟอร์มของ Rails ใส่ 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 ที่บอกว่ามาจากเว็บอื่น
Rails ตั้ง SameSite=Lax ให้คุกกี้โดยค่าเริ่มต้น (ตั้งแต่ 6.1) ตรวจว่าแอปเก่าไม่ได้ปิดไว้

อย่าให้เว็บอื่นโหลดข้อมูลของคุณเป็นสคริปต์

ถ้า URL คืนข้อมูลส่วนตัวเป็น JavaScript เว็บอื่นโหลดผ่านแท็ก <script> แล้วอ่านได้ ให้คืนเป็น JSON และอย่าใช้ JSONP

Rails บล็อกเรื่องนี้ให้แล้วสำหรับ response แบบ .js

ล้างคุกกี้ที่อยู่ถาวรเมื่อพบการปลอม request

เมื่อจับ request ปลอมได้ ให้ล้างคุกกี้ "จดจำฉัน" ด้วย ไม่อย่างนั้นผู้ใช้ก็ล็อกอินกลับเข้ามาเองทันที

ค่าเริ่มต้น Rails แค่ raise error โดยไม่ล้างอะไร ให้เพิ่ม rescue_from ActionController::InvalidAuthenticityToken ที่จบ session และลบคุกกี้เหล่านั้น
XSS ชนะทุกอย่างข้างบน สคริปต์ที่ถูกแทรกรันบนเว็บเราเองและอ่าน token ได้ ต้องกัน XSS ก่อน (หัวข้อ 04)