zoukankan      html  css  js  c++  java
  • [安全]: 浅谈文件上传之客户端安全问题


    漏洞只能减少, 无法根除,
    本文只初步介绍常见的攻击手段及客户端的基本防御

    攻击手段及原理

    • 上传文件是WebShell时,攻击者可通过这些网页后门执行命令并控制服务器;

    • 上传文件是其他恶意脚本时,攻击者可直接执行脚本进行攻击;

    • 上传文件是恶意图片时,图片中可能包含了脚本,加载或者点击这些图片时脚本会悄无声息的执行;

    • 上传文件是伪装成正常后缀的恶意脚本时,攻击者可借助本地文件包含漏洞(Local File Include)执行该文件。如将bad.php文件改名为bad.doc上传到服务器,再通过PHP的include,include_once,require,require_once等函数包含执行。

    客户端问题(非第三方工具 NC Fidder等上传工具)

    • 文件上传检查不严, 没有进行文件格式检查
      • 例如: .php .Php .pHp等
    • 文件名没有检查
      • 例如: xxx.php%00.jpg, (%00为十六进制的0x00字符, 对于服务器来说,因为%00字符截断的关系,最终上传的文件变成了xxx.php)
    • 修改文件名功能是带了后缀( 先传输.jpg后, 改文件名是把文件后缀更换为 .php)

    抵御方法

    • 检查文件名后缀(注意大小写, 可先统一转换小写或是大写)
    • 重构文件名称(防止 xxx.php%00.jpg这种类型)
    • 若是图片, 使用resize函数, 压缩方式更改其大小, 这样就算是脚本, 里面的代码也会被破坏导致无法使用
    • 不可修改文件名后缀

    具体代码实例

    以市面上常见的框架及UI组件库

    Vue

    element-ui upload组件

    <!--
     * @Descripttion: 上传组件
     * @version: 1.0.0
     * @Author: 仲灏
     * @Date: 2019-11-21 10:15:15
     * @LastEditors: 仲灏
     * @LastEditTime: 2019-12-09 14:13:13
     -->
    <template>
      <el-upload
        ref="upload"
        :action="config.action"
        :headers="config.headers"
        :multiple="multiple"
        :data="file"
        :name="config.name"
        :http-request="handleHttpRequest"
        :show-file-list="showFileList"
        :drag="drag"
        :accept="accept"
        :list-type="listType"
        :file-list="fileList"
        :disabled="disabled"
        :limit="limit"
        :on-preview="handlePreview"
        :on-remove="handleRemove"
        :on-success="handleSuccess"
        :on-error="handleError"
        :on-progress="handleProgress"
        :on-change="handleChange"
        :before-upload="handleBeforeUpload"
        :before-remove="handleBeforeRemove"
        :on-exceed="handleExceed"
      >
        <i v-if="listType === 'picture-card' && !drag" class="el-icon-plus" />
        <div v-if="(listType === 'picture' && !drag) || (!drag && listType === 'text')">
          <el-button size="small" type="primary">点击上传</el-button>
          <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
        </div>
    
        <template v-if="drag">
          <div>
            <i class="el-icon-upload" />
            <div class="el-upload__text">
              将文件拖到此处,或
              <em>点击上传</em>
            </div>
            <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
          </div>
        </template>
        <el-dialog append-to-body :visible.sync="previewDialogVisible">
          <img width="100%" :src="dialogImageUrl" alt>
        </el-dialog>
      </el-upload>
    </template>
    <script>
    import Cookies from 'js-cookie'
    export default {
      name: 'ZUpload',
      props: {
        value: {
          type: Array,
          default: () => {
            return []
          }
        },
        label: {
          type: String,
          default: ''
        },
        tag: {
          type: String,
          default: ''
        },
        icon: {
          type: String,
          default: ''
        },
        prop: {
          type: String,
          default: ''
        },
        span: {
          type: Number,
          default: 24
        },
        readonly: {
          type: Boolean,
          default: false
        },
        size: {
          type: String,
          default: ''
        },
        fileSize: {
          type: Number,
          default: 5
        },
        disabled: {
          type: Boolean,
          default: false
        },
        accept: {
          type: String,
          default: ''
        },
        listType: {
          type: String,
          default: ''
        },
        drag: {
          type: Boolean,
          default: false
        },
        showFileList: {
          type: Boolean,
          default: true
        },
        multiple: {
          type: Boolean,
          default: true
        },
        limit: {
          type: Number,
          default: 10
        },
        quality: {
          // 图片质量
          type: Number,
          default: 0.1
        }
      },
      data() {
        return {
          dialogImageUrl: '',
          previewDialogVisible: false,
          file: {
            name: new Date().getTime()
          },
          fileList: [],
          config: {
            headers: {
              Authorization: Cookies.get('Token')
            },
            action: '********************',
            name: 'resource_url'
          },
          compressFile: ''
        }
      },
      mounted() {
        this.fileList = this.value
      },
      methods: {
        handlePreview(file) {
          console.log('点击文件列表中已上传的文件时的钩子')
          this.dialogImageUrl = file.url
          this.previewDialogVisible = true
        },
        handleDownload(file) {
          console.log('点击文件列表下载文件时的钩子')
          if (file.response && file.response.record) {
            window.open(file.response.record.resource_url)
          }
        },
    
        handleBeforeRemove(file, fileList) {
          console.log(`文件列表移除之前的钩子`)
          // return this.$confirm(`确定移除 ${file.name}?`)
          return true
        },
    
        handleRemove(file, fileList) {
          console.log('文件列表移除文件时的钩子')
          if (file.response) {
            const {
              response: {
                record: { id }
              }
            } = file
            this.$api('resource', 'delete', id).then(res => {
              // console.log(res)
              this.fileList = this.fileList.filter(v => v !== file)
            })
          }
          this.$emit('input', fileList)
        },
    
        handleBeforeUpload(file) {
          console.log(`上传文件之前的钩子`)
    
          const typeIsSatisfy = this.accept.split(',').includes(file.type)
    
          const fileType = file.name.substring(file.name.lastIndexOf('.') + 1)
          // const typeIsSatisfy = this.accept.split(',').includes(fileType)
    
          let sizeIsSatisfy = null
          if (Array.isArray(this.fileSize)) {
            sizeIsSatisfy =
              file.size > this.fileSize[0] && file.size < this.fileSize[1]
          } else {
            sizeIsSatisfy = file.size < this.fileSize * 1024 * 1024
          }
    
          if (!typeIsSatisfy) {
            this.$message.error(
              `当前限制文件格式为 ${this.accept},本次选择了文件格式为 ${fileType}`
            )
          }
    
          if (!sizeIsSatisfy) {
            this.$message.error(
              `当前限制小于 ${this.fileSize} M文件,本次选择了 ${(
                file.size /
                1024 /
                1024
              ).toFixed(2)} M大小的文件`
            )
          }
    
          return typeIsSatisfy && sizeIsSatisfy
        },
    
        handleProgress(event, file, filelist) {
          console.log(`文件开始上传时的钩子`)
        },
        // 自定义的上传实现函数
        handleHttpRequest(req) {
          console.log('触发上传行为函数方法')
          if (['image/jpeg', 'image/png', 'image/bmp', 'image/jpg'].includes(req.file.type)) {
            // 压缩图片
            this.compress(req.file, file => {
              const fileData = new FormData()
              fileData.append('resource_url', file)
              fileData.append('name', this.file.name)
              req.onProgress({
                percent: 0
              })
              req.onProgress({
                percent: 90
              })
              this.$api('resource', 'post', '', fileData)
                .then(res => {
                  req.onProgress({
                    percent: 100
                  })
                  req.onSuccess(res)
                })
                .catch(err => {
                  req.onError(err)
                })
            })
          } else {
            const fileData = new FormData()
            fileData.append('resource_url', req.file)
            fileData.append('name', this.file.name)
            req.onProgress({
              percent: 0
            })
            req.onProgress({
              percent: 90
            })
            this.$api('resource', 'post', '', fileData)
              .then(res => {
                req.onProgress({
                  percent: 100
                })
                req.onSuccess(res)
              })
              .catch(err => {
                req.onError(err)
              })
          }
        },
        handleSuccess(response, file, fileList) {
          console.log('文件上传成功时的钩子')
          console.log(response)
          if (response.code === 200) {
            this.fileList = fileList
            const fileArr = []
            fileList.forEach(v => {
              if (v.response) {
                let item = {}
                item = {
                  name: v.response.record.id,
                  url: v.response.record.resource_url
                }
                fileArr.push(item)
              } else {
                fileArr.push({ name: v.name, url: v.url })
              }
            })
            this.$emit('input', JSON.parse(JSON.stringify(fileArr)))
          }
        },
    
        // eslint-disable-next-line handle-callback-err
        handleError(err, file, filelist) {
          console.log(`文件上传失败时的钩子`)
          this.$message.error(err)
        },
    
        handleChange(file, fileList) {
          console.log(`文件状态改变时的钩子`)
        },
    
        handleExceed(files, fileList) {
          this.$message.warning(
            `当前限制选择 ${this.limit} 个文件,本次选择了 ${
              files.length
            } 个文件,共选择了 ${files.length + fileList.length} 个文件`
          )
        },
    
        /** 图片压缩,默认同比例压缩
         *  @param {Object} fileObj
         *  图片对象
         *  回调函数有一个参数,base64的字符串数据
         */
        compress(fileObj, callback) {
          const _this = this
          try {
            const image = new Image()
            image.src = URL.createObjectURL(fileObj)
            image.onload = function() {
              const that = this // 默认按比例压缩
              let w = that.width
              let h = that.height
              const scale = w / h
              w = fileObj.width || w
              h = fileObj.height || w / scale
              let quality = _this.quality // 默认图片质量为0.7 // 生成canvas
              const canvas = document.createElement('canvas')
              const ctx = canvas.getContext('2d') // 创建属性节点
              const anw = document.createAttribute('width')
              anw.nodeValue = w
              const anh = document.createAttribute('height')
              anh.nodeValue = h
              canvas.setAttributeNode(anw)
              canvas.setAttributeNode(anh)
              ctx.drawImage(that, 0, 0, w, h) // 图像质量
              if (fileObj.quality && fileObj.quality <= 1 && fileObj.quality > 0) {
                quality = fileObj.quality
              } // quality值越小,所绘制出的图像越模糊
              const base64 = canvas.toDataURL('image/jpeg', quality) // 压缩完成执行回调
    
              const file = _this.dataURLtoFile(base64)
              const blob = _this.base64ToBlob(base64)
              callback(file, blob, base64)
            }
          } catch (error) {
            console.log('压缩失败', error)
          }
        },
        // 将base64转换成file对象
        dataURLtoFile(dataurl, filename = this.file.name) {
          const arr = dataurl.split(',')
          const mime = arr[0].match(/:(.*?);/)[1]
          const suffix = mime.split('/')[1]
          const bstr = atob(arr[1])
          let n = bstr.length
          const u8arr = new Uint8Array(n)
          while (n--) {
            u8arr[n] = bstr.charCodeAt(n)
          }
          return new File([u8arr], `${filename}.${suffix}`, { type: mime })
        },
        base64ToBlob(urlData) {
          const bytes = window.atob(urlData.split(',')[1]) // 去掉url的头,并转换为byte
          // 处理异常,将ascii码小于0的转换为大于0
          const ab = new ArrayBuffer(bytes.length)
          const ia = new Uint8Array(ab)
          for (let i = 0; i < bytes.length; i++) {
            ia[i] = bytes.charCodeAt(i)
          }
          return new Blob([ab], { type: 'image/png' })
        }
      }
    }
    </script>
    

    组件的使用

    <!--
     * @Descripttion:
     * @version:
     * @Author: 仲灏
     * @Date: 2019-12-05 13:41:14
     * @LastEditors: 仲灏
     * @LastEditTime: 2019-12-09 14:15:49
     -->
    <template>
      <ZUpload
        v-model="content"
        :readonly="props.readonly"
        :size="props.size"
        :file-size="props.fileSize"
        :disabled="props.disabled"
        :accept="props.accept"
        :list-type="props.listType"
        :drag="props.drag"
        :show-file-list="props.showFileList"
        :multiple="props.multiple"
        :limit="props.limit"
        :quality="props.quality"
      />
    </template>
    
    <script>
    import ZUpload from '@/components/ZUpload'
    
    export default {
      name: 'Test',
      components: { ZUpload },
      data() {
        return {
          content: [],
          props: {
            label: '文件上传 upload',
            tag: 'z-upload',
            icon: 'iconfont icon-wenjianshangchuan',
            prop: 'elupload',
            span: 24,
            class: '',
            value: [],
            rules: [{ required: true, message: '请上传文件', trigger: 'blur' }],
            readonly: false,
            size: 'small',
            fileSize: 5,
            disabled: false,
            accept: 'image/jpeg,image/gif,image/png,image/bmp,image/jpg',
            listType: 'picture-card',
            drag: false,
            showFileList: true,
            multiple: true,
            limit: 10,
            quality: 1
          }
        }
      }
    }
    </script>
    

    在这里插入图片描述

  • 相关阅读:
    update set from 语句用法
    SQL WITH AS用法
    SQL插入数据的多种方式
    本地临时表“#” 与 全局临时表 "##"
    sql常用语句
    关于mybatis转义SQL动态执行语句变慢的原因
    关于SQL中OUTER APPLY 得用法
    AspNetPager控件的简单使用
    JQuery常用的案例
    JQuery的学习笔记
  • 原文地址:https://www.cnblogs.com/izhaong/p/12154258.html
Copyright © 2011-2022 走看看