
2025-02-06
最近,项目做安全检查,扫描出来一个风险:
程序允许用户控制重定向或跳转到另一个URL的情况。如果程序没有验证不可信的用户输入,攻击者就可以提供一个URL,将无辜的用户从合法的域名重定向到攻击者的钓鱼网站。
也给出了常见的解决方案:
从客观上来说,这种风险确实存在,所以还是需要修复一下,这里选择使用白名单过滤的方式。
HttpServletResponseWrapper
来拦截sendRedirect
调用 重点在于重写sendRedirect
方法,获取重定向的地址,但是是否重定向的逻辑,交于后续过滤结果决定
/**
* 描述:
* 用于获取 重定向地址: 即 response.sendRedirect(url)中的url
* 其中url为原始值,可能是相对地址,也可能是绝对地址
* @author Vic.xu
* @since 2025-02-08 9:34
*/
public class RedirectResponseWrapper extends HttpServletResponseWrapper {
/**
* 重定向的地址
*/
private String redirectUrl;
/**
* Constructs a response adaptor wrapping the given response.
*
* @param response The response to be wrapped
* @throws IllegalArgumentException if the response is null
*/
public RedirectResponseWrapper(HttpServletResponse response) {
super(response);
}
/**
* 重写sendRedirect方法,用于获取重定向地址,而非直接重定向
* 重定向与否由后续逻辑决定
*/
@Override
public void sendRedirect(String location) throws IOException {
this.redirectUrl = location;
}
public String getRedirectUrl() {
return redirectUrl;
}
}
SafelyRedirectFilter
重定向之前对重定向地址进行白名单过滤HttpServletResponseWrapper
获取重定向地址
/**
* 描述:
* 重定向之前对重定向地址进行白名单过滤, 注意把此Filter的顺序设置在前面
* <p>
* 1. 内部系统 应直接放行
* 2. 外部地址 进行白名单过滤
* 2.1 是否白名单:如果既没有设置 白名单过滤逻辑 也没有设置白名单列表 则直接拦截
* 2.2 白名单过滤逻辑 和 白名单列表 满足其一即可
* </p>
*
* @author Vic.xu
* @since 2025-02-08 9:30
*/
public class SafelyRedirectFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(SafelyRedirectFilter.class);
public static final String HTTP = "http://";
public static final String HTTPS = "https://";
/**
* 拦截后的错误处理
*/
protected BiConsumer<HttpServletResponse, String> sendErrorConsumer;
/**
* 白名单过滤逻辑
*/
protected Function<String, Boolean> whiteListFilterFunction;
/**
* 白名单列表
*/
protected List<String> whiteList;
public void setSendErrorConsumer(BiConsumer<HttpServletResponse, String> sendErrorConsumer) {
this.sendErrorConsumer = sendErrorConsumer;
}
public void setWhiteListFilterFunction(Function<String, Boolean> whiteListFilterFunction) {
this.whiteListFilterFunction = whiteListFilterFunction;
}
public void setWhiteList(List<String> whiteList) {
this.whiteList = whiteList;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 使用自定义的HttpServletResponseWrapper来拦截sendRedirect调用
RedirectResponseWrapper responseWrapper = new RedirectResponseWrapper(response);
try {
filterChain.doFilter(request, responseWrapper);
} finally {
// 1. 检查是否有重定向URL
String redirectUrl = responseWrapper.getRedirectUrl();
if (StringUtils.isBlank(redirectUrl)) {
return;
}
if (isInternalRedirect(redirectUrl)) {
// 2. 内部路径,直接放行
response.sendRedirect(redirectUrl);
return;
}
// 3. 进行白名单过滤
boolean white = isWhite(redirectUrl);
LOGGER.info("外部重定向地址[{}]白名单过滤结果", redirectUrl, white);
if (white) {
response.sendRedirect(redirectUrl);
return;
}
// 4. 非白名单,拦截 并提示前端
sendError(response, redirectUrl);
}
}
/**
* 拦截后的错误处理
*/
protected void sendError(HttpServletResponse response, String redirectUrl) throws IOException {
if (sendErrorConsumer != null) {
sendErrorConsumer.accept(response, redirectUrl);
return;
}
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Redirect URL [" + redirectUrl + "] not allowed");
}
/**
* 是否白名单
*/
protected boolean isWhite(String url) {
//如果即没有设置白名单列表 也没有设置白名单过滤函数,则直接拦截
if (whiteList == null && whiteListFilterFunction == null) {
return false;
}
boolean allow = false;
if (whiteList != null) {
allow = whiteList.stream().anyMatch(url::startsWith);
}
if (whiteListFilterFunction != null) {
allow = allow || whiteListFilterFunction.apply(url);
}
return allow;
}
/**
* 是否内部地址
*/
private boolean isInternalRedirect(String url) {
// 判断是否是内部 URL(不带 http/https,或是相对路径)
url = url.trim().toLowerCase(Locale.ROOT);
return !url.startsWith(HTTP) && !url.startsWith(HTTPS);
}
}
1 配置文件中,配置白名单地址
# ***************************************************************
# [03] white list
# ***************************************************************
test.url=http://localhost:8080/test
safely.redirect.white-list[0]=https://baidu.com
safely.redirect.white-list[1]=https://xuqiudong.cn
safely.redirect.white-list[2]=${test.url}
#################################################################
2 配置类获取配置的白名单列表
@ConfigurationProperties(prefix = "safely.redirect")
public class RedirectConfigProperties {
private List<String> whiteList;
public List<String> getWhiteList() {
return whiteList;
}
public void setWhiteList(List<String> whiteList) {
this.whiteList = whiteList;
}
}
3 配置重定向拦截器
@Configuration
@EnableConfigurationProperties(RedirectConfigProperties.class)
public class FilterConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(FilterConfig.class);
private RedirectConfigProperties redirectConfigProperties;
@Autowired
public void setRedirectConfigProperties(RedirectConfigProperties redirectConfigProperties) {
this.redirectConfigProperties = redirectConfigProperties;
}
/**
* 避免重定向攻击
*/
@Bean
public FilterRegistrationBean<SafelyRedirectFilter> safelyRedirectFilter() {
LOGGER.info("注册重定向过滤器:SafelyRedirectFilter");
FilterRegistrationBean<SafelyRedirectFilter> registrationBean = new FilterRegistrationBean<>();
SafelyRedirectFilter safelyRedirectFilter = new SafelyRedirectFilter();
if (redirectConfigProperties.getWhiteList() != null) {
LOGGER.info("白名单列表:\n\t{}" , String.join("\n\t" , redirectConfigProperties.getWhiteList()));
safelyRedirectFilter.setWhiteList(redirectConfigProperties.getWhiteList());
}
registrationBean.setFilter(safelyRedirectFilter);
// 适用于所有请求
registrationBean.addUrlPatterns("/*");
// 优先执行
registrationBean.setOrder(0);
return registrationBean;
}
综上,代码比较简单,测试过程略。