·前言·
Java Spring是目前企业开发中使用较多的一种java开发框架,因此,基于该框架的安全内容尤为重要。Secure Code Warrior编码实验室的Java Spring涉及到的安全问题有9个,分别为缺少功能级别的访问控制、不恰当的身份验证、日志记录和监控不足、SQL注入、明文存储密码、路径遍历、服务器请求伪造、XML外部实体(XXE)、任意文件上传。
因涉及内容较多,完整内容将会在本公众号拆分为多篇内容分别发出。本文为该系列的第六篇——安全问题六:路径遍历。
往期内容请查看Java Spring编码安全系列。
Path traversal mitigation in Java Spring.
Java Spring 中的路径遍历缓解。
Prevent path traversal by rebuilding the path through extracting the filename from the file path.
通过从文件路径中提取文件名来重建路径,从而防止路径遍历。
安全问题六:路径遍历
题目
1、介绍
Path traversal, also known as directory traversal, occurs when a user has control over the construction of a path that is used to identify a file and uses this to gain access to arbitrary files on the system. This is the case in VikingBank's downloadBrochure functionality.
Open up the InfoController in the assignment folder and have a look at said method: the filename is passed as a parameter and used to create the full path.
An attacker controlling the value of the filename could change it to ../../../../../../../../etc/passwd.
When looking up the file, the application would traverse out of the brochures folder (due to each dot dot slash) and retrieve the /etc/passwd file.
This lab will tackle path traversal by constructing the path to the file through filename extraction.
当用户可以控制用于标识文件的路径的构造并使用该路径来访问系统上的任意文件时,就会发生路径遍历,也称为目录遍历。VikingBank 的下载手册功能就是这种情况。
打开分配文件夹中的 InfoController 并查看所述方法:文件名作为参数传递并用于创建完整路径。
控制文件名值的攻击者可以将其更改为../../../../../../../../etc/passwd。
查找文件时,应用程序将遍历小册子文件夹(由于每个点点斜杠)并检索 /etc/passwd 文件。
本实验将通过文件名提取构建文件路径来解决路径遍历问题。
2、源码
InfoController.java
package vikingbank.web.controllers;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import vikingbank.web.storage.StorageService;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@Controller
@RequestMapping("/info")
public class InfoController {
private final StorageService storageService;
public InfoController(StorageService storageService) {
this.storageService = storageService;
}
@GetMapping("downloadBrochure")
public ResponseEntity<byte[]> downloadBrochure(@RequestParam String fileName) throws RuntimeException, IOException {
Path filePath = Path.of(fileName);
Path location = storageService.load(filePath);
byte[] content = Files.readAllBytes(location);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setCacheControl("must-revalidate, post-check=0, pre-check=0");
return new ResponseEntity<>(content, headers, HttpStatus.OK);
}
}
*左右滑动查看更多
文件结构:
3、步骤一 添加扩展程序允许列表
首先,添加一个简单的文件扩展名验证,以确保只能检索允许扩展名的文件。
编写逻辑,以便在使用 DownloadBrochure 方法的 fileName 参数构建完整路径之前执行该逻辑。
3.1 task1
● 创建一个新的允许扩展名的列表。使用列表的好处是可以轻松地添加其他扩展名到允许列表中。在这种情况下,只允许PDF文件。
● 获取文件名的扩展名。
● 创建一个检查,验证文件名的扩展名是否在允许列表中,如果不在,则返回新的ResponseEntity<>(HttpStatus.BAD_REQUEST)。
Step Solution
import java.util.Arrays;
编写一个方法来将文件扩展名与允许的扩展名列表进行比较。
private boolean isValidExtension(String fileName) {
String[] allowedExtensions = { "pdf" };
String fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1);
return Arrays.asList(allowedExtensions).contains(fileExtension.toLowerCase());
}
在构造 Path 之前,调用 downloadBrochure 端点中的方法,并在验证返回 false 时返回 BadRequest。
if (!isValidExtension(fileName)) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
*左右滑动查看更多
根据要求修改代码:
InfoController.java
package vikingbank.web.controllers;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import vikingbank.web.storage.StorageService;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
"/info") (
public class InfoController {
private final StorageService storageService;
private final String[] allowedExtensions = { "pdf" };
public InfoController(StorageService storageService) {
this.storageService = storageService;
}
/**
* 检查文件扩展名是否有效
* @param fileName 文件名
* @return 如果文件扩展名有效,则返回 true;否则返回 false
*/
private boolean isValidExtension(String fileName) {
String fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1);
return Arrays.asList(allowedExtensions).contains(fileExtension.toLowerCase());
}
/**
* 下载宣传册的方法
* @param fileName 要下载的文件名
* @return 包含文件内容的 ResponseEntity 对象
* @throws RuntimeException 如果发生运行时异常
* @throws IOException 如果发生 IO 异常
*/
"downloadBrochure") (
public ResponseEntity<byte[]> downloadBrochure( String fileName) throws RuntimeException, IOException {
// 检查文件扩展名是否有效
if (!isValidExtension(fileName)) {
// 如果无效,返回一个包含 HttpStatus.BAD_REQUEST 的 ResponseEntity 对象
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
// 创建文件路径和存储位置
Path filePath = Path.of(fileName);
Path location = storageService.load(filePath);
// 读取文件内容为字节数组
byte[] content = Files.readAllBytes(location);
// 设置响应头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setCacheControl("must-revalidate, post-check=0, pre-check=0");
// 返回一个包含文件内容、响应头和 HttpStatus.OK 的 ResponseEntity 对象
return new ResponseEntity<>(content, headers, HttpStatus.OK);
}
}
*左右滑动查看更多
4、步骤二 提取文件名
目前,存储路径是通过存储位置的路径与文件路径组合而成的。为了防止目录遍历,不可靠的字符(例如 ../)不应进入完整路径。一种方法是从传入文件路径中提取文件名。
4.1 task1
使用 Path.of().getFileName() 提取文件名。
此方法将提取最后一个分隔符之后的路径部分。F.e. loans.pdf 和 ../../../loans.pdf 都会生成loans.pdf。
● 在文件路径上调用.getFileName() 以返回距离路径根最远的元素。
Step Solution
Path filePath = Path.of(fileName).getFileName();
*左右滑动查看更多
按照要求修改代码:
InfoController.java
package vikingbank.web.controllers;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import vikingbank.web.storage.StorageService;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
"/info") (
public class InfoController {
private final StorageService storageService;
private final String[] allowedExtensions = { "pdf" };
public InfoController(StorageService storageService) {
this.storageService = storageService;
}
/**
* 检查文件扩展名是否有效
* @param fileName 文件名
* @return 如果文件扩展名有效,则返回 true;否则返回 false
*/
private boolean isValidExtension(String fileName) {
String fileExtension = Path.of(fileName).getFileName().toString();
fileExtension = fileExtension.substring(fileExtension.lastIndexOf('.') + 1);
return
Arrays.asList(allowedExtensions).contains(fileExtension.toLowerCase());
}
/**
* 下载宣传册的方法
* @param fileName 要下载的文件名
* @return 包含文件内容的 ResponseEntity 对象
* @throws RuntimeException 如果发生运行时异常
* @throws IOException 如果发生 IO 异常
*/
"downloadBrochure") (
public ResponseEntity<byte[]> downloadBrochure( String fileName) throws RuntimeException, IOException {
// 检查文件扩展名是否有效
if (!isValidExtension(fileName)) {
// 如果无效,返回一个包含 HttpStatus.BAD_REQUEST 的 ResponseEntity 对象
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
// 创建文件路径和存储位置
Path filePath = Path.of(fileName);
Path location = storageService.load(filePath);
// 读取文件内容为字节数组
byte[] content = Files.readAllBytes(location);
// 设置响应头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setCacheControl("must-revalidate, post-check=0, pre-check=0");
// 返回一个包含文件内容、响应头和 HttpStatus.OK 的 ResponseEntity 对象
return new ResponseEntity<>(content, headers, HttpStatus.OK);
}
}
*左右滑动查看更多
提交通过。
分析一下:
在 isValidExtension 方法中,首先将 fileName 转换为 Path 对象,然后通过调用 filePath.getFileName().toString() 获取文件名的字符串表示。
接着,使用 substring 方法从文件名中提取文件扩展名,并将其转换为小写。最后,检查文件扩展名是否在允许的扩展名列表中。
在 downloadBrochure 方法中,首先调用 Path.of(fileName) 创建文件路径,并通过调用 storageService.load(filePath) 获取文件的存储位置。
然后,使用 Files.readAllBytes 方法读取文件内容为字节数组。接下来,设置响应头的内容类型为 MediaType.APPLICATION_PDF,并设置缓存控制头。
最后,返回一个包含文件内容、响应头和 HttpStatus.OK 的 ResponseEntity 对象。如果文件扩展名无效,将返回一个包含 HttpStatus.BAD_REQUEST 的 ResponseEntity 对象。
总结
1、 题目总结
当攻击者可以影响文件或目录的路径时,就可能会发生路径或目录遍历。为了避免此漏洞,需要在构建路径时控制用户输入。如果在业务层面不好实现,当从路径中提取文件名时,需要对可能导致遍历目录的分隔符进行过滤排查。
2、路径遍历通用修复方案
往期回顾
原文始发于微信公众号(安恒信息安全服务):九维团队-绿队(改进)| Java Spring编码安全系列之路径遍历
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论