File type validation

Posted by MinhHungTrinh on 2021-07-30
Estimated Reading Time 7 Minutes
Words 1.2k In Total
Viewed Times

main

Problem

Trong quá trình làm việc với các team dự án, mình thấy một chức năng khá phổ biến là upload file (video, ảnh, pdf…). Chắc chắn đi cùng với chức năng này chúng ta cần đảm bảo validate (kiểm tra) các điều kiện cho đúng. 1 trong những điều kiện validation cần có chính là giới hạn loại file mà bạn cho phép Upload. Bạn có thể thực hiện validate ở backend hoặc frontend (phụ thuộc bạn thực hiện upload file ở client side hay backend side).
Vậy vấn đề ở đây là gì, qua quá trình review qua vài dự án đã từng làm, việc các team thực hiện validate giới hạn loại file có vẻ đang không hiệu quả. Thưởng các team đó sẽ kiểm tra extension hoặc mime type của file. Các thông tin này đôi khi là không chính xác ở chỗ chúng ta có thể fake được. => Đây là giải pháp không tốt.

Nên hôm nay có google n bài để sưu tầm cho anh em tham khảo:

Solution

Giải pháp cho việc kiểm tra loại file mọi người có thể dựa vào Signatures của File. Cụ thể cách làm sẽ là thực hiện đọc vài byte đầu tiên (ở ví dụ dưới mình sẽ đọc 16 bytes). Chúng ta sẽ dựa vào chuỗi số đó để xác định đó là loại file nào. Mọi người có thể tham khảo từ https://en.wikipedia.org/wiki/List_of_file_signatures để xác định.

OK, vậy việc kiểm tra signature sẽ thực hiện ở đâu, ở backend hay client. Nó phụ thuộc vào bạn thực hiện upload file ở Backend Side hay Client Side. Ở bài này mình sẽ không so sánh giữa việc upload ở Backend Side hay Client Side.

  • Việc upload lên Backend Side, có thể file được lưu trữ ở Server hoặc đẩy lên Cloud Storage (ví dụ AWS S3). Vậy thì việc kiểm tra signature sẽ nằm ở trên Backend rồi.
  • Việc upload ở Client Side, ở đây hiểu là chúng ta upload trực tiếp từ Client Side lên các Cloud Storage. Ví dụ sử dụng PreSignUrl để upload trực tiếp lên AWS S3 thì mọi người có thể tham khảo bài này của mình nhé. (https://kipalog.kaopiz.com/posts/Upload-file-to-S3-client-side)

Practice

Kiểm tra ở Client Side: Sử dụng Javascript:

Chúng ta sử dụng FileReader để đọc các bytes của file nhé. Yên tâm là mình thấy library này cũng support gần hết các browser rồi. AE tra ở https://caniuse.com/filereader nhé.

Chúng ta cần làm các steps sau:

  • Cần tạo 1 giao diện upload file (người dùng chọn và thực hiện chọn file để upload)
  • Code Javascript ở Client thực hiện cắt 4 bytes của file.
  • Sử dụng FileReader đọc 4 bytes đó.
  • Dựa vào File Signature để xác định loại file. => validate trực tiếp ở đây.

Lười code nên tham khảo luôn source từ link này: https://medium.com/the-everyday-developer/detect-file-mime-type-using-magic-numbers-and-javascript-16bc513d4e1e#_=_
Trong link có cả JSFiddle của tác giả cho ae test online luôn nhé: https://jsfiddle.net/0t27goLg/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
<!doctype html>
<html lang="en">
<head>
<title>Filereader</title>
<style>
div {
font-family: "Helvetica Neue";
line-height:22px;
font-size:15px;
margin:10px 0;
color: #333;
}
em {
padding: 2px 4px;
background-color: #efefef;
font-style: normal;
}
</style>
</head>

<body>

<input type="file" id="file-selector">

<div id="files"></div>
<script>

const uploads = []

const fileSelector = document.getElementById('file-selector')
fileSelector.addEventListener('change', (event) => {
console.time('FileOpen')
const file = event.target.files[0]

const filereader = new FileReader()

filereader.onloadend = function(evt) {
if (evt.target.readyState === FileReader.DONE) {
const uint = new Uint8Array(evt.target.result)
let bytes = []
uint.forEach((byte) => {
bytes.push(byte.toString(16))
})
const hex = bytes.join('').toUpperCase()

uploads.push({
filename: file.name,
filetype: file.type ? file.type : 'Unknown/Extension missing',
binaryFileType: getMimetype(hex),
hex: hex
})
render()
}

console.timeEnd('FileOpen')
}


const blob = file.slice(0, 4);
filereader.readAsArrayBuffer(blob);
})

const render = () => {
const container = document.getElementById('files')

const uploadedFiles = uploads.map((file) => {
return `<div>
<strong>${file.filename}</strong><br>
Filetype from file object: ${file.filetype}<br>
Filetype from binary: ${file.binaryFileType}<br>
Hex: <em>${file.hex}</em>
</div>`
})

container.innerHTML = uploadedFiles.join('')
}

const getMimetype = (signature) => {
switch (signature) {
case '89504E47':
return 'image/png'
case '47494638':
return 'image/gif'
case '25504446':
return 'application/pdf'
case 'FFD8FFDB':
case 'FFD8FFE0':
return 'image/jpeg'
case '504B0304':
return 'application/zip'
default:
return 'Unknown filetype'
}
}

</script>

</body>
</html>

Kiểm tra ở Backend

Ở trên Backend, ae có thể tự kiểm tra giống như code ở client hoặc có thể sử dụng library. Mình làm việc với PHP và NodeJS nên mình note ở dưới nhé.


Đây là Blog cá nhân của MinhHungTrinh, nơi mình chia sẻ, lưu giữ kiến thức. Nếu các bạn có góp ý, thắc mắc thì vui lòng comment bên dưới cho mình biết nhé. Mình luôn là người lắng nghe và ham học hỏi. Các vấn đề đặc biệt hoặc tế nhị mọi người có thể gửi email tới minhhungtrinhvn@gmail.com. Cảm ơn Mọi Người đã đọc Blog của mình. Yolo!