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

原创 临窗旋墨 2025-02-08 阅读:2155 分类: java后端 专题: 架构设计 标签: 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方法,获取重定向的地址,但是是否重定向的逻辑,交于后续过滤结果决定

java 复制代码
/**
 * 描述:
 *      用于获取 重定向地址: 即 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;
    }
}
3.2 SafelyRedirectFilter 重定向之前对重定向地址进行白名单过滤
  1. 使用自定义的HttpServletResponseWrapper获取重定向地址
  2. 对重定向地址进行过滤
  3. 内部地址放行
  4. 外部地址则根据传入的白名单地址或白名单过滤逻辑进行过滤
  5. 如果均不通过,则返回403,或者根据传入的拦截后的错误处理逻辑,自行处理
java 复制代码
/**
 * 描述:
 *  重定向之前对重定向地址进行白名单过滤, 注意把此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);
    }
}

3.3 使用示例

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

properties 复制代码
# ***************************************************************
# [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 配置类获取配置的白名单列表

java 复制代码
@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 配置重定向拦截器

  • 此处只配置了白名单列表,并没有配置过滤逻辑和错误处理逻辑
java 复制代码
@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;
    }

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

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

评论区

avatar
未登录

暂无评论,来发表第一条评论吧~