JAVA安全编码标准

博客首页文章列表 松花皮蛋me 2019-05-07 13:47
文章首发于公众号 松花皮蛋的黑板报松花皮蛋的黑板报,作者就职于京东,在稳定性保障、敏捷开发、高级JAVA、微服务架构有深入的理解

以下内容摘取自《JAVA安全编码标准》,略做修改和补充解释,这是一个把书读薄和知识串通的过程

一、输入验证和数据净化

  • 1、净化穿越受信边界的非受信数据,比如使用PreparedStatement防止SQL注入漏洞
  • 2、验证前规范化字符串,比如使用Unicode编码防止XSS跨站脚本漏洞
  • 3、在验证之前标准化路径名,使用file.getCannonicalPath()特殊处理软连接、”.”、“..”、相对路径,避免目录遍历漏洞
  • 4、不要记录未经净化的用户输入,以免注入,从而让管理员误以为系统行为
  • 5、限制传递给ZipInputStream的文件大小,通过ZipEntry.getSize()在解压前判断,如果过大则抛出异常
  • 6、使用ASCII字符集的子集作为文件名和路径名,当包括特殊字符如控制字符、空格、分隔符、命令行解释器、脚本和解析器时,会引起不可预期的行为
  • 7、从格式字符串中排除用户输入,避免拒绝服务
  • 8、不要向Runtime.exec()方法传递非受信、未净化的数据
  • 9、净化传递给正则表达式的非受信数据
  • 10、如果没有指定适当的locale,不要使用locale相关方法处理与locale相关的数据,最常见的就是大小写转换toUpperCase,正确做法是”title”.toUpperCase(Locale.ENGLISH)
  • 11、不要拆分两种数据结构的字符串,因为有补充字符和合并字符的存在,需要避免多字节编码问题
  • 12、移除或者替代任何字符串时,必须进行验证,避免成为关键字
  • 13、确保在不同的字符编码中无损转换字符串数据,不推荐使用string.getBytes(charset),推荐使用charsetEncoder类
  • 14、在文件或者网络IO两端使用兼容的编码方式

二、声明和初始化

  • 1、防止类的循环初始化,因为声明为static final的一个字段为并不能保证它在被读之前已经完全初始化
  • 2、不要重用Java标准库的已经公共的标识、公共的工具类、接口或者包,重用名称和定义不良好的import会导致不可预期的行为
  • 3、将所有增强for语句的循环变量声明为final类型,比如Iterator迭代时,直接修改next时会抛异常,声明为final后会直接产生编译器错误
public class Cycle {
    private final int balance;
    private static final Cycle c = new Cycle();

    private static final int deposit = (int) (Math.random()*100);

    public Cycle() {
        balance = deposit-10;
    }

    public static void main(String[] args) {

        //-10
        System.out.println("balance is:"+c.balance);
    }
}

三、表达式

  • 1、不要忽略方法的返回值
  • 2、不要解引用空指针
  • 3、使用两个参数的Arrays.equals()方法来比较两个数组的内容
  • 4、不要用相等操作符来比较两个基础数据类型的值
  • 5、确保使用正常的类型来自动封装数值
  • 6、不要在一个表达式中对同一变量进行多次写入
  • 7、不要在断言assert中使用有副作用的表达式,因为当关闭断言功能后,表达式将不会执行

四、数值类型与运算

  • 1、检测和向上转型避免整数溢出
  • 2、不要对同一数据进行位运算和数学运算,避免对变量数据解析的混淆
  • 3、确保除法运算和模运算的除数不为0
  • 4、使用可容纳无符号数据合法取值范围的整数类型
  • 5、不要使用浮点数float和double进行精细计算
  • 6、使用stricftp修饰符确保跨平台浮点运算的一致性
  • 7、不要尝试与无序的非数值NaN进行比较,因为表达式NaN==NaN总是返回false
  • 8、检查浮点输入特殊的数值,比如Double.isNan(double d)、Double.isinfinite(double d)
  • 9、不要使用浮点变量作为循环计数器
  • 10、不要从浮点字元构造BigDecimal对象,避免精度损失
  • 11、不要比较或者审查以字符串表达的浮点数值,除非显式去除字符串额外尾随的0
  • 12、需要慎重处理向下转型,比如int类型转成byte类型,避免精度损失
  • 13、需要慎重处理向上转型,比如float类型转成double类型,避免精度损失

五、面向对象

  • 1、只有受信子类能对具有不变性的类和方法进行扩展
  • 2、声明数据成员为私有并提供可访问的封装器方法
  • 3、当改变基类时,保存子类之间的依赖,不能破坏子类所依赖的程序不可变性。比如说,假设HashMap一开始只提供get、set方法,突然有一天新增了个entrySet方法,子类通过entrySet绕过权限检查进行修改
  • 4、在新代码中,不要混用具有范型和非范性的原始数据类型。当一个参数化的数个类型要访问一个对象,而这个对象又不是参数化数据类型时,会产生堆污染,未经检查的警告在错误时排查较困难
  • 5、不可变类为可变实例(成员)提供复制功能,避免传递给非受信代码时修改原来的实例,特别需要注意的是ThreadLocal是浅拷贝,避免引用逸出
  • 6、对可变输入和可变的内部组件创建防御性复制。起因是著名的TOCTOU漏洞, 一个程序先通过 access 判断用户是否有权限访问一个文件,然后通过 open 打开该文件,攻击者可以在时间间隙中间改变这个文件。当元素为可变对象的索引时,需要进行深复制
  • 7、不允许敏感类复制其自身,也就是不应该实现Cloneable接口,也不应该提供复制构造方法
  • 8、不要在嵌套类中暴露外部类的私有字段
  • 9、不要使用公有静态的非final变量
  • 10、在构造函数中尽可能的不出现异常

六、方法

  • 1、不要使用断言验证方法参数,断言失败后并不会抛出一个适当真实的异常
  • 2、进行安全检测的方法必须声明为private或final
  • 3、对类、接口、方法和数据成员的可访问性进行限制,避免子类覆盖后访问权限过大
  • 4、确保构造函数不会调用可覆写的方法,避免子类发起基类的创建时却调用了子类的方法,得到一个未初始化的值
  • 5、不要在clone()中调用可覆写的方法
  • 6、定义了equals()方法的类必须定义hashCode()方法
  • 7、实现compareTo()方法时遵守常规合约,满足传递性等
  • 8、不要使用析构函数,因为它的执行是没有固定时间的,不能保证有效性,它的调用是无序的,另外在停止运行前,JVM可能不会去调用孤立对象的析构函数,尝试在析构函数中更新状态会失败也不会有警告

七、异常行为

  • 1、不要消除或勿略可检查的异常
  • 2、不能允许异常泄漏敏感信息
  • 3、记录日记时应避免异常
  • 4、在方法失败时恢复对象先前的状态
  • 5、不要在finally程序段非正常退出,比如使用return\break\continute\throw,非正常退出会导致try程序段非正常终止,从而消除从try\catch中抛出的任何异常
  • 6、不要在finally程序段遗漏可检查异常
  • 7、不要抛出未声明的可检查异常
  • 8、不要抛出RuntimeException、Exception、Throwable,尽量抛出明确异常
  • 9、不要捕捉NullPointerException或任何它的基类

八、可见性和原子性

  • 1、当需要读取共享基础数据类型变量时,需要保证其他可见性,勿必声明为volatile变量或者正确进行代码同步
  • 2、认为只包含不可变对象的引用的类是不可变的,这样的假设是错误的
  • 3、保证对于共享变量的组合操作(+=、++等)是原子性的
  • 4、即使每一个方法是相互独立并且是原子性的,也不要假设一组调用是原子性的,因为它可能仅仅是曾经满足条件而已
  • 5、保证串联在一起的方法调用(Builder模式)是原子性的
  • 6、保证在读写64位的数值时的原子性

九、锁

  • 1、通过私有final锁对象可以同步那些与非受信代码交互的类,因为它满足不可变原则,JVM使尽优化也不会出现线程安全问题
  • 2、不要基于那些可能被重用的对象进行同步,比如说Boolean、Integer类型的锁
  • 3、不要基于那些通过getClass()返回的类对象来实现同步
  • 4、不要基于高层并发对象的内置锁来实现同步,java.util.concurrent.locks包中Lock和Condition接口的实现类,比如重入锁ReetrantLock
  • 5、即使集合是可访问的,也不要基于集合视图使用同步,可以使用Collections.synchronizedMap(map)进行同步,不可以使用map.keySet()进行同步
  • 6、对那些可以被非受信代码修改的静态字段,需要同步进入
  • 7、不要使用一个实例锁(非静态的类成员)来保护共享静态数据
  • 8、使用相同的方式请求和释放锁来避免死锁
  • 9、在异常条件时,保证释放已经持有的锁
  • 10、不要执行那些持有锁时会阻塞的操作
  • 11、不要使用不正确形式的双重检查惯用法,需要保证延迟初始化必须在多线程中是同步的
  • 12、当类方法和类成员使用不同的内置锁时,需要明确锁保护的是哪个对象,比如下面这段代码是线程不安全的
public class ListHelper<E> {

public List<E> list = Collections.synchronizedList(new ArrayList<>());

public synchronized boolean putIfAbsent(E x) {
    boolean absent = !list.contains(x);
    if(absent) {
        list.add(x);
    }
    return absent;
}

十、线程API

  • 1、不要调用Thread.run(),因为run方法中的语句是由当前线程而不是由新创建的线程来执行的,正确的操作是Thread.start()
  • 2、不能调用ThreadGroup方法,它的API可能会导致竞态、内存泄漏以及不一致的对象状态
  • 3、通过(notify()、signal())所有等待中的线程而不是单一线程,因为不能保证哪一个线程会接到通知,除非所有线程的等候条件是一致的
  • 4、始终在循环体中调用wait()和await()方法,避免中间线程修改状态、恶意的通知、误送的通知、虚拟唤醒的漏洞
  • 5、确保可以终止受阻线程,比如readlIne()阻塞于网络IO时,在IO完成前它无法对变更的标记做出响应,需要避免拒绝服务漏洞
  • 6、不要使用Thread.stop()来终止线程,stop会造成线程停止操作并抛出ThreadDeath异常,会使对象处于不一致的状态

十一、线程池

  • 1、使用线程池处理流量突发情况以实现降低性能运行
  • 2、不要使用有限的线程池来执行相互依赖的任务,避免线程饥饿死锁
  • 3、确保提交至线程池的任务是可中断
  • 4、确保线程池中正在执行的任务不会失败而不给出任何提示,不仅会造成资源泄漏,还会对失败的诊断很困难,因为线程池中的线程是可回收的。可以覆写ThreadPoolExecutor回调的afterExecute()方法或者Future.get()
  • 5、程序必须确保线程池中的线程执行的每一个任务只能见到正确初始化的ThreadLocal对象实例

十二、与线程安全相关的其他规则

  • 1、不要使用非线程安全方法来覆写线程安全方法
  • 2、不要让this引用在创建对象时泄漏,常见途径有:
  • 2.1、从创建对象的构造函数中调用一个非私有的、可覆写的方法时,该方法返回thirs
  • 2.2 、从可变类的一个非私有的方法返回this
  • 2.3、将this作为参数传递给一个在创建对象的构造函数中调用的外部方法
  • 2.4、使用内隐类,内隐类维护指向外部对象的this引用的一个副本
  • 2.5、在创建对象的构造函数中将this赋给公有的静态变量,从而将其公开
  • 2.6、从构造函数中抛出一个异常
  • 2.7、传递内部对象状态至一个外部方法
  • 3、不在在初始化类时使用后台线程,避免初始化循环和死锁
  • 4、不要发布部分初始化的对象,因为JMM允许多个线程在对象初始化开始后和结束后观察到对象

十三、输入输出

  • 1、不要操作共享目录中的文件,因为强制文件锁FileLock有很多的限制
  • 2、使用合适的访问权限创建文件
  • 3、发现并处理与文件相关的错误,一般的文件操作方法通常使用返回值而不是抛出异常来指示其错误
  • 4、在终止前移除临时文件
  • 5、在不需要时关闭资源,推荐使用try-with-resource方案
  • 6、不要使用Buffer中的wrap()或duplicate()创建缓存,并将缓存暴露给非受信代码,对这些缓存区的修改会导致数值的修改
  • 7、不能在一个单独的InputStream上创建多个缓存区封装器,重定向InputStream会导致不可预期的错误,往征会抛出EOFException异常
  • 8、不要让外部进程阻塞输入和输出流
  • 9、对读取一个字符或者字节的方法,使用int类型的返回值,仅当读取到末尾时会返回-1,不要过早将返回的值转成byte或char类型
  • 10、不要使用write()方法输出超过0~255的整数,超过后数值的高位部分会被截去
  • 11、使用read()方法保证填充一个数组,如果没有达到len的要求,此方法会堵塞
  • 12、不要将原始的二进制数据作为字符数据读入,比如说不指定编码的情况下将BigInteger的字节数组转换成字符串时会损失信息
  • 13、为小端数据的读写提供方法,不要使用java.io.DataInputStream中readShort()、readByte()等和对应的写方法,它们仅针对大端字节序数据进行操作
  • 14、不要在受信边界外记录敏感信息
  • 15、在程序终止时执行正确的清理动作,避免在不确定的状态下继续执行,可利用addShutdownHook()

十四、序列化

  • 1、在类的演化过程中维护其序列化的兼容性,保证显示指定serialVersionUID或者通过serialPersistenFields使用自定义的序列化
  • 2、不要偏离序列化方法的正确签名,也就是readObject()、readObjectNoData()、writeObject()方法必须声明为私有,而readResolve()、writeReplace()方法不能声明为私有
  • 3、在将对象向信任边界之外发送时,需要签名并且封装敏感对象
  • 4、不要序列化未经加密的敏感数据
  • 5、不要允许序列化和反序列化绕过安全管理器
  • 6、不能序列化内部类实例,当内部类被序列化时,包含在外部类的字段也会被序列化
  • 7、在反序列化时,必须在readObject()方法中对私有的可变组件进行防御性复制
  • 8、不要对实现定义的不可变因素使用默认的序列化格式,反序列会创建一个新的类实例但是不会调用它的构造函数
  • 9、不要从readObject()方法中调用可以被覆写的方法,因为基类的反序列化发生在类反序列化前,所以在readObject()调用可覆写方法会读取到子类被完全创建之前的状态
  • 10、在序列化时避免出现内存和资源泄漏,需要注意的是ObjectOutputStream维持了一个引用表来对先前序列化的对象进行跟踪
  • 11、不要在(会进行序列化和反序列化的)POJO类上做业务逻辑

十五、平台安全性

  • 1、不要允许特权代码块越过受信边界泄漏敏感信息,比如从doPrivileged()代码块中返回指向敏感资源的引用
  • 2、不要在特权代码块中使用没有验证或者非受信的变量
  • 3、不要基于非受信源进行安全检查,任何非受信对象或者参数必须在检查之前做防御性深度复制
  • 4、使用安全管理器检查来操作敏感操作
  • 5、不要使用反射来增加类、方法、字段的可访问性
  • 6、不要依赖于默认的URLClassLoader和java.util.jar提供的自动化签名检查
  • 7、当编写一个自定义的类装载器时,在给源代码覆予任何权限前,必须调用基类的getPermissions()方法获知默认的系统规则

十六、其他

  • 1、在交换安全数据时,使用SSLSocket而不是Socket
  • 2、生成强随机数,推荐使用SecureRandom类来生成高质量的随机数也不是Random类
  • 3、不要硬编码敏感信息
  • 4、当一个遍历正在进行时,不要修改它对应的集合,正常的做法是封装到同步集合中,如Collections.synchronizedList(list)或者new CopyOnWriteArrayList()
  • 5、防止多次实例化单例对象,需要确保设置构造方法为私有、跨线程的可见性、类不能被序列化、类不能被克隆,如果它是被一个自定义的类装载器装载,要防止类被垃圾回收