【问题标题】:File Uploads with Turbogears 2使用 Turbogears 2 上传文件
【发布时间】:2011-01-23 10:26:27
【问题描述】:

我一直在尝试找出使用 Turbogears 2 管理文件上传的“最佳实践”方法,但到目前为止还没有真正找到任何示例。我已经找到了一种实际上传文件的方法,但我不确定它的可靠性。

另外,获取上传文件名的好方法是什么?

    file = request.POST['file']
    permanent_file = open(os.path.join(asset_dirname,
        file.filename.lstrip(os.sep)), 'w')
    shutil.copyfileobj(file.file, permanent_file)
    file.file.close()
    this_file = self.request.params["file"].filename 
    permanent_file.close()

所以假设我理解正确,这样的事情会避免核心“命名”问题吗? id = UUID。

    file = request.POST['file']
    permanent_file = open(os.path.join(asset_dirname,
        id.lstrip(os.sep)), 'w')
    shutil.copyfileobj(file.file, permanent_file)
    file.file.close()
    this_file = file.filename
    permanent_file.close()

【问题讨论】:

  • 是的,使用uuid1().hexuuid4().hex 将解决命名问题和大多数安全问题。您不需要在 uuid 上调用 lstrip()(不要使用 id 作为变量名 - 它会掩盖内置的 id())。因此,使用uuid 生成一个唯一名称并将上传的数据复制到上传目录中该名称的文件中。如果您需要存储用户提供的文件名,请将其保存为元数据,也许在您的数据库中。您将面临其他安全问题,此处描述了其中几个问题:scanit.be/uploads/php-file-upload.pdf
  • 好的,感谢您的帮助 mhawke。伟大的阅读。出于好奇,是否有任何真正的理由使用 uuid4().hex 而不是 uuid4()?前者生成了一个稍微更人性化的 UUID,用于 URL 等。 (我正在考虑生成两个 UUID,一个用于下载 URL,另一个用于实际文件名/ID。
  • 除了文件名的长度之外,uuid4().hexstr(uuid4()) 或多或少是相同的。我认为为 URL 和文件名使用独立的 UUID 没有任何优势,因为您现在需要一个额外的层来将 URL 中的 UUID 映射到实际文件。

标签: python turbogears2


【解决方案1】:

我只想让任何来这里寻找答案的人都知道Allesandro Molina 的伟大图书馆Depot 是这个问题的最佳答案。

它解决了命名和复制问题,并且可以很好地整合到您的 TurboGears 应用程序中。您可以将它与 MongoDB GridFS 一起使用,如下例所示:

from depot.manager import DepotManager

# Configure a *default* depot to store files on MongoDB GridFS
DepotManager.configure('default', {
    'depot.backend': 'depot.io.gridfs.GridFSStorage',
    'depot.mongouri': 'mongodb://localhost/db'
})

depot = DepotManager.get()

# Save the file and get the fileid
fileid = depot.create(open('/tmp/file.png'))

# Get the file back
stored_file = depot.get(fileid)
print stored_file.filename
print stored_file.content_type

或者您可以轻松地在 SQLAlchemy 模型中创建附件字段,例如:

from depot.fields.sqlalchemy import UploadedFileField

class Document(Base):
    __tablename__ = 'document'

    uid = Column(Integer, autoincrement=True, primary_key=True)
    name = Column(Unicode(16), unique=True)

    content = Column(UploadedFileField)

...然后,存储带有附件的文档(源可以是文件或字节)变得如此简单:

doc = Document(name=u'Foo', content=open('/tmp/document.xls'))
DBSession.add(doc)

Depot 同时支持LocalFileStorageMongoDBGridFSStorage 和亚马逊的S3Storage。而且,至少对于存储在本地和 S3 中的文件,fileid 将由uuid.uuid1() 生成。

【讨论】:

    【解决方案2】:

    @mhawke - 你是对的,你必须处理它 - 取决于你对文件所做的事情,如果存在名称冲突并不重要,例如你只关心某些数据的最新版本可能没有问题,或者文件名实际上并不重要,只是文件内容,但它仍然是不好的做法。

    您可以在 tmp 目录中使用命名的临时文件,然后在验证后将文件移动到其最终位置。或者您可以像这样检查文件名不存在:

    file.name = slugify(myfile.filename)
    name, ext = os.path.splitext(file.name)
    while os.path.exists(os.path.join(permanent_store, file.name)):
        name += '_'
        file.name = name + ext
    
    raw_file = os.path.join(permanent_store, file.name)
    

    slugify 方法将用于整理文件名...

    【讨论】:

    • 现在我们进入竞争条件。在检查文件是否已经存在和实际创建它之间会有一个时间间隔。如果两个用户同时上传同名文件怎么办? os.path.exists() 在这两种情况下都可以返回False,然后在创建文件时,一个文件将覆盖另一个文件。
    【解决方案3】:

    我不太了解 Turbogears 以及它是否可以提供任何东西来避免以下情况,但在我看来,这段代码充满了危险。恶意用户可能会覆盖(或创建)Turbogears python 进程具有写入权限的任何文件。

    如果asset_dirname/tmpfile.filename 的内容是../../../../../../../etc/passwd,文件的内容是root::0:0:root:/root:/bin/bash,会怎样?在 UNIX 环境中,此代码(权限待定)将以截断模式打开文件 /tmp/../../../../../../../etc/passwd,然后将上传文件的内容复制到其中 - 有效地覆盖系统的密码文件并指定没有密码的 root 用户。大概也有一些讨厌的事情可以对 Windows 机器做。

    好的,这是一个极端的例子,它要求 python 以root 运行(没有人这样做,是吗?)。即使python以低权限用户运行,之前上传的文件也可以随意覆盖。

    总而言之,不要相信用户输入,在这种情况下,用户提供的文件名在 file.filename 中可用。

    【讨论】:

    • 是的,我意识到这是一个有风险的系统。我不太确定python通常如何处理文件(从PHP背景移动,所以这是一个有点困难的改变,虽然我喜欢python。)这就是为什么我希望设计一个更好的Turbogears上传系统的人会知道并且帮忙。谢谢,这是有用的第一步。 :)
    • 我认为 turbogears 提供了一组有用的控件,称为 Tosca Widgets,并且有一个用于文件上传的小部件。我建议您对此进行调查,因为您希望最终得到符合标准的东西(wrt turbogears),并且有望避免我在您的示例中指出的那种漏洞。
    • 非常感谢您的想法。看起来 Tosca Widgets 确实有一个“文件字段”字段。他们的文档有些令人困惑,但到目前为止我发现了这一点。 toscawidgets.org/documentation/tw.forms/modules/fields/…
    【解决方案4】:

    涡轮齿轮箱不只是带有附加功能的挂架吗?你可以在那里查看帮助:

    http://wiki.pylonshq.com/display/pylonsdocs/Form+Handling#file-uploads

    但是,这仍然包含 mhawke 提到的潜在安全漏洞:

    os.path.join(permanent_store, myfile.filename.lstrip(os.sep))
    

    如果文件名不知何故为../../../../../etc/passwd,则与上述相同,那么您可以替换该文件...

    所以你可以像这样得到实际的文件名:

    os.path.join(permanent_store, myfile.filename.split(os.sep).pop())
    

    【讨论】:

    • 使用os.path.join(permanent_store,myfile.filename.split(os.sep).pop()) 仍然存在信任用户提供的数据的问题。如果用户上传的文件具有相同的基本名称怎么办?如果基本名称发生冲突,之前上传的文件仍将被覆盖。
    • 那么一个可行的解决方案是从文件名中删除所有正斜杠吗?
    • 那么 pop 只是使用文件名并忽略路径并阻止安全漏洞在您的临时位置之外访问。
    【解决方案5】:

    Werkzeug 有一个非常好的帮助函数来保护文件名,称为secure_filename。我认为您可以采用和使用它。

    【讨论】:

      【解决方案6】:

      关于如何去,我会支持已经给出的好的答案。

      这是我关于存储文件命名的 2 便士。

      确实使用原始名称保存文件可能会导致漏洞。 我对原始名称的唯一用途(如果有的话)是提示 mime 类型检测。

      无论如何,要保存的文件应该被赋予唯一的名称,通过记录标识或类似的东西,并保存在应用程序目录所有者控制下的地方,他是普通用户,或者在其他一些存储服务中,如前面提到的仓库等。

      这是一个跨语言良好系统设计的问题:)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-09-04
        • 1970-01-01
        • 2011-12-14
        • 1970-01-01
        • 1970-01-01
        • 2023-03-11
        • 2013-05-07
        相关资源
        最近更新 更多