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方法,用于获取重定向地址,而非直接重定向* 重定向与否由后续逻辑决定*/@Overridepublic 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;}@Overrideprotected 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. 检查是否有重定向URLString 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/testsafely.redirect.white-list[0]=https://baidu.comsafely.redirect.white-list[1]=https://xuqiudong.cnsafely.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;@Autowiredpublic void setRedirectConfigProperties(RedirectConfigProperties redirectConfigProperties) {this.redirectConfigProperties = redirectConfigProperties;}/*** 避免重定向攻击*/@Beanpublic 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;}
综上,代码比较简单,测试过程略。