漏洞只能減少, 無法根除,
本文只初步介紹常見的攻擊手段及客戶端的基本防禦
攻擊手段及原理
-
上傳文件是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>