首页 > 数据库 >前后端不分离 "老" 项目,SQL 注入漏洞处理实践

前后端不分离 "老" 项目,SQL 注入漏洞处理实践

时间:2024-08-28 15:03:25浏览次数:5  
标签:return String 分离 private 漏洞 SQL new public

前言

接上篇的 XSS 漏洞处理实践,这次是针对 SQL 注入漏洞的处理实践。我们的后端代码,在项目初期没有使用世面上的 ORM 框架,而是使用 springJdbcTemplate 简单的封装了增删改查的 DAO 方法。然后暴露一通用的 Controller 层接口,这样无论是前端还是后端都更加 “方便了”! 前端可以直接指定参数和将要使用的 sql 标识,然后调统一的接口,后端可以直接将通用的这个 DAO,传什么都行(可以写 sql 语句,或是 xml 中写的sql 等等),总之就是非常的通用。

随着项目的业务的增长,后端代码这套 "简便" 的封装逻辑、“开发范式”已经是项目安全漏洞的罪魁祸首。伴随而来的严重漏洞主要有:

  1. 越权漏洞
  2. SQL 注入漏洞

对于越权漏洞,此篇不细说。主要是大量的前端页面调用的都是后端那个通用的 Controller 层的方法,权限根本就控不了。对于 SQL 注入问题,归咎于那个通用的 DAO,代码中充斥着大量的 SQL 拼接,并且大量的 Controller 层直接 try catch 调用 service 异常直接将 SQL 异常信息返回,导致表以及字段信息严重暴露。

SQL 注入漏洞处理实践

屏蔽 SQL 异常信息返回客户端

由于项目中存在大量的 Controller 方法直接 try catch 调用 service的异常,并将异常信息直接返回给客户端,这里要将 SQL 异常信息屏蔽掉,防止暴露数据库表细节。怎么改呢?挨个改 Controller 方法肯定改不完的, 所以这里就优先针对这个通用的 DAO 下手了。

@Override
    public List<Map<String, Object>> queryList(String sql, HashMap<String, Object> params) {
        // ...
        try {
            log.info("queryList sql=> [{}]", sql);
            log.info("queryList params=> {}", Arrays.toString(oarr));
            List<Map<String, Object>> l = getJdbcTemplate(tableName).query(sql, oarr, this.columnMapper());
            EncryptUtil.decryptDataBatch(l, sql, "mobile");
            return l;
        } catch (Exception e) {
            this.printErrorContext(e,sql, params, p,AlarmMetricEnum.BASE_DAO_QUERY_LIST);
            throw e;
        }
    }

上面是一个通用的 queryList 方法,try catch 住最终的 getJdbcTemplate(tableName).query(sql, oarr, this.columnMapper());然后在 catch 中去记录 sql 执行异常并通知,最后将异常向上抛出去。这里我的思路是修改异常的 msg 信息,将原生异常 msg 替换掉,不改变原生异常类型,因为保不准调用方会有拿这个异常类型去判断去做些什么处理。

这里我是直接写了一个切面,切这个 DAO 类,代码如下:

@Aspect
@Component
public class DatabaseExceptionAspect {

    private static final Logger log = LoggerFactory.getLogger(DatabaseExceptionAspect.class);

    @AfterThrowing(pointcut = "execution(* *.*.*(..))", throwing = "ex") //
    public void handleException(Exception ex) {
        log.info("DatabaseExceptionAspect, 捕获异常:{}", ex.getMessage());
        if (ex instanceof BadSqlGrammarException) {
            handleBadSqlGrammarException((BadSqlGrammarException) ex);
        } else if (ex instanceof DuplicateKeyException) {
            handleDuplicateKeyException((DuplicateKeyException) ex);
        } else if (ex instanceof DataIntegrityViolationException) {
            handleDataIntegrityViolationException((DataIntegrityViolationException) ex);
        } else if (ex instanceof DataAccessException) {
            handleDataAccessException((DataAccessException) ex);
        }
    }
    
    private void handleBadSqlGrammarException(BadSqlGrammarException ex) throws BadSqlGrammarException {
        String filteredMessage = filterSensitiveInformation(ex.getMessage());
        throw new BadSqlGrammarException("", filteredMessage, new SQLException(filteredMessage));
    }
    // ... 省略

    private String filterSensitiveInformation(String message) {
        // 可以对message 处理,此处先返回固定值
        return "数据库操作异常";
    }
}

使用 @AfterThrowing 通知,然后判断异常的类型。这里将能够暴露数据库敏感信息的异常进行判断,然后替换 msg 再向上抛出去即可

过滤器 Filter 处理 SQL 注入

后端存在 sql 注入的问题 sql 在短时间内改是改不完的,这里选用 Filter 来处理 SQL 注入,也仅仅是简单的防御,采用正则去校验传参,没有任何正则表达式可以完美地防止所有SQL注入攻击,只是防御SQL注入的多个层次中的一层。

看了一下我们的接口请求类型存在普通的 GET 请求,存量大的 POST + application/x-www-form-urlencoded 以及 POST + application/json,还有附件上传的 POST + form-data 类型。这样的老项目接口没有遵从 RESTFUL 风格并不奇怪,都是在接口方法上标注 @RequestMapping ,全由前端决定是 POST还是 GET请求。对于POST + form-data 这种只用于附件上传,所以在过滤器中可以不处理,下面是代码:

public class SqlInjectFilter implements Filter {

    private static final Logger log = LoggerFactory.getLogger(SqlInjectFilter.class);

    private static final String CONF_KEY = "sql_inject_filter_config";

    private ConfigProp configProp = new ConfigProp();

    private static final AntPathMatcher ANT_MATCHER = new AntPathMatcher();

    // 没有任何正则表达式可以完美地防止所有SQL注入攻击,只是防御SQL注入的多个层次中的一层
    private static final String REFINED_SQL_INJECTION_REGEX =
            "(\\bEXEC(UTE)?\\b|UNION(\\s+ALL)?\\s+SELECT|INSERT\\s+INTO\\s+.+?VALUES|UPDATE\\s+.+?SET|DELETE\\s+FROM|\\bALTER\\b|\\bDROP\\b|\\bTRUNCATE\\b|\\bCREATE\\b|\\bGRANT\\b|\\bREVOKE\\b|\\bRENAME\\b|\\bSHOW\\b|\\bUSE\\b|\\/\\*.*?\\*\\/|--[\\s\\S]*?\\n|SELECT\\s+.*?\\s+FROM)";

    private static final Pattern SQL_PATTERN = Pattern.compile(REFINED_SQL_INJECTION_REGEX, Pattern.CASE_INSENSITIVE);

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse res = (HttpServletResponse) servletResponse;

        String requestURI = req.getRequestURI();
        // 对特定 path 放行
        if(StringUtil.ignoreSuffix(requestURI)){
            chain.doFilter(servletRequest, servletResponse);
            return;
        }
        
        // 未开启 filter 放行
        if (!this.configProp.isOpen) {
            chain.doFilter(servletRequest, servletResponse);
            return;
        }

        // 在排除路径内放行
        if (exclude(requestURI, this.configProp)) {
            chain.doFilter(servletRequest, servletResponse);
            return;
        }

        // 构建 wrapper 包装 request
        SqlInjectHttpServletRequestWrapper sqlInjectHttpServletRequestWrapper = new SqlInjectHttpServletRequestWrapper(req);
        
        // json 和 其他contentType 分开处理
        Map<String, Object> parameterMap = Maps.newTreeMap();
        Map<String, Object> jsonParameterMap = Maps.newTreeMap();
        this.loadParameterMap(parameterMap, jsonParameterMap, sqlInjectHttpServletRequestWrapper);

        // 递归校验是否有SQL关键字
        if (validateParametersForSQLInjection(parameterMap, res) || validateParametersForSQLInjection(jsonParameterMap, res)) {
            return;
        }

        chain.doFilter(sqlInjectHttpServletRequestWrapper, servletResponse);
    }

    private <T> boolean validateParametersForSQLInjection(T value, HttpServletResponse response) throws IOException {
        if (value instanceof String) {
            return !isSqlInject((String) value, response);
        } else if (value instanceof Map) {
            for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
                if (validateParametersForSQLInjection(entry.getValue(), response)) {
                    return true;
                }
            }
        } else if (value instanceof List) {
            for (Object item : (List<?>) value) {
                if (validateParametersForSQLInjection(item, response)) {
                    return true;
                }
            }
        }
        return false;
    }

    private void loadParameterMap(Map<String, Object> paramMap, Map<String, Object> jsonParamMap, SqlInjectHttpServletRequestWrapper requestWrapper) {
        // 区分 json 和其他类型请求参数,分开处理
        if ("POST".equalsIgnoreCase(requestWrapper.getMethod()) && isJsonContentType(requestWrapper.getContentType())) {
            String body = requestWrapper.getBody();
            if (JSONUtil.isTypeJSON(body)) {
                Object jsonBody = JSONObject.parse(body);
                if (jsonBody instanceof JSONObject) {
                    JSONObject jsonObject = (JSONObject) jsonBody;
                    jsonParamMap.putAll(jsonObject.getInnerMap());
                } else if (jsonBody instanceof JSONArray) {
                    jsonParamMap.put("body", jsonBody);
                }
            }
        }

        Map<String, String[]> parameterMap = requestWrapper.getParameterMap();
        Set<Map.Entry<String, String[]>> entries = parameterMap.entrySet();
        for (Map.Entry<String, String[]> entry : entries) {
            String[] values = entry.getValue();
            if (values == null) {
                continue;
            }
            if (values.length > 1) {
                paramMap.put(entry.getKey(), Arrays.asList(values));
            } else if (values.length == 1) {
                paramMap.put(entry.getKey(), values[0]);
            }
        }
    }

    private boolean exclude(String requestURI, ConfigProp configProp) {
        if (configProp.getExcludeUrl().contains(requestURI)) {
            return true;
        }
        for (String pattern : configProp.getExcludeUrlPattern()) {
            if (ANT_MATCHER.match(pattern, requestURI)) {
                return true;
            }
        }
        return false;
    }


    private boolean isSqlInject(String value, HttpServletResponse res) throws IOException {
        if (value!= null && SQL_PATTERN.matcher(value).find()) {
            log.info(" SqlInjectionFilter isSqlInject,入参中有非法字符: {}", value);
            outMessage(res, "存在非法请求参数");
            return false;
        }
        return true;
    }

    private void outMessage(HttpServletResponse res, String message) throws IOException {
        res.setCharacterEncoding("utf-8");
        res.setHeader("Cache-Control", "no-cache");
        res.setContentType("text/html");
        PrintWriter pw = res.getWriter();
        pw.print(message);
        pw.flush();
        pw.close();
    }

    private boolean isJsonContentType(String contentType) {
        if (contentType == null) {
            return false;
        }
        return contentType.toLowerCase().startsWith("application/json");
    }


    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 加载管控点配置
        String config = ConfigUtil.getApplication(CONF_KEY);
        if (JSONUtil.isTypeJSON(config)) {
            this.configProp = JSON.parseObject(config, ConfigProp.class);
        }
        
        // 加载 init 参数
        String excludeUrl = filterConfig.getInitParameter("excludeUrl");
        if (StringUtil.isNotBlank(excludeUrl)) {
            this.configProp.getExcludeUrl().addAll(Arrays.asList(excludeUrl.split(",")));
        }

        String excludeUrlPattern = filterConfig.getInitParameter("excludeUrlPattern");
        if (StringUtil.isNotBlank(excludeUrlPattern)) {
            this.configProp.getExcludeUrlPattern().addAll(Arrays.asList(excludeUrlPattern.split(",")));
        }
    }

    private static class ConfigProp {
        // 是否开启
        boolean isOpen = true;
        // 排除的路径
        List<String> excludeUrl = new ArrayList<>();
        // 排除路径规则模板
        List<String> excludeUrlPattern = new ArrayList<>();

        public boolean isOpen() {
            return isOpen;
        }

        public void setOpen(boolean open) {
            isOpen = open;
        }

        public List<String> getExcludeUrl() {
            return excludeUrl;
        }

        public void setExcludeUrl(List<String> excludeUrl) {
            this.excludeUrl = excludeUrl;
        }

        public List<String> getExcludeUrlPattern() {
            return excludeUrlPattern;
        }

        public void setExcludeUrlPattern(List<String> excludeUrlPattern) {
            this.excludeUrlPattern = excludeUrlPattern;
        }
    }

}
public class SqlInjectHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private static final Logger log = LoggerFactory.getLogger(SqlInjectHttpServletRequestWrapper.class);

    private final String body;
    private boolean isFormDataContentType = false;

    public SqlInjectHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        if (ServletFileUpload.isMultipartContent(request)) { // form-data 不处理
            body = null;
            isFormDataContentType = true;
        } else {
            StringBuilder stringBuilder = new StringBuilder();
            BufferedReader bufferedReader = null;
            try {
                InputStream inputStream = request.getInputStream();

                if (inputStream != null) {
                    bufferedReader = new BufferedReader(new InputStreamReader(inputStream));

                    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) {
                log.error("Error reading the request body...");
            } finally {
                if (bufferedReader != null) {
                    try {
                        bufferedReader.close();
                    } catch (IOException ex) {
                        log.error("Error closing bufferedReader...");
                    }
                }
            }
            body = stringBuilder.toString();
        }
    }


    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (isFormDataContentType) {
            return super.getInputStream();
        }
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
            }

            @Override
            public int read() {
                return byteArrayInputStream.read();
            }
        };
    }

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

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

    @Override
    public Map<String, String[]> getParameterMap() {
        return super.getParameterMap();
    }

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

    @Override
    public String[] getParameterValues(String name) {
        return super.getParameterValues(name);
    }

    public String getBody() {
        return this.body;
    }

}

说一下处理的思路:

  1. 对这个 Filter 做一些额外配置,即在 init 方法中进行初始化配置,可以排除路径等等
  2. doFilter 方法中首先对放行 path 进行判断,然后使用自定义的 wrapper 包装 request。因为这里也要对 post + application/json请求的参数也要过滤,而 request 对象读取一次流数据,流就关闭了,所以需要自定义的 wrapper 包装 request,将请求内容缓存。之后控制器再调用getReader读取请求内容就不会由于在 Filter 读取过而报错。
  3. 在自定义的 SqlInjectHttpServletRequestWrapper 中的构造首先判断了下是否是附件上传类型,如果是就不将 inputStream 转为 json字符串了,同样在 getInputStreamgetReader方法中都进行了判断。
  4. 在过滤器 loadParameterMap 方法中使用两个 map 去存储请求参数,然后在 validateParametersForSQLInjection 方法中递归去判断请求参数的值中是否存在非法恶意 sql
  5. isSqlInject 方法中使用正则去匹配请求参数的值,这里使用部分匹配,比如 select 不会认为是非法,但是 select * from 就是非法的

标签:return,String,分离,private,漏洞,SQL,new,public
From: https://www.cnblogs.com/hkz329/p/18384717

相关文章

  • MySQL:简述对索引的认识
    一、为什么要有索引?一般的应用系统,读操作的比例远远大于写操作的比例,而且插入操作和一般的更新操作很少出现性能问题。在生产环境中,我们遇到最多的,也是最容易出现性能问题的,还是一些复杂的查询操作,因此对查询语句的优化显然是重中之重。说起查询优化,就不得不提到索引了。......
  • Windows系统安装MySQL
    下载MySQL打开网址MySQL::DownloadMySQLCommunityServer点击图下所示位置Download进入图下所示界面,点击图下所示位置不登录下载已下载完成安装MySQL将下载好的压缩包解压到一个专门的位置,该软件为绿色版软件,解压即可使用配置环境变量我们想要让MySQL可以在wind......
  • 【MySQL】mysql索引和事务(面试经典问题)
    欢迎关注个人主页:逸狼创造不易,可以点点赞吗~如有错误,欢迎指出~目录mysql索引代价查看索引创建索引 删除索引索引背后的数据结构B树B+树B+树与B树的区别B+树的优势mysql事务 事务涉及的四个核心特性:隔离性详细解释脏读不可重复读幻读隔离性的四......
  • postgresql下Schema和DataBase
    database—>schema—>table1.同一个实例下,不同database是不能相互访问的,即独立的。2.同一个数据库,不同模式下的表是可以相互访问,即可共享的3.不同模式下,表名可以是一样。也就是表在模式下是独立。##授权某个库下的某个模式下有创建表的权限grantcreateondatabasedb_na......
  • sqlserver调优的相关查询
    SQLServer系统卡顿可能由多种原因引起,如硬件资源不足、查询性能问题、锁争用、并发连接过多等。以下是一些排查和优化步骤:1.检查硬件资源CPU使用率:检查SQLServer的CPU使用情况,特别是是否有单个查询占用了过多的CPU资源。使用TaskManager或PerformanceMonitor查......
  • RapidCMS 几个常见漏洞
    侵权声明本文章中的所有内容(包括但不限于文字、图像和其他媒体)仅供教育和参考目的。如果在本文章中使用了任何受版权保护的材料,我们满怀敬意地承认该内容的版权归原作者所有。如果您是版权持有人,并且认为您的作品被侵犯,请通过以下方式与我们联系:[[email protected]]。我们将在确......
  • ZoneMinder视频监控系统SQL注入
    0x01漏洞描述:ZoneMinder(简称ZM)是一套基于Linux操作系统的摄像机的视像数据监控的应用软件(大家可以简单理解为网络摄像机)。ZoneMinder支持单一或多台视像镜头应用,包括摄取、分析、记录(包括移动侦测功能)、和监视来源。index.php接口处存在sql注入,未经身份验证的远程攻击者除......
  • H3C-IMC智能管理中心RCE漏洞复现
    0x01漏洞描述:autoDeploy接口中存在远程代码执行漏洞,未经身份攻击者可通过该漏洞在服务器端任意执行代码,写入后门,获取服务器权限,进而控制整个web服务器。该漏洞利用难度较低,建议受影响的用户尽快修复。0x02搜索语句:Fofa:(title="用户自助服务"&&body="/selfservice/java......
  • MySQL索引底层实现原理
    索引的本质MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。提取句子主干,就可以得到索引的本质:索引是数据结构。我们知道,数据库查询是数据库的最主要功能之一。我们都希望查询数据的速度能尽可能的快,因此数据库系统的设计者会从查询算法的角度进行优化。最......
  • 查看mysql的版本号
    1.1在命令行登录mysql,即可看到mysql的版本号[root@heyong~]#mysql-uroot-pEnterpassword:WelcometotheMySQLmonitor.Commandsendwith;or\g.YourMySQLconnectionidis487032Serverversion:5.7.17MySQLCommunityServer(GPL)Copyright(c)2000,......