数据和代码要分清(java中如何避免安全问题)

简介: Web 安全方面的很多漏洞,都是源自把数据当成了代码来执行,也就是注入类问题

Web 安全方面的很多漏洞,都是源自把数据当成了代码来执行,也就是注入类问题,比如:


客户端提供给服务端的查询值,是一个数据,会成为 SQL 查询的一部分。攻击者通过修改这个值注入一些 SQL,来达到在服务端运行 SQL 的目的,相当于把查询条件的数据变为了查询代码。这个叫做SQL 注入。


对于规则引擎,我们可能会用动态语言做一些计算,和 SQL 注入一样外部传入的数据只能当做数据使用,如果被攻击者利用传入了代码,那么代码可能就会被动态执行。这个叫代码注入。


对于用户注册、留言评论这些功能,服务端会从客户端收集一些信息,本来用户名、邮箱这类信息是纯文本信息,但是攻击者把信息替换为了 JavaScript 代码。这些信息在页面呈现时,可能就相当于执行了 JavaScript 代码。而服务端可能把这样的代码,当作普通信息保存到了数据库。攻击者通过构建 JavaScript 代码来实现修改页面呈现、盗取信息,甚至蠕虫攻击的方式,叫做 XSS(跨站脚本)攻击。

一、SQL注入能做的比你想象的还要多

如果你还不知道什么是SQL注入,可以查看我的这篇文章进行了解天天听SQL注入,SQL注入到底是怎么注入的?


SQL注入最经典的例子,就是传入 or 1=1 作为密码实现登录。而这种简单的攻击方式,在十几年前可以突破很多后台系统,但现在不行了。


近几年大家的安全意识增强了,都知道使用参数化查询来避免SQL注入问题(例如mybatis中的#{}取值方式)。但还是有几点需要注意


第一、SQL注入不只是存在于get请求(不只是存在URL传参中)


从注入的复杂程度来说,修改URL和修改post请求的请求体没有区别,因为攻击者是使用工具执行请求,而不是修改浏览器上面的URL执行请求的。甚至cookie也有可能用来注入,任何提供数据的地方都可能称为注入点。


第二、不返回数据的接口同样存在注入问题


攻击者完全可以构造不正确的SQL语句,使执行出错,如果服务端直接显示了错误信息,那攻击者需要的数据很有可能被带出来。即使没有错误信息,攻击者也可以通过盲注的方式进行攻击。


对于错误信息,在开发中服务端应当使用全局错误捕获来拦截所有的异常,并封装好自定义的错误提示返回客户端。


第三、SQL注入不仅仅是可以用来突破登录


SQL注入完全可以实现下载整个数据库的内容。其次根据木桶原理,整个系统的安全性受限于安全级别最低的那块短板。因此对于安全问题,并不是只加强防范某些重点模块就行。


在mybatis中,{}”是参数化的方式,“${}”只是占位符替换。


比如 LIKE 语句。因为使用“#{}”会为参数带上单引号,导致 LIKE 语法错误,所以一些开发人员会退而求其次,选择“${}”的方式,比如


@Select("SELECT id,name FROM `userdata` WHERE name LIKE '%${name}%'")

List<UserData> selectByName(@Param("name") String name);

正确的做法是,使用“#{}”来参数化 name 参数,对于 LIKE 操作可以使用 CONCAT 函数来拼接 % 符号:


@Select("SELECT id,name FROM `userdata` WHERE name LIKE CONCAT('%',#{name},'%')")

List<UserData> selectByNameRight(@Param("name") String name);

或者使用bind标签对原字符串前后绑定百分号后再使用#{}获取绑定后的值使用


二、XSS必须严防丝堵

XSS 问题的根源是,原本是让用户传入或输入正常数据的地方,被攻击者替换为了 JavaScript 脚本,页面没有经过转义直接显示了这个数据,然后脚本就被执行了。更严重的是,脚本没有经过转义就保存到了数据库中,随后页面加载数据的时候,数据中混入的脚本又当做代码执行了。攻击者就可以利用这个漏洞来盗取敏感数据,诱骗用户访问钓鱼网站等。


写一段代码测试下。首先,服务端定义两个接口,其中 index 接口查询用户名信息返回给xss页面,save 接口使用 @RequestParam 注解接收用户名,并创建用户保存到数据库;然后,重定向浏览器到 index 接口


@Controller
@RequestMapping("xss")
public class XssController {
    @Resource
    private UserRepository userRepository;
    @GetMapping
    public String index(ModelMap modelMap) {
        //查数据库
        User user = userRepository.findById(1L).orElse(new User());
        //给View提供Model
        modelMap.addAttribute("username", user.getName());
        return "xss";
    }
    @PostMapping
    public String save(@RequestParam("username") String username, HttpServletRequest request) {
        User user = new User();
        user.setId(1L);
        user.setName(username);
        userRepository.save(user);
        //保存完成后重定向到首页
        return "redirect:/xss/";
    }
 }
//用户类,同时作为DTO和Entity
@Entity
@Data
public class User {
    @Id
    private Long id;
    private String name;
}

使用 Thymeleaf 模板引擎渲染页面。代码比较简单,页面加载的时候会在标签显示用户名,用户输入用户名提交后调用 save 接口创建用户


<div style="font-size: 14px">
    <form id="myForm" method="post" th:action="@{/xss/}">
        <label th:utext="${username}"/>
        <input id="username" name="username" size="100" type="text"/>
        <button th:text="Register" type="submit"/>
    </form>
</div>

打开xss页面后,在文本框中输入 <script>alert('test')</script> 点击 Register 按钮提交,页面会弹出 alert 对话框,并且脚本呗保存到了数据库



大家可能想到了,解决方式就是 HTML 转码。既然是通过 @RequestParam 来获取请求参数,那就定义一个 @InitBinder 实现数据绑定的时候,对字符串进行转码即可:


@ControllerAdvice
public class SecurityAdvice {
    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        //注册自定义的绑定器
        binder.registerCustomEditor(String.class, new PropertyEditorSupport() {
            @Override
            public String getAsText() {
                Object value = getValue();
                return value != null ? value.toString() : "";
            }
            @Override
            public void setAsText(String text) {
                //赋值时进行HTML转义
                setValue(text == null ? null : HtmlUtils.htmlEscape(text));
            }
        });
    }
}


针对这个场景,此做法确实是可行的,脚本没有被执行,也被转码后保存到了数据库中



但是这种方式并没有从根源解决问题,@InitBinder 是 Spring Web 层面的处理逻辑,如果有代码不通过 @RequestParam 来获取数据,而是直接从 HTTP 请求获取数据的话,这种方式就不会奏效。比如使用request.getParameter()。


更合理的解决方法是顶一个filter,实现servlet层面的统一参数替换。


@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class XssFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(new XssRequestWrapper((HttpServletRequest) request), response);
    }
}
public class XssRequestWrapper extends HttpServletRequestWrapper {
    public XssRequestWrapper(HttpServletRequest request) {
        super(request);
    }
    @Override
    public String[] getParameterValues(String parameter) {
        //获取多个参数值的时候对所有参数值应用clean方法逐一清洁
        return Arrays.stream(super.getParameterValues(parameter)).map(this::clean).toArray(String[]::new);
    }
    @Override
    public String getHeader(String name) {
        //同样清洁请求头
        return clean(super.getHeader(name));
    }
    @Override
    public String getParameter(String parameter) {
        //获取参数单一值也要处理
        return clean(super.getParameter(parameter));
    }
    //clean方法就是对值进行HTML转义
    private String clean(String value) {
      return StringUtils.isEmpty(value)? "" : HtmlUtils.htmlEscape(value);
    }
}


不过,这种方式还是不够彻底,原因是无法处理通过 @RequestBody 注解提交的 JSON 数据。处理JSON数据,需要自定义一个 Jackson 反列化器,来实现反序列化时的字符串的 HTML 转义


@Bean
public Module xssModule() {
    SimpleModule module = new SimpleModule();
    module.module.addDeserializer(String.class, new XssJsonDeserializer());
    return module;
}
public class XssJsonDeserializer extends JsonDeserializer<String> {
    @Override
    public String deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        String value = jsonParser.getValueAsString();
        if (value != null) {
            //对于值进行HTML转义
            return HtmlUtils.htmlEscape(value);
        }
        return value;
    }
    @Override
    public Class<String> handledType() {
        return String.class;
    }
}


这样就实现了既能转义 Get/Post 通过请求参数提交的数据,又能转义请求体中直接提交的 JSON 数据。


你可能觉得做到这里,我们的防范已经很全面了,但其实不是。这种只能堵新漏,确保新数据进入数据库之前转义。如果因为之前的漏洞,数据库中已经保存了一些 JavaScript 代码,那么读取的时候同样可能出问题。因此,我们还要实现数据读取的时候也转义。


之前我们处理了 JSON 反序列化问题,那么就需要同样处理序列化,实现数据从数据库中读取的时候转义,否则读出来的 JSON 可能包含 JavaScript 代码。


修改之前的 SimpleModule 加入自定义序列化器,并且实现序列化时处理字符串转义


@Bean
public Module xssModule() {
    SimpleModule module = new SimpleModule();
    module.addDeserializer(String.class, new XssJsonDeserializer());
    module.addSerializer(String.class, new XssJsonSerializer());
    return module;
}
public class XssJsonSerializer extends JsonSerializer<String> {
    @Override
    public Class<String> handledType() {
        return String.class;
    }
    @Override
    public void serialize(String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        if (value != null) {
            //对字符串进行HTML转义
            jsonGenerator.writeString(HtmlUtils.htmlEscape(value));
        }
    }
}


经过修改后,即使数据库中已经保存了 JavaScript 代码,呈现的时候也只能作为 HTML 显示了。现在,对于进和出两个方向,都实现了补漏。


相关文章
|
17天前
|
设计模式 Java
Java设计模式:组合模式的介绍及代码演示
组合模式是一种结构型设计模式,用于将多个对象组织成树形结构,并统一处理所有对象。例如,统计公司总人数时,可先统计各部门人数再求和。该模式包括一个通用接口、表示节点的类及其实现类。通过树形结构和节点的通用方法,组合模式使程序更易扩展和维护。
Java设计模式:组合模式的介绍及代码演示
|
8天前
|
安全 Java API
java安全特性
java安全特性
21 8
|
6天前
|
Java
java小工具util系列4:基础工具代码(Msg、PageResult、Response、常量、枚举)
java小工具util系列4:基础工具代码(Msg、PageResult、Response、常量、枚举)
20 5
|
8天前
|
Java API 开发者
探索Java中的Lambda表达式:简洁与强大的代码实践
本文深入探讨Java中Lambda表达式的定义、用法及优势,通过实例展示其如何简化代码、提升可读性,并强调在使用中需注意的兼容性和效率问题。Lambda作为Java 8的亮点功能,不仅优化了集合操作,还促进了函数式编程范式的应用,为开发者提供了更灵活的编码方式。
|
4天前
|
Java 开发者
探索Java中的Lambda表达式:简化你的代码之旅##
【8月更文挑战第62天】 Java 8的发布为开发者带来了诸多新特性,其中最引人注目的无疑是Lambda表达式。这一特性不仅让代码变得更加简洁,还极大地提升了开发的效率。本文将通过实际示例,展示如何利用Lambda表达式来优化我们的代码结构,同时探讨其背后的工作原理和性能考量。 ##
|
7天前
|
Java API 开发者
探索Java中的Lambda表达式:简化代码,提升效率
【9月更文挑战第27天】在Java 8中引入的Lambda表达式为编程带来了革命性的变化。通过简洁的语法和强大的功能,它不仅简化了代码编写过程,还显著提升了程序的执行效率。本文将深入探讨Lambda表达式的本质、用法和优势,并结合实例演示其在实际开发中的应用。无论你是Java新手还是资深开发者,都能从中获得启发,优化你的代码设计。
|
8天前
|
Java Linux Python
Linux环境下 代码java调用python出错
Linux环境下 代码java调用python出错
24 3
|
7天前
|
安全 Java 开发者
Java修饰符与封装:理解访问权限、行为控制与数据隐藏的重要性
Java中的修饰符和封装概念是构建健壯、易维护和扩展的Java应用程序的基石。通过合理利用访问权限修饰符和非访问修饰符,开发者能够设计出更加安全、灵活且高效的代码结构。封装不仅是面向对象编程的核心原则之一,也是提高软件项目质量和可维护性的关键策略。
10 1
|
8天前
|
存储 Java 索引
使用java代码实现左右括号查找
使用java代码实现左右括号查找
|
9天前
|
算法 Java
java 概率抽奖代码实现
java 概率抽奖代码实现
下一篇
无影云桌面