最后更新:2026-04-17
开放重定向(Open Redirect)是指应用程序将用户重定向到未经验证的外部 URL,攻击者可利用此漏洞构造钓鱼链接,使受害者以为访问的是合法网站,实则被引导至恶意站点。
| 维度 | 评级 |
|---|---|
| OWASP Top 10 | A01:2025 - Broken Access Control |
| CWE | CWE-601 |
| 严重程度 | 中危 |
| 类型 | 说明 |
|---|---|
| 直接重定向 | redirect?url=https://evil.com |
| 绕过白名单 | 利用 URL 解析差异绕过域名校验 |
| 协议混淆 | //evil.com(协议相对 URL) |
| 参数污染 | ?url=good.com&url=evil.com |
// [VULNERABLE] 直接使用用户提供的 URL 进行重定向
@GetMapping("/redirect")
public void redirect(@RequestParam String url, HttpServletResponse response)
throws IOException {
// 危险:未校验 url 参数,可重定向到任意外部地址
response.sendRedirect(url);
}
// 攻击:/redirect?url=https://evil.com/phishing
// 钓鱼链接:https://bank.com/redirect?url=https://evil.com/login
// [SECURE] 白名单校验目标域名
@GetMapping("/redirect")
public void redirect(@RequestParam String url, HttpServletResponse response)
throws IOException {
if (!isSafeUrl(url)) {
response.sendRedirect("/error/invalid-redirect");
return;
}
response.sendRedirect(url);
}
private static final Set<String> ALLOWED_HOSTS = Set.of(
"www.example.com",
"app.example.com",
"docs.example.com"
);
private boolean isSafeUrl(String url) {
try {
URI uri = new URI(url);
String host = uri.getHost();
// 只允许相对路径或白名单域名
if (host == null) {
// 相对路径,安全
return url.startsWith("/") && !url.startsWith("//");
}
return ALLOWED_HOSTS.contains(host.toLowerCase());
} catch (URISyntaxException e) {
return false;
}
}
// [VULNERABLE] 登录成功后直接重定向到用户指定的 returnUrl
@PostMapping("/login")
public String login(@RequestParam String username,
@RequestParam String password,
@RequestParam(required = false) String returnUrl) {
if (authService.authenticate(username, password)) {
// 危险:returnUrl 未校验,登录后可跳到攻击者的钓鱼页
return "redirect:" + returnUrl;
}
return "redirect:/login?error";
}
// 攻击:/login?returnUrl=https://evil.com
// [SECURE] 登录后重定向仅允许相对路径
@PostMapping("/login")
public String login(@RequestParam String username,
@RequestParam String password,
@RequestParam(required = false) String returnUrl,
HttpServletRequest request) {
if (authService.authenticate(username, password)) {
String safeUrl = getSafeReturnUrl(returnUrl, request);
return "redirect:" + safeUrl;
}
return "redirect:/login?error";
}
private String getSafeReturnUrl(String returnUrl, HttpServletRequest request) {
if (returnUrl == null || returnUrl.isBlank()) {
return "/dashboard";
}
// 只允许相对路径,不允许协议相对 URL(//evil.com)
if (returnUrl.startsWith("/") && !returnUrl.startsWith("//")) {
return returnUrl;
}
// 同域绝对路径校验
try {
URI uri = new URI(returnUrl);
String requestHost = request.getServerName();
if (requestHost.equals(uri.getHost())) {
return returnUrl;
}
} catch (URISyntaxException ignored) {}
return "/dashboard";
}
// [VULNERABLE] 仅检查 URL 是否包含合法域名,可被绕过
private boolean isAllowed(String url) {
// 危险:contains 检查可被绕过
// 攻击:https://evil.com?redirect=example.com
// https://example.com.evil.com
return url.contains("example.com");
}
// [SECURE] 严格解析 URL 的 host 部分
private boolean isAllowed(String url) {
try {
URI uri = URI.create(url);
String host = uri.getHost();
if (host == null) return false;
// 精确匹配 host,防止子域名欺骗
return host.equals("example.com") || host.endsWith(".example.com");
} catch (IllegalArgumentException e) {
return false;
}
}
sendRedirect(、"redirect:" + 用户输入拼接url/redirect/return 参数为外部域名,观察是否跳转//evil.com、https://evil.com@example.comURI.getHost() 精确匹配,不用 containsreturnUrl 只接受 / 开头且不以 // 开头的路径DefaultRedirectStrategy 并设置 contextRelative=true