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个字节(一个起始字节,一个结尾字节)。
字节类型 范围

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

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 风险评估

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

2.11.10 参考书目

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

  1. 《Java安全编码标准》一导读

    前 言 在Java编程语言中,关键的安全编码要素是采用良好的文档和强制的编码规范.本书提供了在Java语言中的一系列安全编码规则.这些规则的目标是消除不安全的编码实践,因为这些不安全的因素会导致可利用 ...

  2. 《Java安全编码标准》迷你书

    <Java安全编码标准>迷你书 本书不仅从语言角度系统而详 细地阐述Java安全编码的要素.标准.规范和最佳实践,而且从架构设计的角度分析了Java API存在的设计缺陷和可能存 在的安全 ...

  3. java 获取星期几_java中获取日期是星期几的两种方法

    java中取得指定日期是星期几可以采用下面两种方式取得日期是星期几:(推荐:java视频教程) 1.使用Calendar类//根据日期取得星期几 public static String getWee ...

  4. JUC里面的相关分类|| java并发编程中,关于锁的实现方式有两种synchronized ,Lock || Lock——ReentrantLock||AQS(抽象队列同步器)

    JUC分类 java并发编程中,关于锁的实现方式有两种synchronized ,Lock AQS--AbstractQueuedSynchronizer

  5. java调用外联服务用xml,Spring IOC 依赖注入的两种方式:XML和注解

    IoC,直观地讲,就是容器控制程序之间的关系,而非传统实现中,由程序代码直接操控.这也就是所谓"控制反转"的概念所在.控制权由应用代码中转到了外部容器,控制权的转移是所谓反转.Io ...

  6. [Java中实现Excel表导入导出]基于easy-poi和EasyExcel两种方式实现

    第一种:基于easy-poi实现Excel导入导出 1.导出Excel表格 第一步:在pom文件中导入依赖 <!--基于easy-poi实现Excel导入导出--><dependen ...

  7. java 更改 常量池_JVM中三个常量池(两种常量池)的解析及其随jdk版本的变化

    目录 常量池 静态常量池 运行时常量池 字符串常量池 三个常量池的关系 其随jdk版本的变化 常量池 请注意常量池是线程共享数据区,常量池的内容: 常量池的好处: 常量池是为了避免频繁的创建和销毁对象 ...

  8. Java并发基础01. 传统线程技术中创建线程的两种方式

    传统的线程技术中有两种创建线程的方式:一是继承Thread类,并重写run()方法:二是实现Runnable接口,覆盖接口中的run()方法,并把Runnable接口的实现扔给Thread.这两种方式 ...

  9. java校验码的设计_Java动态验证码单线设计的两种方法

    1.java的动态验证码我这里将介绍两种方法: 一:根据java本身提供的一种验证码的写法,这种呢只限于大家了解就可以了,因为java自带的模式编写的在实际开发中是没有意义的,所以只供学习一下就可以了 ...

最新文章

  1. patch是什么意思啊_学 Vue 看这个就够了 - 什么是 Vue.js
  2. 你所知道的都是错的!产品经理的十大认知错误
  3. ubuntu php多版本共存切换
  4. win8下cocos2dx3.2移植android平台及代码打包APK
  5. 死磕 java同步系列之开篇
  6. Mr.J-- jQuery学习笔记(三十一)--事件操作方法(onoff)
  7. 【Python之旅】第二篇(七):集合
  8. Android-动画-view 动画笔记
  9. 常用plc编程软件阵营划分
  10. JAVA电商商城系统
  11. Mac下Appium环境搭建
  12. 银行卡四要素验证API接口用法简介
  13. 将阿里云盘挂载为本地磁盘使用
  14. Git - git 入门
  15. 计算机音乐怎么把音乐放u盘,怎么把音乐拷贝到u盘
  16. 多功能搜索友联自助交换多色彩皮肤网站图片本地化附带交易系统网址导航源码蜘蛛
  17. UGUI动画快速制作
  18. vector 通俗易懂描述
  19. vue 多层子组件调用父组件的方法(传参方式bind方法 或 注入 provide() {}方法)
  20. 跨境电商APP如何高效测品提升GMV?真金白银经验总结告诉你

热门文章

  1. 数据库51年来十八件大事年表
  2. 学无止境,我还在进步
  3. 文件系统,磁盘配额,数据存储,lvm 逻辑卷管理器
  4. 短视频抢了直播饭碗,花椒、映客们逆袭抖音、快手无门
  5. Vue的computed(计算属性)使用实例之TodoList
  6. Java 集合 Collection、Iterator
  7. 魔术方法 :__callStatic( )实例详解
  8. 群发的我不回??!!
  9. android Viewpager HorizontalScrollView 实现分页栏拖拽
  10. 最老程序员创业札记:全文检索、数据挖掘、推荐引擎应用36