Skip to content

Latest commit

 

History

History
225 lines (185 loc) · 9.29 KB

文件上传.md

File metadata and controls

225 lines (185 loc) · 9.29 KB

upload file

先做个简单的文件上传实例, 利用flask实现

@app.route('/upload', methods=['GET','POST'])
def upload():
    if request.files.get('filename'):
        file = request.files.get('filename')
        upload_dir = os.path.join(os.path.dirname(__file__), 'uploadfile')
        dir = os.path.join(upload_dir, file.filename)
        with open(dir, 'wb') as f:
            f.write(file.read())
        # file.save(dir)
        return render_template('upload.html', file='上传成功')
    else:
        return render_template('upload.html', file='选择文件')

然后如果需要读取上传文件,可以利用文件读取里的方式,或者使用flask的自带方法

return send_from_directory(os.path.join(os.path.dirname(__file__), 'uploadfile'), file)

django中实现一个文件上传样例。

def UPLOADFILE(request):
    if request.method == 'GET':
        return render(request, 'upload.html', {'file':'选择文件'})
    elif request.method == 'POST':
        dir = os.path.join(os.path.dirname(__file__), '../static/upload')
        file = request.FILES.get('filename')
        name = os.path.join(dir, file.name)
        with open(name, 'wb') as f:
            f.write(file.read())
        return render(request, 'upload.html', {'file':'上传成功'})

这些样例代码都存在未限制文件大小,未限制文件后缀,保存文件的时候可能会目录穿越造成覆盖。如果未限制大小,利用多线程上传的时候可能会对系统资源进行大量的消耗,从而导致dos的做用。

如果没有限制后缀,会造成文件上传,但在框架中的文件上传跟常规的又有些不一样,我们知道在django中都是需要路由来请求,如果我们只是单纯的上传一个py文件,并不会造成常规的文件上传利用。除非你用eval这种处理了文件。

但也不是百分百没问题,如果使用Apache加Python的环境开发,那就跟常规的网站类似了。

在httpd.conf中配置了对python的解析存在一段AddHandler mod_python .py。那么通过链接请求的时候,比如http://www.xxx.com/test.py,就会被解析。

还有一种是文件名的文件覆盖,例如功能需要批量上传,允许压缩包形式上传文件,然后解压到用户资源目录,如果此处存在问题,可能会覆盖关键文件来造成代码执行。比如__init__.py文件。

@app.route('/zip', methods=['GET','POST'])
def zip():
    if request.files.get('filename'):
        zip_file = request.files.get('filename')
        files = []
        with zipfile.ZipFile(zip_file, "r") as z:
            for fileinfo in z.infolist():
                filename = fileinfo.filename
                dat = z.open(filename, "r")
                files.append(filename)
                outfile = os.path.join(app.config['UPLOAD_FOLDER'], filename)
                if not os.path.exists(os.path.dirname(outfile)):
                    try:
                        os.makedirs(os.path.dirname(outfile))
                    except OSError as exc:
                        if exc.errno != errno.EEXIST:
                            print("\n[WARN] OS Error: Race Condition")
                if not outfile.endswith("/"):
                    with io.open(outfile, mode='wb') as f:
                        f.write(dat.read())
                dat.close()
        return render_template('upload.html', file=files)
    else:
        return render_template('upload.html', file='选择文件')

以上就是一个上传压缩包并且解压到目录的代码,他会按照解压出来的文件夹和文件进行写入目录。构造一个存在问题的压缩包,上传后可以看到文件并不在uploadfile目录,而在根目录下

>>> z_info = zipfile.ZipInfo(r"../__init__.py")
>>> z_file = zipfile.ZipFile("C:/Users/user/Desktop/bad.zip", mode="w")
>>> z_file.writestr(z_info, "print('test')")
>>> z_file.close()

项目如果被重新启动,就会看到界面输出了test字段。

模块也提供了一种安全的方法来解压,``zipfile.extract替换zipfile.ZipFile`,但是并不代表`extractall`也是安全的。

修复代码

对于文件的大小,上传的类型中已经有特定的属性来获取

#django 
file.size  #获取文件大小,字节
#flask
app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024  #限制1M大小

对于文件类型,flask给出了完整的限制,利用已有的函数和方式

ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])  #白名单
app.config['UPLOAD_FOLDER'] = os.path.join(os.path.dirname(__file__), 'uploadfile')

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS

@app.route('/upload', methods=['GET','POST'])
def upload():
    if request.files.get('filename'):
        file = request.files.get('filename')
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)  #处理文件名
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            return render_template('upload.html', file='上传成功')
        else:
            return render_template('upload.html', file='不允许类型')
    else:
        return render_template('upload.html', file='选择文件')

django也可以使用类似如上的写法

ALLOWED_EXTENSIONS = settings.ALLOWED_EXTENSIONS
UPLOAD_FOLDER = settings.UPLOAD_FOLDER

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS

def UPLOADFILE(request):
    if request.method=='GET':
        return render(request,'upload.html')
    else:
        img=request.FILES.get('filename')
        if img.size < 100000 and allowed_file(img.name):
            f=open(img.name,'wb')
            for line in img.chunks():
                f.write(line)
            f.close()
            return render(request, 'upload.html', {'file':'上传成功'})
        else:
            return render(request, 'upload.html', {'file':"不允许的类型或者大小超限"})

上面的写法中,明显有一个问题就是没有处理文件名,flask中有secure_filename,django中并没有这个函数。下面把上面的代码再进一步处理一下,根据验证通过的后缀来修改文件名,如果担心重名可以使用时间戳str(time.time())

import uuid

ALLOWED_EXTENSIONS = settings.ALLOWED_EXTENSIONS
MAX_SIZE = settings.MAX_FILE_SIZE
UPLOAD_FOLDER = settings.UPLOAD_FOLDER

def allowed_file(filename):
    if '.' in filename and filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS:
        filext = filename.rsplit('.', 1)[1]
        return str(uuid.uuid5(uuid.NAMESPACE_DNS, filename))+"."+filext
    else:
        return None

def UPLOADFILE(request):
    if request.method=='GET':
        return render(request,'upload.html')
    else:
        img=request.FILES.get('filename')
        if img.size < MAX_SIZE and allowed_file(img.name):
            name = UPLOAD_FOLDER+allowed_file(img.name)
            f=open(name,'wb')
            for line in img.chunks():
                f.write(line)
            f.close()
            return render(request, 'upload.html', {'file':'上传成功'})
        else:
            return render(request, 'upload.html', {'file':"不允许的类型或者大小超限"})

使用django自带的文件上传的方式

MEDIA_ROOT = os.path.join(BASE_DIR,'media')    #以后会自动将文件上传到指定的文件夹中
MEDIA_URL = '/media/'   #以后可以使用这个路由来访问上传的媒体文件
MAX_FILE_SIZE = 2097152  #文件大小

from django.conf.urls.static import static
from django.conf import settings
urlpatterns = [
    path('', views.IndexView.as_view()),   #配置路由
]+static(settings.MEDIA_URL,document_root = settings.MEDIA_ROOT)

#定义model,下面的FileExtensionValidator只在使用表单的使用有用,通过表单验证来限制。
models.FileField(upload_to='%Y/%m/%d',validators=[validators.FileExtensionValidator(['jpg','png'],message='必须是图像文件')], default='')

ALLOWED_EXTENSIONS = settings.ALLOWED_EXTENSIONS
MAX_SIZE = settings.MAX_FILE_SIZE
#定义一个views
class IndexView(View):
    def filename(self, file):
        if '.' in file and file.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS:
            filext = file.rsplit('.', 1)[1]
            return str(uuid.uuid5(uuid.NAMESPACE_DNS, file))+"."+filext
        else:
            return None
    def get(self,request):
        return render(request,'upload.html')
    def post(self,request):
        myfile = request.FILES.get('filename')
        try:
            if myfile.size <= MAX_SIZE and self.filename(myfile.name):
                myfile.name = self.filename(myfile.name)
                File.objects.create(filename=myfile.name, filext=myfile).save()
                return render(request, 'upload.html', {'file':'上传成功'})
            else:
                return render(request, 'upload.html', {'file':'不允许的类型或大小超限'})
        except Exception as e:
            return render(request,'upload.html', {'file':"不允许的类型或大小超限"})

这样就可以通过model来控制上传目录,然后还是采用如上的限制和判断标准,至于文件重名,django会自动添加字符串来防止重名文件。

还可以自定义field字段来限制:http://codingdict.com/questions/4840