一、基本介绍
路径穿越漏洞(Path Traversal Vulnerability)是一种安全漏洞,攻击者利用该漏洞可以访问系统中本不应被访问的文件或目录。通过构造特定的输入,攻击者可以绕过应用程序的安全机制,访问敏感文件或执行未授权的操作。
1.1 原理
路径穿越通常利用了文件路径的相对引用特性。攻击者可以在输入中使用特殊字符(如 ../
或 ..\
)来导航到父目录,从而访问不应该被访问的文件。例如,如果一个Web应用程序允许用户下载文件,攻击者可以输入 ../../etc/passwd
,试图获取系统的密码文件。
1.2 漏洞示例
假设有一个Web应用程序接受用户输入的文件名来读取文件并返回内容。如果没有对输入进行适当的验证,攻击者可以提交一个包含路径穿越字符的文件名,从而获取到服务器上的任意文件。
1.3 影响
路径穿越漏洞可能导致:
-
敏感数据泄露,例如配置文件、密码文件等。 -
服务器上的任意文件读取,可能导致系统被入侵。 -
进一步的攻击,例如通过获取敏感信息进行身份盗用。
二、修复方案
在操作系统中".."代表的是向上级目录跳转,如果程序在处理到诸如用 ../../../../../etc/passwd 的文件名时没有进行防护,则会跳转出当前工作目录,跳转到到其他目录中;从而返回系统敏感文件给用户。其危害为泄漏源码、泄漏系统敏感文件。
再此,建议统一编写一个校验公共函数,统一命名为PathTraversalValidator, 其他项目默认使用该公共函数校验即可。示例代码
2.1 Go代码示例
// bad: 任意文件读取
func handler(w http.ResponseWriter, r *http.Request) {
path := r.URL.Query()["path"][0]
// 未过滤文件路径,可能导致任意文件读取
data, _ := ioutil.ReadFile(path)
w.Write(data)
// 对外部传入的文件名变量,还需要验证是否存在../等路径穿越的文件名
data, _ = ioutil.ReadFile(filepath.Join("/home/user/", path))
w.Write(data)
}
// bad: 任意文件写入
func unzip(f string) {
r, _ := zip.OpenReader(f)
for _, f := range r.File {
p, _ := filepath.Abs(f.Name)
// 未验证压缩文件名,可能导致../等路径穿越,任意文件路径写入
ioutil.WriteFile(p, []byte("present"), 0640)
}
}
// good: 检查压缩的文件名是否包含..路径穿越特征字符,防止任意写入
func unzipGood(f string) bool {
r, err := zip.OpenReader(f)
if err != nil {
fmt.Println("read zip file fail")
return false
}
for _, f := range r.File {
if !strings.Contains(f.Name, "..") {
p, _ := filepath.Abs(f.Name)
ioutil.WriteFile(p, []byte("present"), 0640)
} else {
return false
}
}
return true
}
2.2 Java代码示例
// bad
@RequestMapping("/path/delete")
public void delete(HttpServletRequest request, HttpServletResponse response) {
String filePath = request.getParameter("path");
File file = new File(filePath); // 文件全路径由客户端传入,禁止
file.delete();
}
// good
@RequestMapping("/path/download")
public void download(HttpServletRequest request, HttpServletResponse response) {
String fileName = request.getParameter("name");
String DIR = "/data/file/upload/"; //文件服务器映射目录,非web目录
/*
* 防护方法:判断用户输入文件名是否存在..,存在则可能导致跨目录
*/
if(fileName.contains("..")) {
return;
}
File file = new File(DIR + fileName);
try {
InputStream inputStream = new FileInputStream(file);
OutputStream out = response.getOutputStream();
byte[] b = new byte[100];
int len;
while ((len = inputStream.read(b)) > 0) {
out.write(b, 0, len);
}
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// good
@RequestMapping("/path/upload")
public void safe_upload(@RequestParam(value="file") MultipartFile file) throws IOException {
String fileName = file.getOriginalFilename();
String DIR = "/data/file/upload/"; //文件服务器映射目录,非web目录
String filePath = DIR + fileName;
File tmpFile = new File(filePath);
/*
* 防护方法:File对象获取绝对路径,进行startwith判断
*/
if(!tmpFile.getCanonicalPath().startsWith(DIR)) {
throw new IOException(); // 这里return也可以
}
try {
InputStream in = file.getInputStream();
FileUtils.copyInputStreamToFile(in, tmpFile);
} catch (IOException e) {
e.printStackTrace();
}
}
// good
@RequestMapping("/path/delete")
public void safe_delete(HttpServletRequest request) {
/*
* 防护方法:判断用户输入的文件后缀名是否在白名单中,是的话执行下一步操作
*/
String webRootPath = request.getSession().getServletContext().getRealPath("/");
String fileName = request.getParameter("name");
if(fileName.contains("..")) {
return;
}
int pos = fileName.lastIndexOf(".");
String ext = fileName.substring(pos);
String whiteExt = ".jpg.jpeg.png.gif.bmp"; // 文件类型白名单,根据具体情况而定
if(whiteExt.contains(ext)) {
new File(webRootPath + fileName).delete();
}
}
// good
@RequestMapping("/path/upload")
public void safe_upload(@RequestParam(value="file") MultipartFile file) throws IOException {
String fileName = file.getOriginalFilename();
String ext = fileName.substring(fileName.lastIndexOf("."));
String DIR = "/data/file/upload/"; //文件服务器映射目录,非web目录
String fileName = UUID.randomUUID().toString(); // 文件名为随机字符串
String filePath = DIR + fileName + ext;
File tmpFile = new File(filePath);
try {
InputStream in = file.getInputStream();
FileUtils.copyInputStreamToFile(in, tmpFile);
} catch (IOException e) {
e.printStackTrace();
}
}
2.3 Node.js代码示例
const fs = require("fs");
const path = require("path");
let filename = req.query.ufile;
let root = '/data/ufile';
// bad:未检查文件名/路径
fs.readFile(root + filename, (err, data) => {
if (err) {
return console.error(err);
}
console.log(
异步读取: ${data.toString()}
);
});
// bad:使用path处理过后的路径参数值做校验,仍可能有路径穿越风险
filename = path.join(root, filename);
if (filename.indexOf("..") < 0) {
fs.readFile(filename, (err, data) => {
if (err) {
return console.error(err);
}
console.log(data.toString());
});
};
// good:检查了文件名/路径,是否包含路径穿越字符
if (filename.indexOf("..") < 0) {
filename = path.join(root, filename);
fs.readFile(filename, (err, data) => {
if (err) {
return console.error(err);
}
console.log(data.toString());
});
};
2.4 PHP代码示例
// bad:未检查文件名/路径
if(isset(
$_GET['filename'])){
$
path = "/var/www/html/" .
$_GET['filename'];
echo file_get_contents($
path);
}
// good:检查了文件名/路径,是否包含路径穿越字符
if(isset(
$_GET['filename'])){
$
path = "/var/www/html/" .
$_GET['filename'];
if(strpos($
path, '..') === false){
echo file_get_contents($path);
}else{
echo "filename is not valid";
}
}
2.5 Python代码示例
// good
import os
ALLOWED_EXTENSIONS = ['txt','jpg','png']
def allowed_file(filename):
if ('.' in filename and
'..' not in filename and
os.path.splitext(filename)[1].lower() in ALLOWED_EXTENSIONS):
return filename
return None
// good
import os
upload_dir = '/tmp/upload/' # 预期的上传目录
file_name = '../../etc/hosts' # 用户传入的文件名
absolute_path = os.path.join(upload_dir, file_name) # /tmp/upload/../../etc/hosts
normalized_path = os.path.normpath(absolute_path) # /etc/hosts
if not normalized_path.startswith(upload_dir): # 检查最终路径是否在预期的上传目录中
raise IOError()
三、延伸拓展
在大部分上传文件的业务场景中,部分后端会使用前端传入的文件名,这时就容易出现路径穿越风险。但是有些语言框架层在文件上传时,会对文件名做规范化处理,可以有效防止风险。
3.1 Java
如以下代码
在Java Spring Boot中,默认情况下,并没有对MultipartFile
的文件名进行路径过滤,包括像../..
这样的相对路径。这是因为Spring框架通常更倾向于提供开发者更大的灵活性和自由度,以便他们根据实际需求来自定义和控制文件上传的行为。在文件上传的过程中,Spring框架不会默认执行严格的文件名过滤,因为这可能会限制一些合法的文件名,对于某些特定的业务场景可能不适用。相对路径的问题通常是由于不正确的文件路径处理而产生的,因此开发者需要自行确保文件上传的安全性。
正确写法,判断文件名是否包含..,或者直接对文件名重新命名做md5,不使用用户上传的文件名。
@RestController
public void safe_delete(HttpServletRequest request) {
/*
* 防护方法:判断用户输入的文件后缀名是否在白名单中,是的话执行下一步操作
*/
String webRootPath = request.getSession().getServletContext().getRealPath("/");
String fileName = request.getParameter("name");
if(fileName.contains("..")) {
return;
}
int pos = fileName.lastIndexOf(".");
String ext = fileName.substring(pos);
String whiteExt = ".jpg.jpeg.png.gif.bmp"; // 文件类型白名单,根据具体情况而定
if(whiteExt.contains(ext)) {
new File(webRootPath + fileName).delete();
}
}
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
CommonsMultipartFile
中的 getOriginalFilename
方法对文件名进行了处理
3.2 Go
对于Go语言标准库中的mime/multipart
包,在处理文件上传时,会自动规范化文件路径,这意味着路径中的..
会被解析并去除,以防止路径遍历攻击。
curl -X POST -F "imgFile=@/Users/admin/Desktop/test.txt;filename=./../xe.txt" http://localhost:9000/api/upload
func uploadFile(w http.ResponseWriter, r *http.Request) {
file, handler, err := r.FormFile("imgFile")
if err != nil {
fmt.Println("Error retrieving the file")
fmt.Println(err)
return
}
defer file.Close()
// 创建一个新文件来保存上传的文件
f, err := os.OpenFile("uploadFile/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Println("Error creating the file")
fmt.Println(err)
return
}
defer f.Close()
// 将上传的文件内容复制到新文件中
_, err = io.Copy(f, file)
if err != nil {
fmt.Println("Error copying file contents")
fmt.Println(err)
return
}
fmt.Fprintf(w, "File uploaded successfullyn")
}
类似的Gin框架会调用Go语言标准库中的mime/multipart包来解析form-data请求体,该包会自动处理文件名中的相对路径符号,确保最终的文件名只包含合法的字符。
// FileName returns the filename parameter of the Part's Content-Disposition
// header. If not empty, the filename is passed through filepath.Base (which is
// platform dependent) before being returned.
func (p *Part) FileName() string {
if p.dispositionParams == nil {
p.parseContentDisposition()
}
filename := p.dispositionParams["filename"]
if filename == "" {
return ""
}
// RFC 7578, Section 4.2 requires that if a filename is provided, the
// directory path information must not be used.
return filepath.Base(filename)
}
原文始发于微信公众号(SDL安全):路径穿越漏洞的原理与防护
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论