guest@websec ~ % man 13-ssrf
13 · Server-Side Request Forgery (SSRF)
ฟีเจอร์ไหนก็ตามที่เซิร์ฟเวอร์เราไปดึง URL ของผู้ใช้ (รูปจากลิงก์, webhook, ตัวอย่างลิงก์, render PDF) ถูกใช้ย้อนกลับมาได้ ผู้โจมตีเข้าเครือข่ายภายในเราไม่ได้ แต่เซิร์ฟเวอร์เราเข้าได้ และ SSRF ทำให้มันไปดึงให้เขา
ผู้โจมตีเล็งอะไร
- Cloud metadata (
169.254.169.254) ให้ข้อมูลรับรองคลาวด์ของเซิร์ฟเวอร์ นี่คือเบื้องหลังการเจาะ Capital One ปี 2019 - บริการภายใน ที่เชื่อทุกอย่างในเครือข่ายส่วนตัว
- บริการ localhost ในเครื่องเดียวกัน
ทำไม blocklist ง่ายๆ จึงล้มเหลว
บล็อกคำว่า "localhost" ในสตริงใช้ไม่ได้ IP เขียนได้หลายแบบ hostname ชี้เข้าภายในได้ และ redirect ก็พาเข้าไปได้ ลองดู:
try:
| การตรวจ | ผลตัดสิน | สิ่งที่เกิดขึ้นจริง |
|---|
วิธีป้องกัน
- เลี่ยงได้ก็เลี่ยง: รับไฟล์อัปโหลดแทน หรือดึงเฉพาะจาก host ที่รู้จัก
- ถ้าเลี่ยงไม่ได้: resolve hostname เอง บล็อก IP ภายใน แล้วเชื่อมต่อไปที่ IP นั้นตรงๆ
- อย่าตาม redirect แบบไม่ตรวจ
- ตั้ง timeout สั้น และอย่าแสดง response กลับไปให้ผู้ใช้
- บน AWS บังคับ IMDSv2 เพื่อให้ GET ธรรมดาอ่าน metadata ไม่ได้
Rails ไม่ได้ช่วยอะไร ระวัง
URI.open(params[…]), HTTP client ที่ใช้ URL ของผู้ใช้ และ attach(io: URI.open(url)) gem ssrf_filter ทำส่วนที่ยากให้# Gemfile: gem "ssrf_filter" def import_avatar(url) uri = URI.parse(url) raise ArgumentError, "http(s) only" unless %w[http https].include?(uri.scheme) response = SsrfFilter.get(uri, max_redirects: 0) # resolves, blocks private IPs, pins IP raise "not an image" unless response["content-type"].to_s.start_with?("image/") avatar.attach(io: StringIO.new(response.body), filename: "avatar") rescue SsrfFilter::Error, URI::InvalidURIError errors.add(:avatar_url, "can't be fetched") # generic message, no body echo end avatar.attach(io: URI.open(params[:avatar_url]), filename: "a") # SSRF
// Node: request-filtering-agent blocks private IPs after DNS resolution. // It needs an agent-aware client (node-fetch, axios, got): Node's built-in fetch ignores `agent`. import fetch from "node-fetch"; import { useAgent } from "request-filtering-agent"; const res = await fetch(url, { agent: useAgent(url), redirect: "manual" }); await fetch(req.body.url); // SSRF