iT邦幫忙

2021 iThome 鐵人賽

DAY 5
1

前言

前面幾天談的都是純文字的資料驗證,像是信箱、電話等等,但很多 API server 除了文字資料外也會提供上傳檔案、照片的功能,尤其現在那麼多電商一定會需要使用者上傳大頭貼、商品照之類的,有時甚至還會上傳影片,所以今天要來說說對於使用者上傳的檔案有哪些應該做的檢查

檔案類型

前端能做的事

雖然這個系列是以後端的 API Server 為主,不過這邊也順便講一下在前端怎麼限制檔案類型:平常在前端實作上傳檔案時一定會有一個 <input type="file"> 的 HTML 元素,這樣使用者才能按下按鈕選擇檔案。

而從 HTML5 開始,input 元件多了一個 accept 屬性可以設定,所以你可以透過 <input type="file" accept=".png,.jpg"> 來設定副檔名,或是直接用 <input type="file" accept="image/*,video/*"> 這種 MIME-Type 的格式來接受所有的圖片、影片檔,那使用者在選擇時就會像下圖這樣不會選錯檔案

但因為這只是前端的限制,只要 API 在那邊,攻擊者一定有辦法直接從瀏覽器以外的地方上傳檔案,所以馬上來說說後端對於使用者傳上來的檔案該做哪些檢查

後端

先舉個 Node.js 的例子,如果你的 API server 是用 multer 在處理檔案上傳的話,那可以透過 fileFilter 來設定要接受哪些類型的檔案,譬如說當 file.mimetype 是 jpg 或 jpeg 才接受,其他的檔案類型則拒絕上傳

var upload = multer({
    storage: storage,
    fileFilter: (req, file, cb) => {
        if (file.mimetype == "image/jpg" || file.mimetype == "image/jpeg") {
            cb(null, true)  // 接受這個檔案
        } else {
            cb(null, false) // 拒絕這個檔案
            return cb(new Error('Only .jpg/.jpeg format allowed!'))
        }
    }
})

app.post('/avatar', upload.single('avatar'), function (req, res) {
    // save image here
});

而在 Gin(Go 的框架)裡面做起來也滿簡單的,只要先從 *gin.Context 裡面拿到檔案,再判斷 MIME Type 是不是允許的類型就可以了~如果不是的話就直接回一個 400 給他

import (
    "github.com/gabriel-vasile/mimetype"
    "github.com/gin-gonic/gin"
)

func handler(ctx *gin.Context) error {
    fileHeader, err := ctx.FormFile("file")
    file, err := fFile.Open()
        
    mimeType, err := mimetype.DetectReader(io.Reader) // 偵測 MIME type
        
    // 把不要的格式拒絕掉
    if mimeType != "image/png" {
        c.JSON(400, gin.H{"error": "Only .png format allowed!"})
    }
}

檔案大小

除了檔案類型之外,限制檔案大小也非常重要,如果完全不做限制,那攻擊者只要同時上傳一堆很大的檔案,譬如說一次塞給你 10GB,那整個伺服器就會變得非常慢,記憶體也有可能因此被塞爆

因為這個功能非常常見,所以 Node.js 的 multer 也實作了一個 limits 屬性讓你做設定,如果你想限制上傳的大頭貼最大 10MB,那就設定 10 << 20(也就是 10 * 1024 * 1024 bytes)就可以了~

var upload = multer({
    storage: storage,
    fileFilter: (req, file, cb) => { /* ... */ },
    limits: { fileSize: 10 << 20 } // 在這裡設定 max size = 10MB
});

而 Go 的話也很簡單,只要在進到 handler 之前先過一個 size limiter 就可以了,這樣就能確保上傳的檔案都不會太大

import "github.com/gin-contrib/size"

func handler(ctx *gin.Context) {
    // 先檢查剛剛有沒有發生錯誤
    if len(ctx.Errors) > 0 {
        return
    }

    // 都沒問題再把檔案存起來
}

func main() {
    router := gin.Default()

    // 先檢查 request size 再進到 handler
    router.POST("/avatar", limits.RequestSizeLimiter(10 << 20), handler)

    router.Run(":8888")
}

小結

今天介紹了為什麼 API Server 在收到檔案時要檢查 MIME Type 跟檔案大小,也示範了在 Node.js 跟 Go 裡面怎麼用現有的 library 來實作

關於「入口管制」的部分就到今天結束,如果對於內容有什麼問題的話歡迎在下方留言,沒有的話明天就要開始「流量限制」的部分囉~

延伸閱讀


上一篇
Day04-入口管制(三)
下一篇
Day06-流量限制(一)
系列文
從以卵擊石到堅若磐石之 Web API 安全性全攻略30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言