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


    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 语法错误,所以一些开发人员会退而求其次,选择“${}”的方式,比如

    1. @Select("SELECT id,name FROM `userdata` WHERE name LIKE '%${name}%'")
    2. List selectByName(@Param("name") String name);

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

    1. @Select("SELECT id,name FROM `userdata` WHERE name LIKE CONCAT('%',#{name},'%')")
    2. List selectByNameRight(@Param("name") String name);

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

    二、XSS必须严防丝堵

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

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

    1. @Controller
    2. @RequestMapping("xss")
    3. public class XssController {
    4. @Resource
    5. private UserRepository userRepository;
    6. @GetMapping
    7. public String index(ModelMap modelMap) {
    8. //查数据库
    9. User user = userRepository.findById(1L).orElse(new User());
    10. //给View提供Model
    11. modelMap.addAttribute("username", user.getName());
    12. return "xss";
    13. }
    14. @PostMapping
    15. public String save(@RequestParam("username") String username, HttpServletRequest request) {
    16. User user = new User();
    17. user.setId(1L);
    18. user.setName(username);
    19. userRepository.save(user);
    20. //保存完成后重定向到首页
    21. return "redirect:/xss/";
    22. }
    23. }
    24. //用户类,同时作为DTO和Entity
    25. @Entity
    26. @Data
    27. public class User {
    28. @Id
    29. private Long id;
    30. private String name;
    31. }

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

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

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

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

    1. @ControllerAdvice
    2. public class SecurityAdvice {
    3. @InitBinder
    4. protected void initBinder(WebDataBinder binder) {
    5. //注册自定义的绑定器
    6. binder.registerCustomEditor(String.class, new PropertyEditorSupport() {
    7. @Override
    8. public String getAsText() {
    9. Object value = getValue();
    10. return value != null ? value.toString() : "";
    11. }
    12. @Override
    13. public void setAsText(String text) {
    14. //赋值时进行HTML转义
    15. setValue(text == null ? null : HtmlUtils.htmlEscape(text));
    16. }
    17. });
    18. }
    19. }

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

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

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

    1. @Component
    2. @Order(Ordered.HIGHEST_PRECEDENCE)
    3. public class XssFilter implements Filter {
    4. @Override
    5. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    6. chain.doFilter(new XssRequestWrapper((HttpServletRequest) request), response);
    7. }
    8. }
    9. public class XssRequestWrapper extends HttpServletRequestWrapper {
    10. public XssRequestWrapper(HttpServletRequest request) {
    11. super(request);
    12. }
    13. @Override
    14. public String[] getParameterValues(String parameter) {
    15. //获取多个参数值的时候对所有参数值应用clean方法逐一清洁
    16. return Arrays.stream(super.getParameterValues(parameter)).map(this::clean).toArray(String[]::new);
    17. }
    18. @Override
    19. public String getHeader(String name) {
    20. //同样清洁请求头
    21. return clean(super.getHeader(name));
    22. }
    23. @Override
    24. public String getParameter(String parameter) {
    25. //获取参数单一值也要处理
    26. return clean(super.getParameter(parameter));
    27. }
    28. //clean方法就是对值进行HTML转义
    29. private String clean(String value) {
    30. return StringUtils.isEmpty(value)? "" : HtmlUtils.htmlEscape(value);
    31. }
    32. }

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

    1. @Bean
    2. public Module xssModule() {
    3. SimpleModule module = new SimpleModule();
    4. module.module.addDeserializer(String.class, new XssJsonDeserializer());
    5. return module;
    6. }
    7. public class XssJsonDeserializer extends JsonDeserializer {
    8. @Override
    9. public String deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JsonProcessingException {
    10. String value = jsonParser.getValueAsString();
    11. if (value != null) {
    12. //对于值进行HTML转义
    13. return HtmlUtils.htmlEscape(value);
    14. }
    15. return value;
    16. }
    17. @Override
    18. public Class handledType() {
    19. return String.class;
    20. }
    21. }

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

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

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

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

    1. @Bean
    2. public Module xssModule() {
    3. SimpleModule module = new SimpleModule();
    4. module.addDeserializer(String.class, new XssJsonDeserializer());
    5. module.addSerializer(String.class, new XssJsonSerializer());
    6. return module;
    7. }
    8. public class XssJsonSerializer extends JsonSerializer {
    9. @Override
    10. public Class handledType() {
    11. return String.class;
    12. }
    13. @Override
    14. public void serialize(String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
    15. if (value != null) {
    16. //对字符串进行HTML转义
    17. jsonGenerator.writeString(HtmlUtils.htmlEscape(value));
    18. }
    19. }
    20. }

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

  • 相关阅读:
    还摆个屁的烂?用Python画如此漂亮的专业插图 ?简直So easy!
    YOLO 系列论文精读 & YOLOv4
    【亲妈教学】配置Gzip压缩,含前后端步骤
    y106.第六章 微服务、服务网格及Envoy实战 -- 可观测应用之指标和日志(十七)
    [如何编译openGauss对应版本的wal2json.so]
    索引——MySQL
    【BurpSuite】插件开发学习之J2EEScan - 汇总篇(主动+被动1-76)
    【Koltin Flow(三)】Flow操作符之中间操作符(三)
    Unity 制作登录功能02-创建和链接数据库(SQlite)
    Spring application.properties
  • 原文地址:https://blog.csdn.net/qq_41890624/article/details/126723367