英雄背景无分隔线
准则

文件上传

在某个时候,应用程序需要允许用户将文件(用于使用或仅用于存储)上传应用程序中的某个位置,这种情况很常见。虽然看起来很简单,但由于与处理文件上传的方式相关的潜在风险,该功能的实现方式可能非常关键。

看看这个简短的例子,只是为了更好地直观地理解我们的意思。

假设这是一个允许用户上传个人资料照片的应用程序:

公共字符串 uploadProfilePicture(表单文件上传文件)
{
//生成保存上传文件的路径
变量路径 = $”。/uploads/avatars/ {request.user.ID}/{UploadedFile.file.fileName}”;

//保存文件
var localFile = file.openWrite(路径);
localfile.Write (UploadedFile.readToEnd ());
localfile.flush ();
localfile.close ();

//更新头像
Userprofile.updateUserProfilePicture(请求用户,路径)

返回路径;
}

这将是一个非常基本的上传功能,也恰好容易受到路径遍历的影响。

根据应用程序的确切实现,攻击者可以上传另一个页面/脚本(比如.asp、.aspx 或.php文件),这样就可以直接调用和执行任意代码。这也可能允许覆盖现有文件。

问题 1-保存到本地磁盘而不是外部数据存储

随着云服务的使用变得越来越普遍,应用程序通过容器交付,高可用性设置已成为标准,应不惜一切代价基本上避免将上传文件写入应用程序本地磁盘的做法。

应尽可能将文件上传到某种形式的中央存储器(块存储或数据库)。在这种情况下,这可以避免所有类别的安全漏洞。

问题 2-未验证扩展

在许多利用文件上传漏洞的情况下,它依赖于上传具有特定扩展名的文件的能力。因此,强烈建议对可以上传的文件使用扩展名的 “允许列表”。

确保使用您的语言/框架提供的方法来获取文件的扩展名,以避免出现诸如空字节注入之类的问题。

验证上传的内容类型也可能很诱人,但这样做会使上传内容变得非常脆弱,因为用于特定文件的内容类型可能因操作系统而异。它实际上也不会告诉你有关文件本身的任何信息,因为内容类型纯粹是扩展名的映射。

问题 3-无法阻止路径遍历

文件上传的另一个常见问题是,它们也往往容易受到路径遍历的影响。这本身就是整件事,因此,与其试图在这里进行总结,不如给出完整的指导方针 路径遍历 看一看。

更多例子

下面,我们还有更多关于安全和不安全文件上传的示例,你可以看看。

C#-不安全

公共字符串 uploadProfilePicture(iformFile UploadedFile Upl
{
//生成保存上传文件的路径
变量路径 = $”。/uploads/avatars/ {request.user.ID}/{UploadedFile.file.fileName}”;

//保存文件
var localFile = file.openWrite(路径);
localfile.Write (UploadedFile.readToEnd ());
localfile.flush ();
localfile.close ();

//更新头像
Userprofile.updateUserProfilePicture(请求用户,路径)

返回路径;
}

C#-安全

<string>允许公开列表扩展名 = new () {“.png”, “.jpg”, “.gif”};

公共字符串 uploadProfilePicture(iformFile UploadedFile Upl
{
//注意:最好的选择是避免将文件保存到本地磁盘。
var basePath = path.getFullPath (”./上传/头像/ “);

//通过不使用提供的文件名来防止路径遍历。还需要避免文件名冲突。
var newFileName = 生成文件名(UploadedFile.fileName)

//生成保存上传文件的路径
var canonicalPath = Path.Combine(basePath,newFilename);

//确保我们没有意外保存到基础文件夹之外的文件夹
如果 (!canonicalPath.startsWith (basePath))
{
返回 badRequest(“试图将文件保存在上传文件夹之外”);
}

//确保只保存允许的扩展名
如果 (!isFileAllowedExtension(上传后允许的扩展名)
{
返回 badRequest(“不允许扩展”);
}

//保存文件
var localFile = file.openWrite(权威路径);
localfile.Write (UploadedFile.readToEnd ());
localfile.flush ();
localfile.close ();

//更新头像
用户配置文件。更新用户配置文件图片(request.user,canonicalPath)

返回路径;

公共布尔生成文件名(字符串 originalFileName){
返回 $ “{guid.newGUID ()} {path.getExtension(原始文件名)}”;
}

<string>public bool isFileAllowedExtension(字符串文件名,列表扩展名){
返回扩展名.contains (path.getExtension (文件名));
}

Java-不安全

@Controller
公共类 FileUploadController {

@RequestMapping(值 = “/文件/上传”,方法 = requestMethod.POST)
@ResponseBody
公共回应实体<String>上传文件 (@RequestParam (“文件”) 多部分文件文件,@AuthenticationPrincipal 用户用户) {

试试 {

字符串上传路径 =”。/uploads/avatars/ "+ principal.getName () + “/” + file.getOriginalFileName ();

文件传输文件 = 新文件(上传路径);
File.transferto(传输文件);

} catch(异常 e){
返回新的 responseEntity<>(“上传错误”,httpStatus.internal_Server_Error);
}

返回新的 responseEntity<>(上传路径,httpStatus.created);
}
}

Java-安全

@Controller
公共类 FileUploadController {

@RequestMapping(值 = “/文件/上传”,方法 = requestMethod.POST)
@ResponseBody
公共回应实体<String>上传文件 (@RequestParam (“文件”) 多部分文件文件,@AuthenticationPrincipal 用户用户) {

试试 {
字符串 baseFolder = paths.get (”./uploads/avatars/ “) .normalize ();
字符串上传路径 = paths.GET (baseFolder.toString () +
生成文件名 (file.getOriginalFileName ())) .normalize ();
//确保扩展名是允许的类型
如果 (!isAllowedExtension (file.getOriginalFileName ()) {
返回新的 responseEntity<>(“不允许扩展”,httpStatus.Forbidden);
}

//确保文件未保存在上传根目录之外
如果 (!uploadPath.toString () .startsWith (baseFolder.toString ()) {
返回新的 responseEntity<> (“不允许将文件保存在基本文件夹之外。“,httpstatus.forbidden);
}

File transferFile = 新文件 (uploadPath.toString ());
file.transferTo (uploadPath.toString ());

} catch(异常 e){
返回新的 responseEntity<>(“上传错误”,httpStatus.internal_Server_Error);
}

返回新的 responseEntity<>(上传路径,httpStatus.created);
}

私有字符串 generateFileName(字符串文件名){
返回 UUID.randomUUID () .toString () + “.” + fileNameUtils.getExtension (文件名);
}

私有布尔值 isAllowedExtension(字符串文件名){
字符串 [] allowedExtensions = {"jpg”, “png”, “gif”};
字符串扩展名 = filenameUtils.getExtension(文件名);
返回允许的扩展名。包含(扩展名);
}
}

Python-Flask-不安全

@app .route ('/文件/上传',methods= ['POST'])
def upload_file ():

文件 = request.files ['文件']

SavedFilePath = os.path.join (”./uploads/avatars/ “,file.filename)
file.save(保存的文件路径)

返回 SavedFilePath

Python-Flask-安全

@app .route ('/文件/上传',methods= ['POST'])
def upload_file ():

文件 = request.files ['文件']
baseFolder = os.path.normpath (”./上传/头像/ “)
SavedFilePath = os.path.normpath(os.path.join(基本文件夹,generate_file_name(文件名)))

# 确保扩展名处于允许范围内
如果不是 is_extension_allowed(文件名):
返回 “不允许使用此扩展”

# 确保我们要保存的文件不在库外
如果没有 SavedFilePath.startsWith(基本文件夹):
返回 “试图将文件保存在基本文件夹之外”

file.save(保存的文件路径)

返回 SavedFilePath

def generate_file_name(文件名):
返回 str (uuid.uuid4 ()) + os.path.splitext(文件名)[1]

def is_extension_allowed(文件名):
在 (“.png”、“.jpg”、“.gif”) 中返回 os.path.splitext(文件名)[1]