代码安全:重定向之前对重定向地址进行白名单过滤

文章来源原创   作者:临窗旋墨   发布时间:2025-02-08   阅读:88   标签:springboot 分类:我的编码 专题:解决方案

代码安全:重定向之前对重定向地址进行白名单过滤

2025-02-06

一、问题背景

最近,项目做安全检查,扫描出来一个风险:

程序允许用户控制重定向或跳转到另一个URL的情况。如果程序没有验证不可信的用户输入,攻击者就可以提供一个URL,将无辜的用户从合法的域名重定向到攻击者的钓鱼网站。

也给出了常见的解决方案:

  1. 不要直接使用用户输入作为重定向或跳转的目标URL。
  2. 对用户输入进行白名单验证,只允许重定向或跳转到已知安全的URL。
  3. 对用户输入进行编码或过滤,去除可能导致重定向或跳转的字符(如?,&,=等)。
  4. 在重定向或跳转之前,给用户一个确认提示,让用户自己决定是否继续。
  5. 注意发送到服务器的所有可用于重定向用户的不可信输入源,例如 cookie、URL 组件、文件名、查询结果、请求标头等。

从客观上来说,这种风险确实存在,所以还是需要修复一下,这里选择使用白名单过滤的方式。

二、解决方案:白名单过滤

  1. 自定义Filter过滤器,获取重定向地址
  2. 项目内部地址,放行
  3. 外部地址,则进行白名单过滤
  4. 非白名单地址,返回403

三、代码

3.1 自定义的HttpServletResponseWrapper来拦截sendRedirect调用

重点在于重写sendRedirect方法,获取重定向的地址,但是是否重定向的逻辑,交于后续过滤结果决定

  1. /**
  2. * 描述:
  3. * 用于获取 重定向地址: 即 response.sendRedirect(url)中的url
  4. * 其中url为原始值,可能是相对地址,也可能是绝对地址
  5. * @author Vic.xu
  6. * @since 2025-02-08 9:34
  7. */
  8. public class RedirectResponseWrapper extends HttpServletResponseWrapper {
  9. /**
  10. * 重定向的地址
  11. */
  12. private String redirectUrl;
  13. /**
  14. * Constructs a response adaptor wrapping the given response.
  15. *
  16. * @param response The response to be wrapped
  17. * @throws IllegalArgumentException if the response is null
  18. */
  19. public RedirectResponseWrapper(HttpServletResponse response) {
  20. super(response);
  21. }
  22. /**
  23. * 重写sendRedirect方法,用于获取重定向地址,而非直接重定向
  24. * 重定向与否由后续逻辑决定
  25. */
  26. @Override
  27. public void sendRedirect(String location) throws IOException {
  28. this.redirectUrl = location;
  29. }
  30. public String getRedirectUrl() {
  31. return redirectUrl;
  32. }
  33. }
3.2 SafelyRedirectFilter 重定向之前对重定向地址进行白名单过滤
  1. 使用自定义的HttpServletResponseWrapper获取重定向地址
  2. 对重定向地址进行过滤
  3. 内部地址放行
  4. 外部地址则根据传入的白名单地址或白名单过滤逻辑进行过滤
  5. 如果均不通过,则返回403,或者根据传入的拦截后的错误处理逻辑,自行处理
  1. /**
  2. * 描述:
  3. * 重定向之前对重定向地址进行白名单过滤, 注意把此Filter的顺序设置在前面
  4. * <p>
  5. * 1. 内部系统 应直接放行
  6. * 2. 外部地址 进行白名单过滤
  7. * 2.1 是否白名单:如果既没有设置 白名单过滤逻辑 也没有设置白名单列表 则直接拦截
  8. * 2.2 白名单过滤逻辑 和 白名单列表 满足其一即可
  9. * </p>
  10. *
  11. * @author Vic.xu
  12. * @since 2025-02-08 9:30
  13. */
  14. public class SafelyRedirectFilter extends OncePerRequestFilter {
  15. private static final Logger LOGGER = LoggerFactory.getLogger(SafelyRedirectFilter.class);
  16. public static final String HTTP = "http://";
  17. public static final String HTTPS = "https://";
  18. /**
  19. * 拦截后的错误处理
  20. */
  21. protected BiConsumer<HttpServletResponse, String> sendErrorConsumer;
  22. /**
  23. * 白名单过滤逻辑
  24. */
  25. protected Function<String, Boolean> whiteListFilterFunction;
  26. /**
  27. * 白名单列表
  28. */
  29. protected List<String> whiteList;
  30. public void setSendErrorConsumer(BiConsumer<HttpServletResponse, String> sendErrorConsumer) {
  31. this.sendErrorConsumer = sendErrorConsumer;
  32. }
  33. public void setWhiteListFilterFunction(Function<String, Boolean> whiteListFilterFunction) {
  34. this.whiteListFilterFunction = whiteListFilterFunction;
  35. }
  36. public void setWhiteList(List<String> whiteList) {
  37. this.whiteList = whiteList;
  38. }
  39. @Override
  40. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
  41. // 使用自定义的HttpServletResponseWrapper来拦截sendRedirect调用
  42. RedirectResponseWrapper responseWrapper = new RedirectResponseWrapper(response);
  43. try {
  44. filterChain.doFilter(request, responseWrapper);
  45. } finally {
  46. // 1. 检查是否有重定向URL
  47. String redirectUrl = responseWrapper.getRedirectUrl();
  48. if (StringUtils.isBlank(redirectUrl)) {
  49. return;
  50. }
  51. if (isInternalRedirect(redirectUrl)) {
  52. // 2. 内部路径,直接放行
  53. response.sendRedirect(redirectUrl);
  54. return;
  55. }
  56. // 3. 进行白名单过滤
  57. boolean white = isWhite(redirectUrl);
  58. LOGGER.info("外部重定向地址[{}]白名单过滤结果", redirectUrl, white);
  59. if (white) {
  60. response.sendRedirect(redirectUrl);
  61. return;
  62. }
  63. // 4. 非白名单,拦截 并提示前端
  64. sendError(response, redirectUrl);
  65. }
  66. }
  67. /**
  68. * 拦截后的错误处理
  69. */
  70. protected void sendError(HttpServletResponse response, String redirectUrl) throws IOException {
  71. if (sendErrorConsumer != null) {
  72. sendErrorConsumer.accept(response, redirectUrl);
  73. return;
  74. }
  75. response.sendError(HttpServletResponse.SC_FORBIDDEN, "Redirect URL [" + redirectUrl + "] not allowed");
  76. }
  77. /**
  78. * 是否白名单
  79. */
  80. protected boolean isWhite(String url) {
  81. //如果即没有设置白名单列表 也没有设置白名单过滤函数,则直接拦截
  82. if (whiteList == null && whiteListFilterFunction == null) {
  83. return false;
  84. }
  85. boolean allow = false;
  86. if (whiteList != null) {
  87. allow = whiteList.stream().anyMatch(url::startsWith);
  88. }
  89. if (whiteListFilterFunction != null) {
  90. allow = allow || whiteListFilterFunction.apply(url);
  91. }
  92. return allow;
  93. }
  94. /**
  95. * 是否内部地址
  96. */
  97. private boolean isInternalRedirect(String url) {
  98. // 判断是否是内部 URL(不带 http/https,或是相对路径)
  99. url = url.trim().toLowerCase(Locale.ROOT);
  100. return !url.startsWith(HTTP) && !url.startsWith(HTTPS);
  101. }
  102. }

3.3 使用示例

1 配置文件中,配置白名单地址

  1. # ***************************************************************
  2. # [03] white list
  3. # ***************************************************************
  4. test.url=http://localhost:8080/test
  5. safely.redirect.white-list[0]=https://baidu.com
  6. safely.redirect.white-list[1]=https://xuqiudong.cn
  7. safely.redirect.white-list[2]=${test.url}
  8. #################################################################

2 配置类获取配置的白名单列表

  1. @ConfigurationProperties(prefix = "safely.redirect")
  2. public class RedirectConfigProperties {
  3. private List<String> whiteList;
  4. public List<String> getWhiteList() {
  5. return whiteList;
  6. }
  7. public void setWhiteList(List<String> whiteList) {
  8. this.whiteList = whiteList;
  9. }
  10. }

3 配置重定向拦截器

  • 此处只配置了白名单列表,并没有配置过滤逻辑和错误处理逻辑
  1. @Configuration
  2. @EnableConfigurationProperties(RedirectConfigProperties.class)
  3. public class FilterConfig {
  4. private static final Logger LOGGER = LoggerFactory.getLogger(FilterConfig.class);
  5. private RedirectConfigProperties redirectConfigProperties;
  6. @Autowired
  7. public void setRedirectConfigProperties(RedirectConfigProperties redirectConfigProperties) {
  8. this.redirectConfigProperties = redirectConfigProperties;
  9. }
  10. /**
  11. * 避免重定向攻击
  12. */
  13. @Bean
  14. public FilterRegistrationBean<SafelyRedirectFilter> safelyRedirectFilter() {
  15. LOGGER.info("注册重定向过滤器:SafelyRedirectFilter");
  16. FilterRegistrationBean<SafelyRedirectFilter> registrationBean = new FilterRegistrationBean<>();
  17. SafelyRedirectFilter safelyRedirectFilter = new SafelyRedirectFilter();
  18. if (redirectConfigProperties.getWhiteList() != null) {
  19. LOGGER.info("白名单列表:\n\t{}" , String.join("\n\t" , redirectConfigProperties.getWhiteList()));
  20. safelyRedirectFilter.setWhiteList(redirectConfigProperties.getWhiteList());
  21. }
  22. registrationBean.setFilter(safelyRedirectFilter);
  23. // 适用于所有请求
  24. registrationBean.addUrlPatterns("/*");
  25. // 优先执行
  26. registrationBean.setOrder(0);
  27. return registrationBean;
  28. }

综上,代码比较简单,测试过程略。

代码参见: basic-support/lcxm-common - Gitee


发表评论

目录