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


@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 显示了。现在,对于进和出两个方向,都实现了补漏。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿演

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值