不安全的 URL 跳转漏洞源于未校验用户可控的跳转参数(如 next),而非 redirect() 函数本身;应通过 urljoin + urlparse 归一化后白名单校验,或优先使用 url_for() 动态生成本域跳转链接。

直接说结论:不安全的 URL 跳转漏洞不是 redirect() 函数的问题,而是你把用户可控的 next、redirect_to、return_url 这类参数未经校验就传给了它——等于主动把跳转控制权交给了攻击者。
为什么用 request.args.get('next') + redirect() 会出事
常见于登录后跳转、OAuth 回调、表单提交后返回等逻辑。比如这段代码:
@app.route('/login', methods=['POST'])
def login():
# ...认证逻辑
next_url = request.args.get('next', '/')
return redirect(next_url) # ⚠️ 危险!
攻击者只要访问 /login?next=https://evil.com/phish,用户登录后就会被无声跳转到钓鱼站。浏览器完全信任 Location 响应头,不会提示任何异常。
关键点在于:redirect() 本身是安全的,但它的输入来源不可信。同样的风险也存在于手动设置 response.headers['Location'] 或前端用 window.location.href = url 跳转(只要 url 来自 URL 参数或 API 响应)。
立即学习“Python免费学习笔记(深入)”;
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
- 旧版 Flask/Django 默认允许
//evil.com、javascript:alert(1)等非法协议或空协议跳转 - 相对路径(如
../admin/delete)若未归一化,可能绕过基础校验 - 某些框架对 Unicode 编码(如
%E2%80%8C零宽字符)处理不一致,导致白名单比对失效
用 urlparse 拆解 + 白名单比对(最通用)
这是兼容性最好、不依赖框架内部机制的方式,适合所有 Python Web 场景。核心是三步:解析 → 归一化 → 白名单检查。
示例代码(带注释):
from urllib.parse import urlparse, urljoin
from flask import request, redirect, abort
<p>def is_safe_redirect_url(target):
ref_url = urlparse(request.host_url)
test_url = urlparse(urljoin(request.host_url, target))
return test_url.scheme in ('', 'http', 'https') and \
test_url.netloc == ref_url.netloc</p><p>@app.route('/login', methods=['POST'])
def login():
next_url = request.args.get('next', '/')
if not is_safe_redirect_url(next_url):
next_url = '/' # 或抛出 400
return redirect(next_url)
-
urljoin()必须调用:它能把next=/path、next=//evil.com、next=https://example.com/path全部转成标准格式再比对 - 只允许
scheme为空(相对路径)、http、https;禁止javascript:、data:、file:等伪协议 - 严格比对
netloc,不能只看域名是否“包含”本域(防止yourapp.evil.com绕过)
用 url_for() 构造跳转(最安全)
如果你的跳转目标是本应用内的页面,优先放弃原始 URL 字符串,改用 url_for() 动态生成。
比如登录后跳回“上一页”,不要拼 next=/user/profile,而是:
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
# ...认证成功
endpoint = request.args.get('next_endpoint', 'index')
args = request.args.to_dict()
# 过滤掉非预期参数,只保留用于 url_for 的键值对
safe_args = {k: v for k, v in args.items() if k not in ['next', 'next_endpoint']}
return redirect(url_for(endpoint, **safe_args))
-
url_for()只接受已注册的endpoint名和参数,框架自动保证路径合法、无跨域、无协议切换 - 必须显式过滤掉原始跳转参数(如
next),否则攻击者仍可传入恶意next_endpoint=javascript:alert - 适用于跳转目标明确为本应用路由的场景;第三方回调(如 OAuth)仍需走白名单校验
容易被忽略的边界情况
真实业务中,以下几点常被跳过,但恰恰是绕过检测的高发点:
- 前端 JS 调用
window.location.href或location.assign()时,若url来自document.referrer或localStorage,同样需要校验 - API 接口返回的跳转字段(如
{"redirect": "..."}),不能只在后端跳转逻辑里校验,JSON 响应体中的该字段也需同步过滤 - 测试时用
localhost或127.0.0.1,上线后变成www.example.com,白名单硬编码未更新会导致误拦 - URL 中的 fragment(
#section)虽不影响跳转域,但若拼接进重定向逻辑,可能被用来混淆审计(例如https://good.com/#/evil.com)

















