《Java安全编码标准》一2.11 IDS10-J不要拆分两种数据结构中的字符串

简介: 本节书摘来自华章出版社《Java安全编码标准》一书中的第2章,第2.11节,作者 (美)Fred Long,Dhruv Mohindra,Robert C. Seacord,Dean F. Sutherland,David Svoboda,更多章节内容可以访问云栖社区“华章计算机”公众号查看

2.11 IDS10-J不要拆分两种数据结构中的字符串

在历史遗留系统中,常常假设字符串中的每一个字符使用8位(一个字节,Java中的byte)。而Java语言使用16位表示一个字符(Java中的Char类型)。遗憾的是,不管是Java的byte类型还是char类型数据,都不能表示所有的Unicode字符。许多字符串使用例如UTF-8编码的方式存储和通信,而在这种编码中,字符长度是可变的。
当Java字符串以字符数组的方式存储时,它可以用一个字节数组来表示,字符串里的一个字符可以用两个连续的或更多的byte类型或者char类型表示。如果拆分一个char类型或byte类型的数组,将会对多字节的字符产生风险。
如果忽略那些补充字符(supplementary character)多字节字符或者整合字符(修改其他字符的那些字符),攻击者可能绕过输入验证。因此,不应该拆分两种数据结构中的字符。

2.11.1 多字节字符

在一些字符集中会使用多字节字符编码,这些字符集要求用一个以上的字节来唯一标识每一个字符。比如,在日文Shift-JIS编码中,就支持多字节编码,其最大的字符长度为2个字节(一个起始字节,一个结尾字节)。
字节类型 范围
image

对结尾字节而言,它可能覆盖单个字节或者多字节字符的起始字节。当一个多字节字符被分拆时,特别是跨不同的缓冲区边界分拆时,它会产生不同的解释,如果没有按照正常的缓冲区边界进行分拆的话。这种差异一般由构造字符时使用的字节有二义性造成。

2.11.2 补充字符

根据Java API[API 2006]对Character类的描述(Unicode的字符表示):
Char数据类型(和Character对象封装的值)需要依赖于初始的Unicode编码定义,其中将其定义为长度为16位的字符编码。Unicode编码后来经过改动,允许使用多于16位来表示字符编码。合法的字符编码位于u0000?~u10FFFF,这就是我们所熟悉的Unicode字符编码值。
Java 2平台在char数组、String和StringBuffer?类中使用UTF-16编码表示。在这种字符表示中,补充字符会用一对char值来表示,第一个高位字符范围是uD800~uDBFF,第二个低位字符范围是uDC00~uDFFF。
一个int值可以表示所有的Unicode编码字符,包括那些补充码。int类型中的最低21位是用来表示Unicode编码的,其他的11个高位必须为0。除非特指,关于补充码和字符值有下面的规则:
那些只能接受char值的方法是不支持补充码的。替代范围内的char值会被认为是未定义的字符。比如,Character.isLetter('uD840')会返回false,即使这种特殊字符紧跟着任何表示字母的字符串的低位替代值。
接受int值的方法支持所有的Unicode字符,包括字符。比如,Character.isLetter(0x2F81A)会返回true,因为这个码点值代表一个字母(在CJK编码中)。

2.11.3 不符合规则的代码示例(读取)

这个不符合规则的代码示例会从一个套接字中读取1024个字节,并且使用这些数据创建一个字符串。它同时使用一个while循环来读取这些字节,就像在FIO10-J中推荐的那样。当检测到套接字中的数据多于1024个字节时,它就会抛出异常。这样的机制能够防止非受信的输入耗尽程序的内存。

public final int MAX_SIZE = 1024;

public String readBytes(Socket socket) throws IOException {
??InputStream in = socket.getInputStream();
??byte[] data = new byte[MAX_SIZE+1];
??int offset = 0;
??int bytesRead = 0;
??String str = new String();
??while ((bytesRead = in.read(data, offset, data.length - offset))
??????????!= -1) {
????offset += bytesRead;
????str += new String(data, offset, data.length - offset, "UTF-8");
????if (offset >= data.length) {
??????throw new IOException("Too much input");
????}
??}
??in.close();
??return str;
}

这个代码示例没有考虑到使用多字节编码的字符和循环选代边界之间的关系。如果在最后一个通过read()方法读取的数据流中存在一个对字节编码的首字节,那么其他的字节只能在循环的下一次处理。然而,可以通过在一个循环中创建一个新的字符串来解决多字节编码问题。这样的话,多字节编码就可能会被错误地解释。

2.11.4 符合规则的方案(读取)

该符合规则的方案将字符串的创建推迟到接收完所有的数据时才完成。

public final int MAX_SIZE = 1024;

public String readBytes(Socket socket) throws IOException {
??InputStream in = socket.getInputStream();
??byte[] data = new byte[MAX_SIZE+1];
??int offset = 0;
??int bytesRead = 0;
??while ((bytesRead = in.read(data, offset, data.length - offset))
??????????!= -1) {
????offset += bytesRead;
????if (offset >= data.length) {
??????throw new IOException("Too much input");
????}
??}
??String str = new String(data, "UTF-8");
??in.close();
??return str;
}

这段代码避免了将跨不同缓冲区的多字节编码字符分隔的问题,使用的方法是,直到读取完所有的数据,才开始创建字符串。

2.11.5 符合规则的方案(Reader)

这个符合规则的方案使用的是Reader而不是InputStream。这个Reader类会快速地将字节数据转换为字符数据,所以能够避免分隔多字节字符的问题。当套接字使用多于1024个字符而不是恰好使用1024字节时,这个例程会自动退出。

public final int MAX_SIZE = 1024;

public String readBytes(Socket socket) throws IOException {
??InputStream in = socket.getInputStream();
??Reader r = new InputStreamReader(in, "UTF-8");
??char[] data = new char[MAX_SIZE+1];
??int offset = 0;
??int charsRead = 0;
??String str = new String(data);
??while ((charsRead = r.read(data, offset, data.length - offset))
?????????!= -1) {
????offset += charsRead;
????str += new String(data, offset, data.length - offset);
????if (offset >= data.length) {
??????throw new IOException("Too much input");
????}
??}
??in.close();
??return str;
}

2.11.6 不符合规则的代码示例(子字符串)

这个不符合规则的代码示例想截取字符串中的首字母。它的做法不对,因为使用了Character.isLetter()方法,这个方法不能处理补充字符和合并字符[Hornig 2007]。

// Fails for supplementary or combining characters
public static String trim_bad1(String string) {
??char ch;
??int i;
??for (i = 0; i < string.length(); i += 1) {
????ch = string.charAt(i);
????if (!Character.isLetter(ch)) {
??????break;
????}
??}
??return string.substring(i);
}

2.11.7 不符合规则的代码示例(子字符串)

这个不符合规则的代码示例想要纠正使用?String.codePointAt()?的错误,这个方法使用了int类型作为输入参数。这对补充字符来说是正确的,但对于合并字符而言却是错误的[Hornig 2007]。

// Fails for combining characters
public static String trim_bad2(String string) {
??int ch;
??int i;
??for (i = 0; i < string.length(); i += Character.charCount(ch)) {
????ch = string.codePointAt(i);
????if (!Character.isLetter(ch)) {
??????break;
????}
??}
??return string.substring(i);
}

2.11.8 符合规则的方案(子字符串)

这个方案可以处理补充字符和合并字符[Hornig 2007]。根据Java API[API 2006]文档对java.text.BreakIterator的说明:
BreakIterator实现了能够在文本边界内定位的方法。BreakIterator?的对象实例维护了当前的位置信息,并且会对文本进行扫描,当遇到文本边界的时候,它会返回字符所在的位置索引。
返回的边界可能是那些补充字符、合并字符。例如,一个音节字符可以被存储为一个基础字符加上一个用来区分的符号。

public static String trim_good(String string) {
??BreakIterator iter = BreakIterator.getCharacterInstance();
??iter.setText(string);
??int i;
??for (i = iter.first(); i != BreakIterator.DONE; i = iter.next()) {
????int ch = string.codePointAt(i);
????if (!Character.isLetter(ch)) {
??????break;
????}????
??}
??// Reached first or last text boundary
??if (i == BreakIterator.DONE) {?
????// The input was either blank or had only (leading) letters
????return "";?
??} else {
????return string.substring(i);
??}
}

如果要对locale敏感的字符串比较、搜索和排序,可以使用?java.text.Collator?类。

2.11.9 风险评估

如果没有考虑到补充字符和合并字符的话,将会导致不可预计的行为。
image

2.11.10 参考书目

image

相关文章
|
17天前
|
人工智能 监控 安全
Java智慧工地(源码):数字化管理提升施工安全与质量
随着科技的发展,智慧工地已成为建筑行业转型升级的重要手段。依托智能感知设备和云物互联技术,智慧工地为工程管理带来了革命性的变革,实现了项目管理的简单化、远程化和智能化。
35 4
|
3月前
|
SQL Java 索引
java小工具util系列2:字符串工具
java小工具util系列2:字符串工具
173 83
|
3月前
|
Java 数据库
java小工具util系列1:日期和字符串转换工具
java小工具util系列1:日期和字符串转换工具
80 26
|
2月前
|
存储 缓存 安全
Java 集合江湖:底层数据结构的大揭秘!
小米是一位热爱技术分享的程序员,本文详细解析了Java面试中常见的List、Set、Map的区别。不仅介绍了它们的基本特性和实现类,还深入探讨了各自的使用场景和面试技巧,帮助读者更好地理解和应对相关问题。
64 5
|
3月前
|
存储 缓存 安全
java 中操作字符串都有哪些类,它们之间有什么区别
Java中操作字符串的类主要有String、StringBuilder和StringBuffer。String是不可变的,每次操作都会生成新对象;StringBuilder和StringBuffer都是可变的,但StringBuilder是非线程安全的,而StringBuffer是线程安全的,因此性能略低。
113 8
|
3月前
|
SQL 安全 Java
Java 异常处理:筑牢程序稳定性的 “安全网”
本文深入探讨Java异常处理,涵盖异常的基础分类、处理机制及最佳实践。从`Error`与`Exception`的区分,到`try-catch-finally`和`throws`的运用,再到自定义异常的设计,全面解析如何有效管理程序中的异常情况,提升代码的健壮性和可维护性。通过实例代码,帮助开发者掌握异常处理技巧,确保程序稳定运行。
77 2
|
3月前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
73 6
|
3月前
|
SQL 安全 Java
安全问题已经成为软件开发中不可忽视的重要议题。对于使用Java语言开发的应用程序来说,安全性更是至关重要
在当今网络环境下,Java应用的安全性至关重要。本文深入探讨了Java安全编程的最佳实践,包括代码审查、输入验证、输出编码、访问控制和加密技术等,帮助开发者构建安全可靠的应用。通过掌握相关技术和工具,开发者可以有效防范安全威胁,确保应用的安全性。
76 4
|
3月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
362 9
|
3月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
58 1