Redian新闻
>
Spring Boot 如何防护 XSS + SQL 注入攻击 ?终于懂了!

Spring Boot 如何防护 XSS + SQL 注入攻击 ?终于懂了!

公众号新闻

点击上方“芋道源码”,选择“设为星标

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 
来源:网络

1. XSS跨站脚本攻击

① XSS漏洞介绍

跨站脚本攻击XSS是指攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被解析执行,从而达到恶意攻击用户的目的。XSS攻击针对的是用户层面的攻击!

② XSS漏洞分类

存储型XSS: 存储型XSS,持久化,代码是存储在服务器中的,如在个人信息或发表文章等地方,插入代码,如果没有过滤或过滤不严,那么这些代码将储存到服务器中,用户访问该页面的时候触发代码执行。这种XSS比较危险,容易造成蠕虫,盗窃cookie

反射型XSS: 非持久化,需要欺骗用户自己去点击链接才能触发XSS代码(服务器中没有这样的页面和内容),一般容易出现在搜索页面

DOM型XSS: 不经过后端,DOM-XSS漏洞是基于文档对象模型(Document Objeet Model,DOM)的一种漏洞,DOM-XSS是通过url传入参数去控制触发的,其实也属于反射型XSS。

③ 防护建议

  • 限制用户输入,表单数据规定值得类型,例如年龄只能是int,name为字母数字组合。
  • 对数据进行html encode处理。
  • 过滤或移除特殊的html标签。
  • 过滤javascript事件的标签。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

2. SQL注入攻击

① SQL注入漏洞介绍

SQL注入(SQLi)是一种注入攻击,可以执行恶意SQL语句。它通过将任意SQL代码插入数据库查询,使攻击者能够完全控制Web应用程序后面的数据库服务器。攻击者可以使用SQL注入漏洞绕过应用程序安全措施;可以绕过网页或Web应用程序的身份验证和授权,并检索整个SQL数据库的内容;还可以使用SQL注入来添加,修改和删除数据库中的记录;

SQL注入漏洞可能会影响使用SQL数据库(如MySQL,Oracle,SQL Server或其他)的任何网站或Web应用程序。犯罪分子可能会利用它来未经授权访问用户的敏感数据:客户信息,个人数据,商业机密,知识产权等。SQL注入攻击是最古老,最流行,最危险的Web应用程序漏洞之一。

②防护建议

使用mybatis中#{}可以有效防止sql注入。

使用#{}时:

<select id="getBlogById" resultType="Blog" parameterType=”int”>
       select id,title,author,content
       from blog where id=#{id}
</select>

打印出执行的sql语句,会看到sql是这样的:

select id,title,author,content from blog where id = ?

不管输入什么参数,打印出的sql都是这样的。这是因为mybatis启用了预编译功能,在sql执行前,会先将上面的sql发送给数据库进行编译,执行时,直接使用编译好的sql,替换占位符“?”就可以了。因为sql注入只能对编译过程起作用,所以像#{}这样预编译成?的方式就很好地避免了sql注入的问题。

mybatis是如何做到sql预编译的呢?

其实在框架底层,是jdbc中的PreparedStatement类在起作用,PreparedStatement是我们很熟悉的Statement的子类,它的对象包含了编译好的sql语句。这种“准备好”的方式不仅能提高安全性,而且在多次执行一个sql时,能够提高效率,原因是sql已编译好,再次执行时无需再编译。

使用${}

<select id="orderBlog" resultType="Blog" parameterType=”map”>
       select id,title,author,content
       from blog order by ${orderParam}
</select>

仔细观察,内联参数的格式由“#{xxx}”变为了${xxx}。如果我们给参数“orderParam”赋值为”id”,将sql打印出来,是这样的:

select id,title,author,contet from blog order by id

显然,这样是无法阻止sql注入的,参数会直接参与sql编译,从而不能避免注入攻击。但涉及到动态表名和列名时,只能使用“${}”这样的参数格式,所以,这样的参数需要我们在代码中手工进行处理来防止注入。

其实,这些都在Java面试库小程序上都有答案,如果你近期准备面试跳槽,建议在上面刷题,涵盖 2000+ 道 Java 面试题,几乎覆盖了所有主流技术面试题。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

3. SpringBoot中如何防止XSS攻击和sql注入

对于Xss攻击和Sql注入,我们可以通过过滤器来搞定,可根据业务需要排除部分请求

① 创建Xss请求过滤类XssHttpServletRequestWraper

代码如下:

public class XssHttpServletRequestWraper extends HttpServletRequestWrapper {

    Logger log = LoggerFactory.getLogger(this.getClass());


    public XssHttpServletRequestWraper() {
        super(null);
    }

    public XssHttpServletRequestWraper(HttpServletRequest httpservletrequest) {
        super(httpservletrequest);
    }

 //过滤springmvc中的 @RequestParam 注解中的参数
    public String[] getParameterValues(String s) {

        String str[] = super.getParameterValues(s);
        if (str == null) {
            return null;
        }
        int i = str.length;
        String as1[] = new String[i];
        for (int j = 0; j < i; j++) {
            //System.out.println("getParameterValues:"+str[j]);
            as1[j] = cleanXSS(cleanSQLInject(str[j]));
        }
        log.info("XssHttpServletRequestWraper净化后的请求为:==========" + as1);
        return as1;
    }

 //过滤request.getParameter的参数
    public String getParameter(String s) {
        String s1 = super.getParameter(s);
        if (s1 == null) {
            return null;
        } else {
            String s2 = cleanXSS(cleanSQLInject(s1));
            log.info("XssHttpServletRequestWraper净化后的请求为:==========" + s2);
            return s2;
        }
    }

 //过滤请求体 json 格式的
    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(inputHandlers(super.getInputStream ()).getBytes ());

        return new ServletInputStream() {

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

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

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

            @Override
            public void setReadListener(ReadListener readListener) { }
        };
    }

 
    public   String inputHandlers(ServletInputStream servletInputStream){
        StringBuilder sb = new StringBuilder();
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new InputStreamReader(servletInputStream, Charset.forName("UTF-8")));
            String line = "";
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (servletInputStream != null) {
                try {
                    servletInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return  cleanXSS(sb.toString ());
    }

    public String cleanXSS(String src) {
        String temp = src;

        src = src.replaceAll("<""<").replaceAll(">"">");
        src = src.replaceAll("\\(""(").replaceAll("\\)"")");
        src = src.replaceAll("'""'");
        src = src.replaceAll(";"";");
        //bgh 2018/05/30  新增
        /**-----------------------start--------------------------*/
        src = src.replaceAll("<""& lt;").replaceAll(">""& gt;");
        src = src.replaceAll("\\(""& #40;").replaceAll("\\)""& #41");
        src = src.replaceAll("eval\\((.*)\\)""");
        src = src.replaceAll("[\\\"\\\'][\\s]*javascript:(.*)[\\\"\\\']""\"\"");
        src = src.replaceAll("script""");
        src = src.replaceAll("link""");
        src = src.replaceAll("frame""");
        /**-----------------------end--------------------------*/
        Pattern pattern = Pattern.compile("(eval\\((.*)\\)|script)",
                Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(src);
        src = matcher.replaceAll("");

        pattern = Pattern.compile("[\\\"\\'][\\s]*javascript:(.*)[\\\"\\']",
                Pattern.CASE_INSENSITIVE);
        matcher = pattern.matcher(src);
        src = matcher.replaceAll("\"\"");

        // 增加脚本
        src = src.replaceAll("script""").replaceAll(";""")
                /*.replaceAll("\"", "").replaceAll("@", "")*/
                .replaceAll("0x0d""").replaceAll("0x0a""");

        if (!temp.equals(src)) {
            // System.out.println("输入信息存在xss攻击!");
            // System.out.println("原始输入信息-->" + temp);
            // System.out.println("处理后信息-->" + src);

            log.error("xss攻击检查:参数含有非法攻击字符,已禁止继续访问!!");
            log.error("原始输入信息-->" + temp);

            throw new CustomerException("xss攻击检查:参数含有非法攻击字符,已禁止继续访问!!");
        }
        return src;
    }

    //输出
    public void outputMsgByOutputStream(HttpServletResponse response, String msg) throws IOException {
        ServletOutputStream outputStream = response.getOutputStream(); //获取输出流
        response.setHeader("content-type""text/html;charset=UTF-8"); //通过设置响应头控制浏览器以UTF-8的编码显示数据,如果不加这句话,那么浏览器显示的将是乱码
        byte[] dataByteArr = msg.getBytes("UTF-8");// 将字符转换成字节数组,指定以UTF-8编码进行转换
        outputStream.write(dataByteArr);// 使用OutputStream流向客户端输出字节数组
    }

    // 需要增加通配,过滤大小写组合
    public String cleanSQLInject(String src) {
        String lowSrc = src.toLowerCase();
        String temp = src;
        String lowSrcAfter = lowSrc.replaceAll("insert""forbidI")
                .replaceAll("select""forbidS")
                .replaceAll("update""forbidU")
                .replaceAll("delete""forbidD").replaceAll("and""forbidA")
                .replaceAll("or""forbidO");

        if (!lowSrcAfter.equals(lowSrc)) {
            log.error("sql注入检查:输入信息存在SQL攻击!");
            log.error("原始输入信息-->" + temp);
            log.error("处理后信息-->" + lowSrc);
            throw new CustomerException("sql注入检查:参数含有非法攻击字符,已禁止继续访问!!");

        }
        return src;
    }

}

② 把请求过滤类XssHttpServletRequestWraper添加到Filter中,注入容器

@Component
public class XssFilter implements Filter {

    Logger log  = LoggerFactory.getLogger(this.getClass());

    // 忽略权限检查的url地址
    private final String[] excludeUrls = new String[]{
            "null"
    };

    public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2)
            throws IOException, ServletException 
{

        HttpServletRequest req = (HttpServletRequest) arg0;
        HttpServletResponse response = (HttpServletResponse) arg1;

        String pathInfo = req.getPathInfo() == null ? "" : req.getPathInfo();
        //获取请求url的后两层
        String url = req.getServletPath() + pathInfo;
        //获取请求你ip后的全部路径
        String uri = req.getRequestURI();
        //注入xss过滤器实例
        XssHttpServletRequestWraper reqW = new XssHttpServletRequestWraper(req);

        //过滤掉不需要的Xss校验的地址
        for (String str : excludeUrls) {
            if (uri.indexOf(str) >= 0) {
                arg2.doFilter(arg0, response);
                return;
            }
        }
        //过滤
        arg2.doFilter(reqW, response);
    }
    public void destroy() {
    }
    public void init(FilterConfig filterconfig1) throws ServletException {
    }
}

上述代码已经可以完成 请求参数、JSON请求体 的过滤,但对于json请求体还有其他的方式实现,有兴趣的请看下面的扩展!

扩展:还可以重写spring中的MappingJackson2HttpMessageConverter来过滤Json请求体

因为请求体在进出Contoroller时,会经过MappingJackson2HttpMessageConverter的一个转换,把请求体转换成我们需要的json格式,所以可以在这里边做一些修改!

@Configuration
public class MyConfiguration {

    @Bean
    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(){
        //自定义转换器
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();


        //转换器日期格式设置
        ObjectMapper objectMapper = new ObjectMapper();
        SimpleDateFormat smt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        objectMapper.setDateFormat(smt);
        converter.setObjectMapper(objectMapper);

        //转换器添加自定义Module扩展,主要是在这里做XSS过滤的!!,其他的是其他业务,不用看
        SimpleModule simpleModule = new SimpleModule();
        //添加过滤逻辑类!
        simpleModule.addDeserializer(String.class,new StringDeserializer());
        converter.getObjectMapper().registerModule(simpleModule);

        //设置中文编码格式
        List<MediaType> list = new ArrayList<>();
        list.add(MediaType.APPLICATION_JSON_UTF8);
        converter.setSupportedMediaTypes(list);

        return converter;
    }

}

真正的过滤逻辑类StringDeserializer

//检验请求体的参数
@Component
public class StringDeserializer extends JsonDeserializer<String{
    @Override
    public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
        String str = jsonParser.getText().trim();
        //sql注入拦截
        if (sqlInject(str)) {
          throw new CustomerException("参数含有非法攻击字符,已禁止继续访问!");
        }

        return xssClean(str);

    }

    public boolean sqlInject(String str) {

        if (StringUtils.isEmpty(str)) {
            return false;
        }

        //去掉'|"|;|\字符
        str = org.apache.commons.lang3.StringUtils.replace(str, "'""");
        str = org.apache.commons.lang3.StringUtils.replace(str, "\"""");
        str = org.apache.commons.lang3.StringUtils.replace(str, ";""");
        str = org.apache.commons.lang3.StringUtils.replace(str, "\\""");

        //转换成小写
        str = str.toLowerCase();

        //非法字符
        String[] keywords = {"master""truncate""insert""select""delete""update""declare""alert","alter""drop"};

        //判断是否包含非法字符
        for (String keyword : keywords) {
            if (str.indexOf(keyword) != -1) {
                return true;
            }
        }
        return false;

    }

    //xss攻击拦截

    public String xssClean(String value) {
        if (value == null || "".equals(value)) {
            return value;
        }

        //非法字符
        String[] keywords = {"<"">""<>""()"")""(""javascript:""script","alter""''","'"};
        //判断是否包含非法字符
        for (String keyword : keywords) {
            if (value.indexOf(keyword) != -1) {
               throw new CustomerException("参数含有非法攻击字符,已禁止继续访问!");
            }
        }

        return value;
    }
}

使用这种形式也可以完成json请求体的过滤,但个人更推荐使用XssHttpServletRequestWraper的形式来完成xss过滤!!



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

已在知识星球更新源码解析如下:

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
Agustín Hernández:中美洲建筑背景下的未来主义巨构SQL能完成哪方面的计算?一文详解关系代数和SQL语法别只会 SpringMVC 了!Spring 又官宣了一个更牛逼的替代框架!SpringBoot 使用线程池如何控制主线程和子线程的事务多地停课!甲流进入高发期,如何防护,一文说清楚→深入剖析 Spring Boot 的 SPI 机制Spring Boot + 规则引擎 URule,太强了!一段青绿色的快闪硬核观察 #910 新必应遭到提示注入攻击,泄露了其秘密打卡,Goodbye Spring BreakSpring Boot |如何让你的 bean 在其他 bean 之前完成加载SpringBoot 如何快速过滤出一次请求的所有日志?了解那些“奇葩”SQL写法,快速写出高效率SQL【周末综艺会6期】Spring Garden Show 和板蓝花儿开SpringBoot 中 MybatisX 插件的简单使用教程(超详细!!)别再自己瞎写工具类了,SpringBoot 内置工具类应有尽有,建议收藏!!一条SQL如何被MySQL架构中的各个组件操作执行的?Spring循环依赖那些事儿(含Spring详细流程图)这种致癌物,只要白天出门就会接触到,该如何防护?英国伦敦爱丁堡带孩子游记 Spring 2023隐语开源首个工业级多方安全数据分析系统SCQL:像写SQL一样「易用」隐私计算Spring Boot + 规则引擎Drools,强!为何今年沙尘天气格外多?孩子应如何防护→中西合璧,天下无敌​9块和59块的黄油差在哪,看完这篇终于懂了砂子和眼睛,匣子和宝珠,语法和语言:论人在语言中的地位过年穿新衣:分放新队服欢乐记SpringBoot + Druid,完美监控 MySQL 性能Spring break下场直播预告| 老牌子厨具的底气哪来的?懂了懂了我懂了!SpringBoot+Flowable 快速实现工作流,so easy!SpringBoot 应用的新命令行界面:Just这是我见过最好的SpringBoot系统!SpringBoot 实现 Excel 导入导出,百万数据量,性能爆表!我终于懂了,那个拒绝收下老板送花的女同事天赋“易昺(bǐng)”,创造历史!如果有人热捧权威,无论是人还是偶像(包括由他们自己定义的科学),那么对他的言行,你要当心了。SpringBoot 实现 MySQL 百万级数据量导出并避免 OOM 的解决方案Just:Spring Boot 应用的新命令行界面
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。