前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >SQL注入安全漏洞!关联方报的错,竟得自己来背锅

SQL注入安全漏洞!关联方报的错,竟得自己来背锅

作者头像
程序员小义
发布2025-01-03 20:34:57
发布2025-01-03 20:34:57
8400
代码可运行
举报
文章被收录于专栏:小义思小义思
运行总次数:0
代码可运行

大家好,我是小义,好久不见,一转眼已经2025年了,真是抱歉又鸽了好久。新年第一篇,讲一个小故事吧。

起因

作为一名程序员,在编程的世界里摸爬滚打了这么久,本以为自己能应对各种技术难题,可没想到,最近却因为一个 SQL 注入安全漏洞,陷入了一场让人哭笑不得又无比头疼的 “背锅” 风波。

前段时间安全员在测试环境抛出了一个SQL注入的安全漏洞,说是我们系统的Bug。

我当时心里就 “咯噔” 一下,赶紧停下手里的活儿,着手去查看相关的代码和日志。

经过

一番排查下来,我发现竟然是 SQL 注入的安全漏洞在作祟。其实对于 SQL 注入,相信大家都不陌生,它就是攻击者通过在用户输入的字段中注入恶意的 SQL 语句,从而达到非法获取数据库信息、篡改数据甚至破坏整个数据库结构的目的。比如常见的在登录页面,如果对用户输入的用户名和密码没有做好严格的校验和过滤,攻击者就可能输入类似 “' or '1'='1' --” 这样的语句,绕过正常的验证逻辑,直接登录系统,造成严重后果。

在我们这个项目里,原本以为对输入数据做了基本的处理,可没想到还是百密一疏。

在和关联方数据交互的接口处,对于传递给他们的一些参数,他们只是简单地进行了格式上的验证,却忽略了深层次的 SQL 语句安全检查。结果报错信息抛到了我们这边,我们也没有进一步捕获,导致把服务器异常回显给前端了。

关联方那边可不管这漏洞到底是怎么产生的,只知道是和我们对接的地方出了问题,所以一股脑地把责任都推到了我们这边。我当时那叫一个委屈,明明大家都应该对数据安全负责啊。

解决

还好只是测试环境,赶紧修复就好了,遇到问题就要解决问题。我们都知道mybatis-plus本身其实提供了多种机制来防止 SQL 注入攻击的。

1、使用@Param 注解实现参数化查询。

2、使用 MyBatis Plus 的 CRUD 方法。

3、配置 SQL 注入过滤器(SqlExplainInterceptor)。

不过这是关联方报的错,我们还是得从入参考虑,最好就是在前端传递入参时一次性把非法参数给过滤掉了。那么怎么实现呢?

1、提取请求参数

首先,把前端接口请求中传递的所有参数提取出来。提取参数需要用到输入流,而在 HTTP 请求中,流(Stream)通常只能读取一次,所以需要进行特殊的缓存处理。

代码语言:javascript
代码运行次数:0
复制
@Slf4j
public class BodyReaderHttpServletRequest extends HttpServletRequestWrapper {
    private String body;
    public BodyReaderHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        StringBuilder stringBuilder=new StringBuilder();
        BufferedReader bufferedReader=null;
        try{
            InputStream inputStream=request.getInputStream();
            if(inputStream!=null){
                bufferedReader =new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
                char[] charBuffer=new char[128];
                int bytesRead=-1;
                while((bytesRead=bufferedReader.read(charBuffer))>0){
                    stringBuilder.append(charBuffer,0,bytesRead);
                }
            }else {
                stringBuilder.append("");
            }
        }catch (IOException ex){
            throw ex;
        }finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException ex) {
                    log.error("close bufferedReader error", ex);
                }
            }
        }
        body=stringBuilder.toString();
    }

    @Override
    public ServletInputStream getInputStream() throws IOException{
        final ByteArrayInputStream bais=new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
        ServletInputStream servletInputStream= new ServletInputStream() {
            @Override
            public boolean isFinished() {
                returnfalse;
            }

            @Override
            public boolean isReady() {
                returnfalse;
            }

            @Override
            public void setReadListener(ReadListener listener) {
                log.info("servletInputStream setReadListener do nothing");
            }

            @Override
            public int read() throws IOException{
               return bais.read();
           }
        };
        return servletInputStream;
    }

    @Override
    public BufferedReader getReader() throws IOException{
        return new BufferedReader(new InputStreamReader(this.getInputStream(), StandardCharsets.UTF_8));
    }

    public String getBody(){
        return this.body;
    }
    
   @Override
   public String getParameter(String name){
        return super.getParameter(name);
   }


    @Override
    public String getHeader(String name){
        return super.getHeader(name);
    }

    @Override
    public Enumeration<String>getHeaderNames(){
        return super.getHeaderNames();
    }

    @Override
    public Enumeration<String>getHeaders(String name){
        return super.getHeaders(name);
    }

}

2、过滤器校验

接着就是对所有参数进行SQL注入的校验,通过过滤器来实现。

代码语言:javascript
代码运行次数:0
复制
@WebFilter(urlPatterns = "/*",filterName = "RequestParamFilter")
@Configuration
public class RequestParamFilter implements Filter {

    private static final String POST_METHOD = "POST";

    @Value("#{'${requestParamFilter.urls}'.split(',')}")
    private List<String> urls;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    /**
     * @description sql注入过滤
     */
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String uri = request.getRequestURI();
        ServletResponse response = servletResponse;
        //获取getInputStream中的流数据,然后将body里的数据重新写入传递
        BodyReaderHttpServletRequest servletRequestWrapper = new BodyReaderHttpServletRequest(request);
        //校验SQL注入
        if (!(CollUtil.isNotEmpty(urls) && urls.contains(uri))) {
            // 获得所有请求参数
            String params = getAllParams(servletRequestWrapper);
            if (sqlValidate(params)) {
                LogUtils.info("请求入参非法!uri:{}",uri);
                throw BusinessEnum.PARAM_ERROR.newException("请求入参非法!");
            }
        }
        long currentTimeMillis = System.currentTimeMillis();
        filterChain.doFilter(servletRequestWrapper,response);
        LogUtils.info("request-uri:{}-->耗时:{}ms",uri, System.currentTimeMillis() - currentTimeMillis);
    }

    /**
     * 获取所有请求参数值
     */
    protected String getAllParams(BodyReaderHttpServletRequest servletRequestWrapper) {
        String params = "";
        //URL参数
        Map<String, String[]> parameterMap = servletRequestWrapper.getParameterMap();
        for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
            String[] values = entry.getValue();
            for (int i = 0; i < values.length; i++) {
                params += values[i];
            }
        }
        //POST请求体参数
        if (POST_METHOD.equals(servletRequestWrapper.getMethod())) {
            String body = servletRequestWrapper.getBody();
            JSONObject jsonObject = JSON.parseObject(body, JSONObject.class);
            for (Object value : jsonObject.values()) {
                if (value instanceof JSONObject) {
                    JSONObject nestedJsonObject = (JSONObject) value;
                    for (Object val : nestedJsonObject.values()) {
                        String valStr = String.valueOf(val);
                        params += valStr;
                    }
                } else {
                    String valueStr = String.valueOf(value);
                    params += valueStr;
                }
            }
        }
        return params;
    }

    /**
     * @description 匹配效验
     */
    protected boolean sqlValidate(String str){
        // 统一转为小写
        String s = str.toLowerCase();
        // 过滤掉的sql关键字,特殊字符前面需要加\\进行转义
        String badStr =
                "select|update|and|or|delete|insert|truncate|char|into|substr|ascii|declare|exec|count|master|into|drop|execute|table|"+
                        "char|declare|sitename|xp_cmdshell|like|from|grant|use|group_concat|column_name|" +
                        "information_schema.columns|table_schema|union|where|order|by|" +
                        "'\\*|\\;|\\-|\\--|\\+|\\,|\\//|\\/|\\%|\\#";
        //使用正则表达式进行匹配
        Pattern pattern = Pattern.compile(badStr);
        Matcher matcher = pattern.matcher(s);
        return matcher.find();
    }

    @Override
    public void destroy() {}

}

结果

以上就是主要的核心代码的实现了,总算把漏洞给修复了。最后一句,安全问题容不得半点马虎,我们不能仅仅满足于功能的实现,更要站在攻击者的角度去审视自己的代码,多去思考可能存在的安全隐患。而且和关联方合作时,也不能仅仅依靠对方或者自己单方面去保障数据安全,而是要共同制定严格的安全规范和交互标准,这样才能为用户提供更可靠的服务。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-01-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员小义 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 起因
  • 经过
  • 解决
    • 1、提取请求参数
    • 2、过滤器校验
  • 结果
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档