路径操作(Path Manipulation)的漏洞,简言之就是在路径中包含有一些特殊字符(… 或者 / 等),导致可以访问到期望目录之外的文件。 比如路径地址是`/usr/local/myapp/../../../etc/passwd`,对应到访问到的文件就是`/etc/passwd`, 而这个文件是系统的文件,保存用户密码。 使用Coverity 扫描的信息如下: ``` Filesystem path, filename, or URI manipulation An attacker may access, modify, or corrupt files that contain sensitive information or are critical to the application. ``` 关于路径操作漏洞,可以参考: [软件弱点预防之 —— Filesystem path, filename, or URI manipulation - 操控文件系统路径、文件名或 URI](https://blog.csdn.net/oscar999/article/details/127293940) 本篇介绍在 Java应用中如何防御路径操作(Path Manipulation)的攻击。 ## 防御方法分析 在代码层面来看, 防御路径操作的方法就是对输入进行验证,根据对字符是否合法的角度来看, 可以分为两种: 1. 黑名单 : 将不安全的字符列入黑名单, 2. 白名单 : 将预期的字符加入白名单。 或者更严格一点,创建一份合法资源名的列表,并且规定用户只能选择其中的文件名。 黑名单的方式实现起来较为简单, 将一些不安全的字符进行转义或者过滤, 比如 `..` 上一级目录字符 和绝对路径目录, 但是这种方式的安全性可能不是很高, 可能会遗漏一些特殊字符。 白名单的安全性较高, 但是需要穷举所有的合法路径相对比较困难, 如果某个路径不包含在里面, 则可能会导致应用的功能不能正常工作。 本篇介绍使用白名单的方式进行防御, 也就是哪些路径是有效的路径。 ## 路径操作的漏洞扫描 路径操作的弱点很容易被Coverity等静态扫描工具扫描出来, 比如下面的代码就是存在路径操纵漏洞的: ``` @RequestMapping("/unsafe") public void unsafe(String filefullName, HttpServletRequest request) { new File(filefullName); } ``` 要规避静态扫描其实很容易,只需要对路径使用函数转换一下, 类似下面: ``` @RequestMapping("/scanSafe") public void scanSafe(String filefullName, HttpServletRequest request) { new File(cleanFilePath(filefullName)); } public static String cleanFilePath(String filePath) { if (filePath != null) { char[] originalChars = filePath.toCharArray(); char[] chars = new char[originalChars.length]; for (int i = 0; i < originalChars.length; i++) { chars[i] = originalChars[i]; } return new String(chars); } else { return null; } } ``` 上面的cleanFilePath 其实没有对路径做任何改动, 也就是说这个函数没有任何作用, 但是这样的改动却是可以骗过Coverity, Coverity会认为 `new File(cleanFilePath(filefullName));` 这个代码是安全的, 当其实这个代码还是存在风险的。 以调用的cleanFilePath() 函数的效果来看, 路径完全没变化。 ``` @Test public void cleanFilePath() { String fileFullName = "C:\\temp\\..\\Windows\\system.ini"; Assertions.assertTrue("C:\\temp\\..\\Windows\\system.ini".equals(cleanFilePath(fileFullName))); } ``` 但是网络上还是有很多煞有介事的代码, 对路径进行转化: 比如: #### 错误解法1 ``` public static String cleanString(String aString) { if (aString == null) return null; String cleanString = ""; for (int i = 0; i < aString.length(); ++i) { cleanString += cleanChar(aString.charAt(i)); } return cleanString; } private static char cleanChar(char aChar) { // 0 - 9 for (int i = 48; i < 58; ++i) { if (aChar == i) return (char) i; } // 'A' - 'Z' for (int i = 65; i < 91; ++i) { if (aChar == i) return (char) i; } // 'a' - 'z' for (int i = 97; i < 123; ++i) { if (aChar == i) return (char) i; } // other valid characters switch (aChar) { case '/': return '/'; case '.': return '.'; case '-': return '-'; case '_': return '_'; case ' ': return ' '; } return '%'; } ``` 上面的代码试图将路径中的不合法的字符替换为 %, 仅认为字母、数字以及`/ . -` 等字符是有效字符。 这个转化不仅没有解决路径操作的风险,而且路径转换也是错的,比如调用示例: ``` @Test public void cleanString() { String fileFullName = "C:\\temp\\..\\Windows\\system.ini"; fileFullName = cleanString(fileFullName); System.out.println(fileFullName); //转化后的结果 C%%temp%..%Windows%system.ini } ``` #### 错误解法2 还有的解法似乎考虑周全, 将中文字符也考虑进来了, 但是同样是漏洞没解决, 功能反而是错的。 ``` public static String normalizeFilePath(String filePath) { if (filePath != null) { char[] originalChars = filePath.toCharArray(); char[] chars = new char[originalChars.length]; for (int i = 0; i < originalChars.length; i++) { if (isValidPathChar(originalChars[i])) { chars[i] = originalChars[i]; } else { chars[i] = '-'; } } return new String(chars); } else { return null; } } public static boolean isValidPathChar(char c) { boolean isValid = false; String whiteListpathChars = "abcdefghigklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/_-:\\."; if (whiteListpathChars.indexOf(c) > 0) { isValid = true; } return isValid; } public static boolean isChineseChar(char c) { Character.UnicodeBlock ub = Character.UnicodeBlock.of(c); return ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B || ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION || ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS || ub == Character.UnicodeBlock.GENERAL_PUNCTUATION; } ``` ## 正确姿势 比较简易的正确方式是先对路径进行规范化处理, 再判断路径是否在合法的路径列表中。 所谓路径归一化就是将路径中的 `..` 和 `.` 进行处理, 比如 ``` Path path = Paths.get("D:\\temp\\subfold1\\..\\..\\subsubfolder1"); path = path.normalize(); Assert.assertEquals("D:\\subsubfolder1", path.toString()); ``` 基于Java 的路径类Path 对路径进行归一化, 之后在对归一化之后的路径进行白名单的判断, 完整的示例代码如下: ``` @RequestMapping("/safeRightWay") public void safeRightWay(String filefullName, HttpServletRequest request) { Path path = Paths.get(filefullName); path = path.normalize(); String filePath = path.getParent().toString(); if(isValidPath(filePath)) { File file = path.toFile(); } } public boolean isValidPath(String filePath) { List<String> list = new ArrayList<String>(); list.add("D:\\temp1"); list.add("D:\\temp2"); return list.contains(filePath); } ``` 如果感觉Path 使用繁琐的话,也可以导入 apache common io 进行归一化处理, 首先导入依赖包: ``` <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-io</artifactId> <version>1.3.2</version> </dependency> ``` 导入之后就可以使用 FilenameUtils.normalize(fileFullName);进行归一化处理了,类似: ``` @Test public void normalize() { String fileFullName = "C:\\temp\\..\\Windows\\system.ini"; fileFullName = FilenameUtils.normalize(fileFullName); System.out.println(fileFullName); // C:\Windows\system.ini } ``` ## 安全的解法 需要注意的是上面使用 Path只是对路径进行规范化处理,并没有进行路径操纵的防御,所以使用Coverity 进行扫码时, `Path path = Paths.get(filefullName);` 这一句还是会被扫描到存在风险。 ``` @RequestMapping("/safeRightWayButScan") public void safeRightWayButScan(String filefullName, HttpServletRequest request) { Path path = Paths.get(filefullName); path = path.normalize(); String filePath = path.getParent().toString(); if(isValidPath(filePath)) { path.toFile(); } } ``` 所以, 安全的解法是结合 FilenameUtils.normalize(fileFullName) 进行处理, 代码如下: ``` @RequestMapping("/safeRightWay") public void safeRightWay(String filefullName, HttpServletRequest request) { filefullName = FilenameUtils.normalize(filefullName); if(isValidPath(filefullName)) { new File(filefullName); } } ``` 注意: 这里只是解决了风险, 但是使用Coverity 扫描时依旧还是扫出了风险,使用Coverity扫描的状况下, 如何完美处理Path Manipulation风险,请参考: [结合Coverity扫描Spring Boot项目进行Path Manipulation漏洞修复](https://blog.csdn.net/oscar999/article/details/128961641) ***** *****