Java一些安全编码标准(第三版)

一、不要使用公有静态的非final变量

安全管理器不会对读取或写人这些变量进行检查。此外,在将新值存储到这些字段之前,是不能通过编程方式进行验证的。在多线程的场合中,非m的公有静态字段会被不一致的方式修改。非受信代码可以通过恶意方法来提供一个非期望的子类型。因此,类必须不能包含非 fnal的公有静态字段。

(一)、不符合的示例代码
这个不符合规则的代码示例在一个用来进行序列化的类中,使用了一个公有静态的非 final的 serialVersionUID 的字段。

class DataSerializer implements Serializable {
	public static long serialVersionUID-1973473122623778747L;
}

(二)、符合的示例代码
这个符合规则的方案声明 serialVersionUID字段为 final和私有的

class DataSerializer implements Serializable {
	public static final long serialVersionUID-1973473122623778747L;
}

二、定义了equlas)方法的类必须定义 hashCode()方法

重写了 Objcct.cquals()方法的类必须同时重写 Objcct.hashCode() 方法。java.lang.Object类要求当两个对象用 equals0) 方法进行比较并得到等同的结果时,对这两个对象调用 hashCode0 方法时必须产生一样的整数结果。
equals0)方法用于判定两个对象实例逻辑上的等同性。因此,对于所有等同的对象,hashCode() 方法必须返回相同的数值。没能遵守这一合约是一个常见的产生错误的原因
(一)、不符合的示例代码
这个不符合规则的代码示例用 HashMap 将信用卡号和字符串联系在一起,随后尝试取得一个信用卡的卡号字符串。预期的数值是 4111111111111111,但实际获得的数值是 null

Public final class CreditCard {
	private final int number;
	public CreditCard(int number) {
		this.number = (short) number;
	}
	public boolean equals(Object o) {
		if (o == this) {
			return true;
		}
		if (!(o.instanceof CreditCard)) {
			return false:
		}
		CreditCard cc = (CreditCard)o;
		return cc.number == number;
	}
	public static void main(String[] args) {
		Map<CreditCard,String> m = new HashMap<CreditCard,String>();
		m.put(new CreditCard(100),"4111111111111111");
		System.out.println(m.get(new CreditCard(100)));
	}
}

造成这一错误行为的原因是 CreditCrad 类覆写了equlas0方法,但没有覆写 hashCode()方法。因此,默认的 hashCode0 方法对不同的对象返回一个不同的数值,即便这些对象在逻辑上是等同的:这些不同的数值导致了查看不同的散列桶,这阻碍了 get() 方法找到预期的数值。要注意的是,为简单起见,这个代码示例在 main() 中列出信用卡号违反了规则。
(二)、符合的示例代码
这个符合规则的方案覆写了 hashCode0 方法,从而保证了由equals() 方法认为相等的两个实例能产生相同的数值。Bloch 详细论述了生成这样的散列函数的方式。

Public final class CreditCard {
	private final int number;
	public CreditCard(int number) {
		this.number = (short) number;
	}
	public boolean equals(Object o) {
		if (o == this) {
			return true;
		}
		if (!(o.instanceof CreditCard)) {
			return false:
		}
		CreditCard cc = (CreditCard)o;
		return cc.number == number;
	}
	public int hashCode() {
		int result = 17;
		reSult = 31 * result + number;
		return result;
	}
	public static void main(String[] args) {
		Map<CreditCard,String> m = new HashMap<CreditCard,String>();
		m.put(new CreditCard(100),"4111111111111111");
		System.out.println(m.get(new CreditCard(100)));
	}
}

三、实现 compareTo()方法时遵守常规合约

选择实现 Comparable 接口展示了一种责任,那就是对compareTo0)方法的实现维持了该方法的使用合约。库中的类,例如 TreeSet 和 TreeMap,接受 Comparable 对象,并使用对应的compareTo() 方法来整理对象。然而,一个实现了 compareTo() 方法的类可能会以一种意外的方式生成非期望的结果。
Java SE 6API[API2006] 规定了 compareTo() 方法的使用合约
1)必须确保对于所有的x和y,sgn(x.compareTo(y)) ==-sgn(y.compareTo(x))成立。(这意味着当 y.compareTo(x) 抛出异常时,xcompareTo(y) 也必须抛出异常。
2)必须确保关联是可传递的:(x.compareTo(y)> 0 && y.compareTo(z)> 0) 意味着 x.compareTo(z)>0
3)必须确保 x.compareTo(y) == 0 暗示对所有的z,存在 sgn(x .compareTo(z)) == sgn(y.
compareTo(z))。
4)强烈建议(x.compareTo(y) == 0) == x.equals(y),但在严格意义上这并不需要。一般来说任何实现了 Comparable 接口并违反了 这个条件的类应该明确地指出这一点。建议使用这样的说
明:“注意:此类有着和 equals 不一致的自身次序。”
在以上的叙述中,符号 sgn表达式代表数学中的 signum 函数,根据表达式的数值是负数0或正数,这个函数返回-1、0或1
compareTo0 方法的实现一定不能违反前三个条件中的任何一条。实现应该尽可能地遵守第四个条件。
(一)、不符合的示例代码
这个不符合规则的代码示例实现了众所周知的游戏“石头-剪刀- 布”,并使用 compareTo0来决定游戏的胜者。

class GameEntry implements Comparable {
	Public enum Roshambo {ROCK,PAPER,SCISSORS}
	private Roshambo value:
	public GameEntry(Roshambo value) {
		this.value = value;
	}
	public int compareTo(Obiect that) {
		if (!(that instanceof Roshambo)) {
			throw new ClassCastException():
		}
		GameEntryt = (GameEntry) that;
		return (value == t.value) ? 0 :(value ==Roshambo.ROCK && t.value == Roshambo.PAPER) ? -1:(value == Roshambo.PAPER && t.value == Roshambo.SCISSORS) ? -1: (value == Roshambo.SCISSORS && t.value == Roshambo.ROCK) ?-1:1;
	}
}

可是,因为石头赢剪刀,剪刀赢布,而石头却输给了布,所以这个游戏违反了要求中的传递性特点。
(二)、符合的示例代码
这个符合规则的方案没有使用 Comparable 接口却实现了同样的游戏。

class GameEntry{
	Public enum Roshambo {ROCK,PAPER,SCISSORS}
	private Roshambo value:
	public GameEntry(Roshambo value) {
		this.value = value;
	}
	public int beats(Obiect that) {
		if (!(that instanceof Roshambo)) {
			throw new ClassCastException():
		}
		GameEntryt = (GameEntry) that;
		return (value == t.value) ? 0 :(value ==Roshambo.ROCK && t.value == Roshambo.PAPER) ? -1:(value == Roshambo.PAPER && t.value == Roshambo.SCISSORS) ? -1: (value == Roshambo.SCISSORS && t.value == Roshambo.ROCK) ?-1:1;
	}
}

四、不能允许异常泄露敏感信息

在异常传递的过程中,如果不对敏感信息进行过滤,常会导致信息泄露,这将有助于攻击者实现对系统的攻击。一个攻击者可以通过精心设计的输入参数来得到应用程序的内部数据结构或机制。异常的文本和异常的类型都可以泄漏信息。例如,FileNotFoundException 信息透露了文件系统布局的信息,并且该异常的类型表明了要求的文件并不存在。
这条规则同时适用于服务器端和客户端应用。攻击者不单只可以从易受攻击的网络服务器收集敏感信息,也可以从使用这些网络服务器的用户网络浏览器那里得到敏感信息。 因此,程序员必须过滤能传递出可信任边界的异常信息和异常类型。下表列出了几种有问题的异常:

异常名称 信息泄露或者威胁的描述
Java.io.FileNotFoundExccption 内部的文件系统结构、用户名列表
Java.sql.SQLException 数据库结构、用户名列表
Java.net.BindException 当非受信客户可以选择服务器端口时,列举已开放的端口名
Java.util.ConcurrentModificationException 提供关于线程不安全代码的信息
Javax.naming.InsufficientResourcesException 不充分的服务器资源(会产生 DoS )
Java.util.MissingResourceException 资源列举
Java.util.jar.JarException 内部的文件系统结构
Java.security.acl.NotOwnerException 所有者列举
Java.lang.OutOfMemoryError DoS
Java.lang.StackOverflowError DoS

输出栈跟踪会意外地将进程结构和状态的信息泄漏给攻击者。当一个在终端运行的 Java 程序被未捕捉的异常终止时,异常的信息和栈跟踪会显示在终端上;栈跟踪可能会泄漏关于程序内部结构的敏感信息。因此,在命令行运行的程序禁止因为未捕捉异常而中断执行。
(一)、不符合规则的代码示例(异常信息和类型引起的泄漏 )
在这个不符合规则的代码示例中,程序需要读取一个用户提供的文件,但是文件系统的内容和布局是敏感的。这个程序接受一个文件名作为参数,却没能防止潜在的异常会将敏感信息展现给用户。

class ExceptionExample {
	public static void main(Stringil aras) throws FileNotFoundException{
		FileInputstream fis  = new FileInputStream(System.getenv("APPDATA") + args[0]);
	}
}

如果要求的文件不存在,FilelnputStream 构造函数会抛出 FileNotFoundException 异常,通过不断地传递虚构的路径给程序,攻击者可以重建内部文件系统结构。
(二)、不符合规则的代码示例(封装和重新抛出敏感异常 )
这个不符合规则的代码示例将异常记人日志,并在重新抛出前将其封装在一个更普通的异常中。

try{
	FileInputstream fis  = new FileInputStream(System.getenv("APPDATA") + args[0]);
}catch(FileNotFoundException e){
	throw new IOException("Unable to retrieve file",e);
}

即使用户不能访问被记录的异常,最初的异常还是包含了很多信息,这仍然有助于攻击者发现文件系统布局的敏感信息。
注意,这个例子违反了规则 FIO04-J,因为它不能在 finally 代码块中关闭输入流。为了省略起见,以下的代码同样省略了这个 fnally 代码块。
(三)、符合的示例代码(安全策略 )
这个符合规则的方案实现了一个安全策略,用户只能打开 c:homepath 中的文件,并且不允许用户发现这个目录之外的文件。当文件不能打开或不在正确的目录内时,这个方案返回一个简洁的错误信息。c:\omepath 目录外文件的任何信息都被屏蔽了。
这个方案还使用了 FilegetCanonicalFile0 方法对文件进行标准化,简化了之后的对路径名进行比较的过程《参见规则IDSO2-J)。

class ExceptionExample {
	public static void main(Stringil aras){
		File file  = null;
		try {
			file    = new File(System.getenv("APPDATA") + args[0]).getCanonicalFile();
			if (!file.getPath().startsWith("c:\\homepath")) {
				System.out.println("Invalid file");
				return;
			}
		}catch(IOException e){
			System.out.println("Invalid file");
			return;
		}
		try{
			FileInputstream fis  = new FileInputStream(file);
		}catch(FileNotFoundException e){
			System.out.println("Invalid file");
			return;
		}
	}
}

五、不要在finally 程序段非正常退出

不要在 finally 程序段中使用 return、break、continue 或 throw 语句。当程序进入带有 finally程序段的 try 程序段时,不管 try 程序段 (或是相应的 atch 程序段)是否完成执行,finally 程序段总是会执行的。造成 finally 程序段非正常终止的语句也会导致 try 程序段非正常终止,从而消除了从 try 或catch 程序段抛出的任何异常
(一)、不符合规则的代码示例
在这个不符合规则的代码示例中,在代码段中的return语句造成了finaly程序段非正常终止。

class TryFinally {
	private static boolean doLogic() {
		try {
			throw new IllegalStateException();
		}finally {
			System.out.println("logic done"];
			return true;
		}
	}
}

return 语句引起的 finally 程序段的非正常终止消除了 IllegalStateException 异常
(二)、符合规则的代码示例
这个符合规则的方案去除了 finally 程序段中的 return 语句。

class TryFinally {
	private static boolean doLogic() {
		try {
			throw new IllegalStateException();
		}finally {
			System.out.println("logic done"];
		}
		// Any return statements must go herer
		//applicable only when exception is thrown conditionally
	}
}

(三)、特例
程序流控制语句的目的地在 finally 程序段内时,是可以接受的。例如,下面的代码没有违反这条规则。这是因为 break 语句只是跳出 while 循环而不是finally 程序段。

class TryFinally {
	private static boolean doLogic() {
		try {
			throw new IllegalStateException();
		}finally {
			int c;
			try{
				while ((c = input.read()) != -1) {
					1f(c>128) {
						break;
					}
				}
			}catch (IOException x) {
				// forward to handler
			}
			System.out.printIn("logic done");
		}
		// Any return statements must go herer
		//applicable only when exception is thrown conditionally
	}
}

六、不要在finally 程序段中遗漏可检查异常

在 finally 程序段中调用的方法可能会抛出异常。没能捕提和处理这样的异常会造成整个 try程序段的非正常终止。这导致了在 try 程序段中抛出异常的丢失,从而阻碍了任何可能的恢复方法去处理这些向题。而且,由于异常改变了程序流程,因此可能不会执行 finally 程序段中在异常抛出后的任何表达式或语句。允许可检查异常逃离finally 程序段同时也违反了规则ERR04-J。
(一)、不符合规则的代码示例
这个不符合规则的代码示例在 finally 程序段中关闭 reader 对象。程序员错误地假设 finally程序段中的语句不会抛出异常,因此没能适当地处理可能发生的异常

public class Operation (
	public static void doOperation(string some_file){
		// ... code to check or set character encoding..
		try{
			BufferedReader reader = new BufferedReader(new FileReader(some file));
			try {
				// Do operations
			}finally{
				reader.close();
				// ... Other cleanup code.4
			} 
		}catch (IOException x)(
			// Forward to handler
		}
	}
}

close() 方法会抛出IOException。如果抛出这个异常,将不会执行随后的其他清理操作。编译器并不能发觉这个问题是因为任何 IOException 都会由外层的 catch 程序段来捕捉。而且,close0) 操作抛出的异常会掩盖 Do opcrations 程序段抛出的异常,这妨碍了可能的恢复处理。
(二)、符合规则的代码示例(在finally 程序段中处理异常 )
这个符合规则的方案将 close() 放在 finally 程序段中一个独立的 try-catch 程序段里。从而可以处理潜在的IOException 并防止它的传递。

public class Operation (
	public static void doOperation(string some_file){
		// ... code to check or set character encoding..
		try{
			BufferedReader reader = new BufferedReader(new FileReader(some file));
			try {
				// Do operations
			}finally{
				try{
					reader.close();
				}catch (IOException x)(
					// Forward to handler
				}
				// ... Other cleanup code.4
			} 
		}catch (IOException x)(
			// Forward to handler
		}
	}
}

(三)、特例
程序流控制语句的目的地在 finally 程序段内时,是可以接受的。例如,下面的代码没有违反这条规则。这是因为 break 语句只是跳出 while 循环而不是finally 程序段。

class TryFinally {
	private static boolean doLogic() {
		try {
			throw new IllegalStateException();
		}finally {
			int c;
			try{
				while ((c = input.read()) != -1) {
					1f(c>128) {
						break;
					}
				}
			}catch (IOException x) {
				// forward to handler
			}
			System.out.printIn("logic done");
		}
		// Any return statements must go herer
		//applicable only when exception is thrown conditionally
	}
}

七、不要使用 write()方法输出超过0~255的整数

在java.io.OutputStream 中定义的 write()方法,将一个类型为 int 的值作为参数,而这个参数必须在0~255之间。因为一个类型为 int 的数值会超出这个范围,如果不能对范用进行检查
对的话,会将该数值的高位部分截去。Write() 方法常见的合约量:它会将一个字节输出。这个被写人的字节组成了参数 b的最低的8位,并会传递给 write() 方法:而b的24 位的高位会被忽略。
(一)、不符合规则的代码示例
这个不符合规则的代码示例没有对用户输入的数值进行验证。任何不在0~ 255 范围的数值会被截断。比如说,write(303) 会在 ASCII 系统中打印出,因为会使用 303 的低 8 位数值而其高 24 位会被忽略。(303 % 256 = 47,即为/的ASCII码。那就是说,其结果是用输入除以256得到的余数。

class ConsoleWrite {
	public static void main(string[] args) {
		//Any input value > 255 will result in unexpected  output
		System.out.write(Integer.valueof(args[0]));
		System.out.flush():
	}
}

(二)、符合规则的代码示例(输入范围检查 )
这个符合规则的方案仅在输人整数在正确的范围内时输出对应的字符。如果输入在 int 型可表示的范围之外,Integer.valueOf()方法会抛出NumberFormatException 异常。如果输入可以表示为一个int 类型,但超出了 write()方法需要的范围,代码会抛出一个 ArithmeticException异常。

class FileWrite{
	public static void main(string[] args) throws NumberFormatException,IOException{
		// Perform range checkingint value
		int value = Integer.valueof(args[0]);
		if(value <O ||  value > 255){
			throw new ArithmeticException("Value is out of range");
		}
	}
}

(三)、符合规则的代码示例(writeInt() )
这个符合规则的方案使用了 DataOutputStream 类中的 writeInt() 方法,它可以输出 int 类型可表示的数值范围。

class FileWrite{
	public static void main(string[] args) throws NumberFormatException,IOException{
		DataOutputStream dos = new DataOutputStream(System.out);
		dos.writeInt(Integer.valueOf(args[0].tostring()));
		System.out.flush ();
	}
}

八、使用 read()方法保证填充一个数组

对 InputStream 和 Rcader 类及其子类的 read 方法的设计在考虑到对 byte 或字符数组进行填充的情况时,是复杂的。根据 Java API 对类 InputStream 的 read(byte[] b,int oft, int len)方法的描述,它提供以下的行为[API 2006]:
默认的这个方法的实现会堵塞,直到所要求读人的输人数据的数量达到 lcn 的要求,检测到文件末尾,或者异常被抛出的情况。鼓励子类提供对这个方法的更有效的实现。
然而,对于read(byte[] b)方法:
从输人流中读取一定数量的字节,并且将其存储到缓存数组 b 中。实际读取的字节数量,作为一个整数返回。这个读取的字节数最多,等于 b的长度。
当发现可获得的输人数据时,read( 方法就会返回结果。作为结果,如果可以获得的数值不
是填充数组时,这些方法会在数组完全填充前停止读取数据。忽略由 read() 方法返回的结果,违反了规则 EXPOO-J。甚至当考虑了返回值时,安全问题也会出现,因为对于默认的 read() 方法的行为来说,它不能保证会填充整个数组缓存。因此,当使用read0)来填充一个数组时,程序必须对 read0 的返回值进行检查,并且必须对仅仅进行了部分填充的情况进行处理。在这样的情况下,程序可以尝试填充数组剩下的部分,或者仅仅对已经填充的数组的那个部分进行操作,或者抛出一个异常。
这个规则仅对使用一个数组作为参数的 read0) 方法适用。为了读入一个单独的字节,可以使用不带参数并且返回值为 int 类型的 InputStream.read0 方法。为了读人一个单独的字符,可以使用不带参数并且将读取字符作为 int 类型返回的 Reader.read()方法。
(一)、不符合规则的代码示例
这个不符合规则代码示例希望在一个 InputStream 中读人 1024 个 UTF-8 编码的字节,并且将其作为一个字符串返回。

public static String readBytes(InputStream in) throws IOException{
	byte[] data = new byte[1024];
	if (in.read(data) == -1) {
		throw new EOFException();
	}
	return new String(data, "UTF-8");
}

这个程序员错误地理解了 read( 方法的使用方法,这样会导致不能完全读人期望的数据。因为数据小于 1024 字节是很有可能的,并且在输入流中还有更多的数据存在。
(二)、符合规则的方案(对 read() 方法的多次调用 )
这个符合规则的方案将所有想要的字节读入缓存区中,对读入的字节总数进行记录,并且调整剩下的字节的偏移量,因此可以保证需要的数据完全读入。它同样通过将结果字符串创建延迟至所有的数据完全读人之后方式,避免了将多字节编码的字符进行跨缓冲的拆分。

public static String readBytes(InputStream in) throws IOException (
	int offset = 0;
	int bytesRead = 0;
	byte[] data=new byte[1024];
	while ((bytesRead = in.read(data, offset,data.length - offset))!= -1){
		offset += bytesRead;
		if (offset > data.length) {
			break;
		}
	}
	String str = new String(data,"UTE-8");
	return str;
}

(三)、符合规则的代码示例(readFully())
对于无参数的 DatalnputStream 类和一个参数的readFully() 方法而言,它可以保证要么读人所有要求的数据,要么抛出异常。如果在读入所需数量的字节数之前检测到输入末尾,或者发生其他的IO 错误,这些方法会抛出 EOFException 异常

public static String readBytes(FileInputStream fis)throws IOException {
	byte[] data = new byte[1024];
	DataInputstream dis = new DataInputStream(fis);
	dis.readFully(data);
	String str = new String(data,"UTF-8"];
	return str;
}

你可能感兴趣的:(java)