《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

相关文章
|
1月前
|
存储 Java
Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。
【10月更文挑战第19天】本文详细介绍了Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。HashMap以其高效的插入、查找和删除操作著称,而TreeMap则擅长于保持元素的自然排序或自定义排序,两者各具优势,适用于不同的开发场景。
44 1
|
1月前
|
存储 Java
告别混乱!用Java Map优雅管理你的数据结构
【10月更文挑战第17天】在软件开发中,随着项目复杂度增加,数据结构的组织和管理至关重要。Java中的Map接口提供了一种优雅的解决方案,帮助我们高效、清晰地管理数据。本文通过在线购物平台的案例,展示了Map在商品管理、用户管理和订单管理中的具体应用,有效提升了代码质量和维护性。
82 2
|
26天前
|
存储 安全 Java
Java零基础-字符串详解
【10月更文挑战第18天】Java零基础教学篇,手把手实践教学!
99 60
|
16天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
36 6
|
16天前
|
SQL 安全 Java
安全问题已经成为软件开发中不可忽视的重要议题。对于使用Java语言开发的应用程序来说,安全性更是至关重要
在当今网络环境下,Java应用的安全性至关重要。本文深入探讨了Java安全编程的最佳实践,包括代码审查、输入验证、输出编码、访问控制和加密技术等,帮助开发者构建安全可靠的应用。通过掌握相关技术和工具,开发者可以有效防范安全威胁,确保应用的安全性。
34 4
|
22天前
|
存储 Java 索引
Java中的数据结构:ArrayList和LinkedList的比较
【10月更文挑战第28天】在Java编程世界中,数据结构是构建复杂程序的基石。本文将深入探讨两种常用的数据结构:ArrayList和LinkedList,通过直观的比喻和实例分析,揭示它们各自的优势与局限,帮助你在面对不同的编程挑战时做出明智的选择。
|
30天前
|
存储 算法 Java
Java 中常用的数据结构
【10月更文挑战第20天】这些数据结构在 Java 编程中都有着广泛的应用,掌握它们的特点和用法对于提高编程能力和解决实际问题非常重要。
30 6
|
1月前
|
存储 Java 开发者
Java中的Map接口提供了一种优雅的方式来管理数据结构,使代码更加清晰、高效
【10月更文挑战第19天】在软件开发中,随着项目复杂度的增加,数据结构的组织和管理变得至关重要。Java中的Map接口提供了一种优雅的方式来管理数据结构,使代码更加清晰、高效。本文通过在线购物平台的案例,展示了Map在商品管理、用户管理和订单管理中的具体应用,帮助开发者告别混乱,提升代码质量。
29 1
|
8天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
7天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
下一篇
无影云桌面