@flusys/nestjs-storage 7.1.0 → 8.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/index.d.ts +3 -3
- package/controllers/file-manager.controller.d.ts +4 -4
- package/controllers/folder.controller.d.ts +2 -2
- package/controllers/index.d.ts +4 -4
- package/controllers/storage-config.controller.d.ts +2 -2
- package/controllers/upload.controller.d.ts +3 -3
- package/docs/index.d.ts +1 -1
- package/docs/storage-swagger.config.d.ts +2 -2
- package/dtos/index.d.ts +4 -4
- package/entities/file-manager-with-company.entity.d.ts +2 -2
- package/entities/file-manager.entity.d.ts +1 -1
- package/entities/folder-with-company.entity.d.ts +1 -1
- package/entities/index.d.ts +15 -15
- package/entities/storage-config-with-company.entity.d.ts +1 -1
- package/enums/index.d.ts +1 -1
- package/fesm/493.js +2768 -0
- package/fesm/537.js +1157 -0
- package/fesm/562.js +786 -0
- package/fesm/{providers/azure-provider.optional.js → chunks/190.js} +33 -30
- package/fesm/chunks/252.js +12 -0
- package/fesm/chunks/327.js +12 -0
- package/fesm/chunks/385.js +12 -0
- package/fesm/{providers/s3-provider.optional.js → chunks/591.js} +37 -34
- package/fesm/chunks/746.js +12 -0
- package/fesm/chunks/798.js +12 -0
- package/fesm/{providers/sftp-provider.optional.js → chunks/918.js} +35 -31
- package/fesm/config/index.js +119 -3
- package/fesm/controllers/index.js +16 -4
- package/fesm/docs/index.js +206 -1
- package/fesm/dtos/index.js +879 -4
- package/fesm/entities/index.js +463 -18
- package/fesm/enums/index.js +36 -1
- package/fesm/index.js +398 -12
- package/fesm/interfaces/index.js +19 -5
- package/fesm/modules/index.js +16 -1
- package/fesm/providers/index.js +364 -3
- package/fesm/runtime.js +252 -0
- package/fesm/services/index.js +16 -6
- package/fesm/utils/index.js +149 -1
- package/index.d.ts +11 -11
- package/interfaces/file-manager.interface.d.ts +1 -1
- package/interfaces/folder.interface.d.ts +1 -1
- package/interfaces/index.d.ts +5 -5
- package/interfaces/storage-config.interface.d.ts +1 -1
- package/interfaces/storage-module-options.interface.d.ts +3 -3
- package/interfaces/storage-provider.interface.d.ts +1 -1
- package/middlewares/file-serve.middleware.d.ts +4 -3
- package/middlewares/index.d.ts +1 -1
- package/modules/index.d.ts +1 -1
- package/modules/storage.module.d.ts +2 -2
- package/package.json +30 -41
- package/providers/azure-provider.optional.d.ts +2 -2
- package/providers/index.d.ts +3 -3
- package/providers/local-provider.d.ts +2 -2
- package/providers/s3-provider.optional.d.ts +2 -2
- package/providers/sftp-provider.optional.d.ts +2 -2
- package/providers/storage-factory.service.d.ts +3 -3
- package/providers/storage-provider.registry.d.ts +2 -2
- package/services/file-manager.service.d.ts +9 -8
- package/services/folder.service.d.ts +6 -6
- package/services/index.d.ts +6 -6
- package/services/storage-config.service.d.ts +3 -3
- package/services/storage-datasource.provider.d.ts +3 -3
- package/services/storage-provider-config.service.d.ts +7 -7
- package/services/upload.service.d.ts +6 -6
- package/utils/image-compressor.util.d.ts +1 -1
- package/utils/index.d.ts +1 -1
- package/cjs/config/event-actions.js +0 -34
- package/cjs/config/index.js +0 -20
- package/cjs/config/message-keys.js +0 -46
- package/cjs/config/storage.constants.js +0 -28
- package/cjs/controllers/file-manager.controller.js +0 -152
- package/cjs/controllers/file-manager.controller.spec.js +0 -135
- package/cjs/controllers/folder.controller.js +0 -102
- package/cjs/controllers/folder.controller.spec.js +0 -86
- package/cjs/controllers/index.js +0 -21
- package/cjs/controllers/storage-config.controller.js +0 -102
- package/cjs/controllers/storage-config.controller.spec.js +0 -85
- package/cjs/controllers/upload.controller.js +0 -234
- package/cjs/controllers/upload.controller.spec.js +0 -132
- package/cjs/docs/index.js +0 -11
- package/cjs/docs/storage-swagger.config.js +0 -188
- package/cjs/docs/storage-swagger.config.spec.js +0 -72
- package/cjs/dtos/file-manager.dto.js +0 -327
- package/cjs/dtos/folder.dto.js +0 -105
- package/cjs/dtos/index.js +0 -21
- package/cjs/dtos/storage-config.dto.js +0 -170
- package/cjs/dtos/upload.dto.js +0 -272
- package/cjs/entities/file-manager-with-company.entity.js +0 -61
- package/cjs/entities/file-manager.entity.js +0 -122
- package/cjs/entities/folder-with-company.entity.js +0 -55
- package/cjs/entities/folder.entity.js +0 -64
- package/cjs/entities/index.js +0 -63
- package/cjs/entities/index.spec.js +0 -18
- package/cjs/entities/storage-config-with-company.entity.js +0 -51
- package/cjs/entities/storage-config.entity.js +0 -96
- package/cjs/enums/file-location.enum.js +0 -17
- package/cjs/enums/index.js +0 -18
- package/cjs/index.js +0 -29
- package/cjs/interfaces/file-manager.interface.js +0 -4
- package/cjs/interfaces/folder.interface.js +0 -4
- package/cjs/interfaces/index.js +0 -22
- package/cjs/interfaces/storage-config.interface.js +0 -4
- package/cjs/interfaces/storage-module-options.interface.js +0 -4
- package/cjs/interfaces/storage-provider.interface.js +0 -4
- package/cjs/middlewares/file-serve.middleware.js +0 -241
- package/cjs/middlewares/file-serve.middleware.spec.js +0 -211
- package/cjs/middlewares/index.js +0 -18
- package/cjs/modules/index.js +0 -18
- package/cjs/modules/storage.module.js +0 -165
- package/cjs/modules/storage.module.spec.js +0 -184
- package/cjs/providers/azure-provider.optional.js +0 -155
- package/cjs/providers/azure-provider.optional.spec.js +0 -228
- package/cjs/providers/index.js +0 -26
- package/cjs/providers/local-provider.js +0 -230
- package/cjs/providers/local-provider.spec.js +0 -239
- package/cjs/providers/s3-provider.optional.js +0 -175
- package/cjs/providers/s3-provider.optional.spec.js +0 -243
- package/cjs/providers/sftp-provider.optional.js +0 -203
- package/cjs/providers/sftp-provider.optional.spec.js +0 -217
- package/cjs/providers/storage-factory.service.js +0 -175
- package/cjs/providers/storage-factory.service.spec.js +0 -253
- package/cjs/providers/storage-provider.registry.js +0 -41
- package/cjs/providers/storage-provider.registry.spec.js +0 -67
- package/cjs/services/file-manager.service.js +0 -387
- package/cjs/services/file-manager.service.spec.js +0 -580
- package/cjs/services/folder.service.js +0 -101
- package/cjs/services/folder.service.spec.js +0 -167
- package/cjs/services/index.js +0 -23
- package/cjs/services/storage-config.service.js +0 -129
- package/cjs/services/storage-config.service.spec.js +0 -257
- package/cjs/services/storage-datasource.provider.js +0 -137
- package/cjs/services/storage-datasource.provider.spec.js +0 -217
- package/cjs/services/storage-provider-config.service.js +0 -171
- package/cjs/services/storage-provider-config.service.spec.js +0 -265
- package/cjs/services/upload.service.js +0 -376
- package/cjs/services/upload.service.spec.js +0 -426
- package/cjs/utils/file-validator.util.js +0 -440
- package/cjs/utils/file-validator.util.spec.js +0 -259
- package/cjs/utils/image-compressor.util.js +0 -162
- package/cjs/utils/image-compressor.util.spec.js +0 -166
- package/cjs/utils/index.js +0 -18
- package/fesm/config/event-actions.js +0 -13
- package/fesm/config/message-keys.js +0 -22
- package/fesm/config/storage.constants.js +0 -7
- package/fesm/controllers/file-manager.controller.js +0 -142
- package/fesm/controllers/file-manager.controller.spec.js +0 -131
- package/fesm/controllers/folder.controller.js +0 -92
- package/fesm/controllers/folder.controller.spec.js +0 -82
- package/fesm/controllers/storage-config.controller.js +0 -92
- package/fesm/controllers/storage-config.controller.spec.js +0 -81
- package/fesm/controllers/upload.controller.js +0 -224
- package/fesm/controllers/upload.controller.spec.js +0 -128
- package/fesm/docs/storage-swagger.config.js +0 -181
- package/fesm/docs/storage-swagger.config.spec.js +0 -68
- package/fesm/dtos/file-manager.dto.js +0 -300
- package/fesm/dtos/folder.dto.js +0 -84
- package/fesm/dtos/storage-config.dto.js +0 -149
- package/fesm/dtos/upload.dto.js +0 -245
- package/fesm/entities/file-manager-with-company.entity.js +0 -51
- package/fesm/entities/file-manager.entity.js +0 -112
- package/fesm/entities/folder-with-company.entity.js +0 -45
- package/fesm/entities/folder.entity.js +0 -54
- package/fesm/entities/index.spec.js +0 -14
- package/fesm/entities/storage-config-with-company.entity.js +0 -41
- package/fesm/entities/storage-config.entity.js +0 -86
- package/fesm/enums/file-location.enum.js +0 -7
- package/fesm/interfaces/file-manager.interface.js +0 -1
- package/fesm/interfaces/folder.interface.js +0 -1
- package/fesm/interfaces/storage-config.interface.js +0 -1
- package/fesm/interfaces/storage-module-options.interface.js +0 -3
- package/fesm/interfaces/storage-provider.interface.js +0 -4
- package/fesm/middlewares/file-serve.middleware.js +0 -190
- package/fesm/middlewares/file-serve.middleware.spec.js +0 -207
- package/fesm/middlewares/index.js +0 -1
- package/fesm/modules/storage.module.js +0 -156
- package/fesm/modules/storage.module.spec.js +0 -180
- package/fesm/providers/azure-provider.optional.spec.js +0 -224
- package/fesm/providers/local-provider.js +0 -182
- package/fesm/providers/local-provider.spec.js +0 -194
- package/fesm/providers/s3-provider.optional.spec.js +0 -239
- package/fesm/providers/sftp-provider.optional.spec.js +0 -213
- package/fesm/providers/storage-factory.service.js +0 -124
- package/fesm/providers/storage-factory.service.spec.js +0 -249
- package/fesm/providers/storage-provider.registry.js +0 -34
- package/fesm/providers/storage-provider.registry.spec.js +0 -63
- package/fesm/services/file-manager.service.js +0 -336
- package/fesm/services/file-manager.service.spec.js +0 -576
- package/fesm/services/folder.service.js +0 -91
- package/fesm/services/folder.service.spec.js +0 -163
- package/fesm/services/storage-config.service.js +0 -119
- package/fesm/services/storage-config.service.spec.js +0 -253
- package/fesm/services/storage-datasource.provider.js +0 -86
- package/fesm/services/storage-datasource.provider.spec.js +0 -172
- package/fesm/services/storage-provider-config.service.js +0 -161
- package/fesm/services/storage-provider-config.service.spec.js +0 -261
- package/fesm/services/upload.service.js +0 -366
- package/fesm/services/upload.service.spec.js +0 -422
- package/fesm/utils/file-validator.util.js +0 -433
- package/fesm/utils/file-validator.util.spec.js +0 -255
- package/fesm/utils/image-compressor.util.js +0 -114
- package/fesm/utils/image-compressor.util.spec.js +0 -162
|
@@ -1,433 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Result of file content validation.
|
|
3
|
-
*/ /**
|
|
4
|
-
* Magic byte signatures for common file types.
|
|
5
|
-
* Each entry maps a hex signature pattern to its MIME type.
|
|
6
|
-
*/ const MAGIC_BYTES = [
|
|
7
|
-
// Images
|
|
8
|
-
{
|
|
9
|
-
signature: [
|
|
10
|
-
0xff,
|
|
11
|
-
0xd8,
|
|
12
|
-
0xff
|
|
13
|
-
],
|
|
14
|
-
offset: 0,
|
|
15
|
-
mimeType: 'image/jpeg'
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
signature: [
|
|
19
|
-
0x89,
|
|
20
|
-
0x50,
|
|
21
|
-
0x4e,
|
|
22
|
-
0x47,
|
|
23
|
-
0x0d,
|
|
24
|
-
0x0a,
|
|
25
|
-
0x1a,
|
|
26
|
-
0x0a
|
|
27
|
-
],
|
|
28
|
-
offset: 0,
|
|
29
|
-
mimeType: 'image/png'
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
signature: [
|
|
33
|
-
0x47,
|
|
34
|
-
0x49,
|
|
35
|
-
0x46,
|
|
36
|
-
0x38,
|
|
37
|
-
0x37,
|
|
38
|
-
0x61
|
|
39
|
-
],
|
|
40
|
-
offset: 0,
|
|
41
|
-
mimeType: 'image/gif'
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
signature: [
|
|
45
|
-
0x47,
|
|
46
|
-
0x49,
|
|
47
|
-
0x46,
|
|
48
|
-
0x38,
|
|
49
|
-
0x39,
|
|
50
|
-
0x61
|
|
51
|
-
],
|
|
52
|
-
offset: 0,
|
|
53
|
-
mimeType: 'image/gif'
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
signature: [
|
|
57
|
-
0x42,
|
|
58
|
-
0x4d
|
|
59
|
-
],
|
|
60
|
-
offset: 0,
|
|
61
|
-
mimeType: 'image/bmp'
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
signature: [
|
|
65
|
-
0x52,
|
|
66
|
-
0x49,
|
|
67
|
-
0x46,
|
|
68
|
-
0x46
|
|
69
|
-
],
|
|
70
|
-
offset: 0,
|
|
71
|
-
mimeType: 'image/webp'
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
signature: [
|
|
75
|
-
0x00,
|
|
76
|
-
0x00,
|
|
77
|
-
0x01,
|
|
78
|
-
0x00
|
|
79
|
-
],
|
|
80
|
-
offset: 0,
|
|
81
|
-
mimeType: 'image/x-icon'
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
signature: [
|
|
85
|
-
0x00,
|
|
86
|
-
0x00,
|
|
87
|
-
0x02,
|
|
88
|
-
0x00
|
|
89
|
-
],
|
|
90
|
-
offset: 0,
|
|
91
|
-
mimeType: 'image/x-icon'
|
|
92
|
-
},
|
|
93
|
-
// Documents
|
|
94
|
-
{
|
|
95
|
-
signature: [
|
|
96
|
-
0x25,
|
|
97
|
-
0x50,
|
|
98
|
-
0x44,
|
|
99
|
-
0x46
|
|
100
|
-
],
|
|
101
|
-
offset: 0,
|
|
102
|
-
mimeType: 'application/pdf'
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
signature: [
|
|
106
|
-
0x50,
|
|
107
|
-
0x4b,
|
|
108
|
-
0x03,
|
|
109
|
-
0x04
|
|
110
|
-
],
|
|
111
|
-
offset: 0,
|
|
112
|
-
mimeType: 'application/zip'
|
|
113
|
-
},
|
|
114
|
-
// Audio
|
|
115
|
-
{
|
|
116
|
-
signature: [
|
|
117
|
-
0x49,
|
|
118
|
-
0x44,
|
|
119
|
-
0x33
|
|
120
|
-
],
|
|
121
|
-
offset: 0,
|
|
122
|
-
mimeType: 'audio/mpeg'
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
signature: [
|
|
126
|
-
0xff,
|
|
127
|
-
0xfb
|
|
128
|
-
],
|
|
129
|
-
offset: 0,
|
|
130
|
-
mimeType: 'audio/mpeg'
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
signature: [
|
|
134
|
-
0xff,
|
|
135
|
-
0xfa
|
|
136
|
-
],
|
|
137
|
-
offset: 0,
|
|
138
|
-
mimeType: 'audio/mpeg'
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
signature: [
|
|
142
|
-
0x4f,
|
|
143
|
-
0x67,
|
|
144
|
-
0x67,
|
|
145
|
-
0x53
|
|
146
|
-
],
|
|
147
|
-
offset: 0,
|
|
148
|
-
mimeType: 'audio/ogg'
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
signature: [
|
|
152
|
-
0x66,
|
|
153
|
-
0x4c,
|
|
154
|
-
0x61,
|
|
155
|
-
0x43
|
|
156
|
-
],
|
|
157
|
-
offset: 0,
|
|
158
|
-
mimeType: 'audio/flac'
|
|
159
|
-
},
|
|
160
|
-
// Video
|
|
161
|
-
{
|
|
162
|
-
signature: [
|
|
163
|
-
0x00,
|
|
164
|
-
0x00,
|
|
165
|
-
0x00,
|
|
166
|
-
0x1c,
|
|
167
|
-
0x66,
|
|
168
|
-
0x74,
|
|
169
|
-
0x79,
|
|
170
|
-
0x70
|
|
171
|
-
],
|
|
172
|
-
offset: 0,
|
|
173
|
-
mimeType: 'video/mp4'
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
signature: [
|
|
177
|
-
0x00,
|
|
178
|
-
0x00,
|
|
179
|
-
0x00,
|
|
180
|
-
0x20,
|
|
181
|
-
0x66,
|
|
182
|
-
0x74,
|
|
183
|
-
0x79,
|
|
184
|
-
0x70
|
|
185
|
-
],
|
|
186
|
-
offset: 0,
|
|
187
|
-
mimeType: 'video/mp4'
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
signature: [
|
|
191
|
-
0x1a,
|
|
192
|
-
0x45,
|
|
193
|
-
0xdf,
|
|
194
|
-
0xa3
|
|
195
|
-
],
|
|
196
|
-
offset: 0,
|
|
197
|
-
mimeType: 'video/webm'
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
signature: [
|
|
201
|
-
0x52,
|
|
202
|
-
0x49,
|
|
203
|
-
0x46,
|
|
204
|
-
0x46
|
|
205
|
-
],
|
|
206
|
-
offset: 0,
|
|
207
|
-
mimeType: 'video/avi'
|
|
208
|
-
},
|
|
209
|
-
// Archives
|
|
210
|
-
{
|
|
211
|
-
signature: [
|
|
212
|
-
0x1f,
|
|
213
|
-
0x8b
|
|
214
|
-
],
|
|
215
|
-
offset: 0,
|
|
216
|
-
mimeType: 'application/gzip'
|
|
217
|
-
},
|
|
218
|
-
{
|
|
219
|
-
signature: [
|
|
220
|
-
0x37,
|
|
221
|
-
0x7a,
|
|
222
|
-
0xbc,
|
|
223
|
-
0xaf,
|
|
224
|
-
0x27,
|
|
225
|
-
0x1c
|
|
226
|
-
],
|
|
227
|
-
offset: 0,
|
|
228
|
-
mimeType: 'application/x-7z-compressed'
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
signature: [
|
|
232
|
-
0x52,
|
|
233
|
-
0x61,
|
|
234
|
-
0x72,
|
|
235
|
-
0x21,
|
|
236
|
-
0x1a,
|
|
237
|
-
0x07
|
|
238
|
-
],
|
|
239
|
-
offset: 0,
|
|
240
|
-
mimeType: 'application/x-rar-compressed'
|
|
241
|
-
}
|
|
242
|
-
];
|
|
243
|
-
/**
|
|
244
|
-
* MIME type aliases - types that are equivalent.
|
|
245
|
-
*/ const MIME_ALIASES = {
|
|
246
|
-
'image/jpeg': [
|
|
247
|
-
'image/jpg'
|
|
248
|
-
],
|
|
249
|
-
'image/jpg': [
|
|
250
|
-
'image/jpeg'
|
|
251
|
-
],
|
|
252
|
-
'video/mp4': [
|
|
253
|
-
'video/quicktime'
|
|
254
|
-
],
|
|
255
|
-
'application/zip': [
|
|
256
|
-
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
257
|
-
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
258
|
-
'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
|
259
|
-
]
|
|
260
|
-
};
|
|
261
|
-
/**
|
|
262
|
-
* File types that are text-based and don't have magic bytes.
|
|
263
|
-
* SECURITY NOTE: Dangerous types (HTML, JS, SVG) are excluded as they can contain scripts.
|
|
264
|
-
* These types require explicit allowlisting and additional content scanning.
|
|
265
|
-
*/ const TEXT_BASED_TYPES = [
|
|
266
|
-
'text/plain',
|
|
267
|
-
'text/csv',
|
|
268
|
-
'text/markdown',
|
|
269
|
-
'application/json',
|
|
270
|
-
'application/xml',
|
|
271
|
-
'application/typescript',
|
|
272
|
-
'text/css'
|
|
273
|
-
];
|
|
274
|
-
/**
|
|
275
|
-
* Dangerous text-based types that can execute scripts.
|
|
276
|
-
* These bypass magic-bytes validation but require explicit allowlisting.
|
|
277
|
-
*/ const DANGEROUS_TEXT_TYPES = [
|
|
278
|
-
'text/html',
|
|
279
|
-
'application/javascript',
|
|
280
|
-
'text/javascript',
|
|
281
|
-
'image/svg+xml',
|
|
282
|
-
'application/xhtml+xml'
|
|
283
|
-
];
|
|
284
|
-
/**
|
|
285
|
-
* ZIP-based format prefixes that are valid when detected as application/zip.
|
|
286
|
-
*/ const ZIP_VARIANT_PREFIXES = [
|
|
287
|
-
'application/vnd.openxmlformats-officedocument',
|
|
288
|
-
'application/x-zip',
|
|
289
|
-
'application/x-compressed'
|
|
290
|
-
];
|
|
291
|
-
/**
|
|
292
|
-
* Utility class for validating file content using magic bytes.
|
|
293
|
-
* Prevents file type spoofing by checking actual file content.
|
|
294
|
-
*/ export class FileValidator {
|
|
295
|
-
// Result Helpers
|
|
296
|
-
static failureResult(message, detectedType, declaredType) {
|
|
297
|
-
return {
|
|
298
|
-
valid: false,
|
|
299
|
-
detectedType,
|
|
300
|
-
declaredType,
|
|
301
|
-
message
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
static successResult(detectedType, declaredType) {
|
|
305
|
-
return {
|
|
306
|
-
valid: true,
|
|
307
|
-
detectedType,
|
|
308
|
-
declaredType
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Detect file type from buffer using magic bytes.
|
|
313
|
-
* @param buffer - File buffer to analyze
|
|
314
|
-
* @returns Detected MIME type or null if unknown
|
|
315
|
-
*/ static detectFileType(buffer) {
|
|
316
|
-
for (const { signature, offset, mimeType } of MAGIC_BYTES){
|
|
317
|
-
if (buffer.length < offset + signature.length) continue;
|
|
318
|
-
let matches = true;
|
|
319
|
-
for(let i = 0; i < signature.length; i++){
|
|
320
|
-
if (buffer[offset + i] !== signature[i]) {
|
|
321
|
-
matches = false;
|
|
322
|
-
break;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
if (matches) {
|
|
326
|
-
return mimeType;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
return null;
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
|
-
* Check if a MIME type is text-based (doesn't have magic bytes).
|
|
333
|
-
*/ static isTextBasedType(mimeType) {
|
|
334
|
-
return TEXT_BASED_TYPES.some((t)=>mimeType.startsWith(t) || mimeType === t);
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Check if a MIME type is a dangerous text type that can execute scripts.
|
|
338
|
-
*/ static isDangerousTextType(mimeType) {
|
|
339
|
-
return DANGEROUS_TEXT_TYPES.some((t)=>mimeType === t);
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Check if two MIME types are compatible (exact match or aliases).
|
|
343
|
-
*/ static mimeTypesMatch(detected, declared) {
|
|
344
|
-
// Exact match
|
|
345
|
-
if (detected === declared) return true;
|
|
346
|
-
// Check aliases
|
|
347
|
-
const aliases = MIME_ALIASES[detected];
|
|
348
|
-
if (aliases?.includes(declared)) return true;
|
|
349
|
-
// Check reverse aliases
|
|
350
|
-
const reverseAliases = MIME_ALIASES[declared];
|
|
351
|
-
if (reverseAliases?.includes(detected)) return true;
|
|
352
|
-
// Check if both are in same category (e.g., both images)
|
|
353
|
-
const detectedCategory = detected.split('/')[0];
|
|
354
|
-
const declaredCategory = declared.split('/')[0];
|
|
355
|
-
// For ZIP-based formats, allow any ZIP-detected file if declared is a ZIP variant
|
|
356
|
-
if (detected === 'application/zip' && ZIP_VARIANT_PREFIXES.some((v)=>declared.startsWith(v))) {
|
|
357
|
-
return true;
|
|
358
|
-
}
|
|
359
|
-
return detectedCategory === declaredCategory;
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Check if a MIME type is in the allowed list.
|
|
363
|
-
*/ static isTypeAllowed(mimeType, allowedTypes) {
|
|
364
|
-
// Wildcard allows all
|
|
365
|
-
if (allowedTypes.includes('*/*')) return true;
|
|
366
|
-
return allowedTypes.some((allowed)=>{
|
|
367
|
-
// Category wildcard (e.g., "image/*")
|
|
368
|
-
if (allowed.endsWith('/*')) {
|
|
369
|
-
const category = allowed.slice(0, -2);
|
|
370
|
-
return mimeType.startsWith(category);
|
|
371
|
-
}
|
|
372
|
-
return allowed === mimeType;
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* Validate file content matches declared MIME type using magic bytes.
|
|
377
|
-
* @param buffer - File buffer
|
|
378
|
-
* @param declaredMimeType - MIME type declared by client
|
|
379
|
-
* @param allowedTypes - List of allowed MIME types/patterns
|
|
380
|
-
* @returns Validation result
|
|
381
|
-
*/ static validateFileContent(buffer, declaredMimeType, allowedTypes = [
|
|
382
|
-
'*/*'
|
|
383
|
-
]) {
|
|
384
|
-
try {
|
|
385
|
-
const detectedType = this.detectFileType(buffer);
|
|
386
|
-
// No magic bytes detected - handle text-based types
|
|
387
|
-
if (!detectedType) {
|
|
388
|
-
return this.validateUndetectedType(declaredMimeType, allowedTypes);
|
|
389
|
-
}
|
|
390
|
-
// Verify detected type matches declared type
|
|
391
|
-
if (!this.mimeTypesMatch(detectedType, declaredMimeType)) {
|
|
392
|
-
return this.failureResult(`File content does not match declared type. Detected: ${detectedType}, Declared: ${declaredMimeType}`, detectedType, declaredMimeType);
|
|
393
|
-
}
|
|
394
|
-
// Verify type is in allowed list
|
|
395
|
-
if (!this.isTypeAllowed(detectedType, allowedTypes)) {
|
|
396
|
-
return this.failureResult(`File type "${detectedType}" is not allowed`, detectedType, declaredMimeType);
|
|
397
|
-
}
|
|
398
|
-
return this.successResult(detectedType, declaredMimeType);
|
|
399
|
-
} catch {
|
|
400
|
-
return this.failureResult('File validation failed');
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Handle validation for files without detectable magic bytes.
|
|
405
|
-
*/ static validateUndetectedType(declaredMimeType, allowedTypes) {
|
|
406
|
-
// Check for dangerous text types first (HTML, JS, SVG)
|
|
407
|
-
if (this.isDangerousTextType(declaredMimeType)) {
|
|
408
|
-
const explicitlyAllowed = allowedTypes.some((t)=>t === declaredMimeType && t !== '*/*' && !t.endsWith('/*'));
|
|
409
|
-
if (!explicitlyAllowed) {
|
|
410
|
-
return this.failureResult(`File type "${declaredMimeType}" is potentially dangerous and not explicitly allowed`, declaredMimeType, declaredMimeType);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
// Safe text-based files don't have magic bytes, trust declared type
|
|
414
|
-
if (this.isTextBasedType(declaredMimeType)) {
|
|
415
|
-
const isAllowed = this.isTypeAllowed(declaredMimeType, allowedTypes);
|
|
416
|
-
return isAllowed ? this.successResult(declaredMimeType, declaredMimeType) : this.failureResult(`File type "${declaredMimeType}" is not allowed`, declaredMimeType, declaredMimeType);
|
|
417
|
-
}
|
|
418
|
-
// Binary files without recognized signatures - be cautious
|
|
419
|
-
return this.failureResult('Unable to verify file type. File may be corrupted or unsupported.', undefined, declaredMimeType);
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Sanitize filename to prevent path traversal and special character issues.
|
|
423
|
-
* @param filename - Original filename
|
|
424
|
-
* @returns Sanitized filename
|
|
425
|
-
*/ static sanitizeFilename(filename) {
|
|
426
|
-
return filename// Remove path components (prevent traversal)
|
|
427
|
-
.replace(/^.*[\\\/]/, '')// Remove null bytes
|
|
428
|
-
.replace(/\0/g, '')// Replace multiple dots with single
|
|
429
|
-
.replace(/\.{2,}/g, '.')// Remove special characters except allowed ones
|
|
430
|
-
.replace(/[^a-zA-Z0-9._-]/g, '_')// Limit length
|
|
431
|
-
.substring(0, 255);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import { FileValidator } from './file-validator.util';
|
|
2
|
-
const JPEG_BYTES = Buffer.from([
|
|
3
|
-
0xff,
|
|
4
|
-
0xd8,
|
|
5
|
-
0xff,
|
|
6
|
-
0xe0,
|
|
7
|
-
0x00,
|
|
8
|
-
0x10
|
|
9
|
-
]);
|
|
10
|
-
const PNG_BYTES = Buffer.from([
|
|
11
|
-
0x89,
|
|
12
|
-
0x50,
|
|
13
|
-
0x4e,
|
|
14
|
-
0x47,
|
|
15
|
-
0x0d,
|
|
16
|
-
0x0a,
|
|
17
|
-
0x1a,
|
|
18
|
-
0x0a,
|
|
19
|
-
0x00,
|
|
20
|
-
0x00
|
|
21
|
-
]);
|
|
22
|
-
const PDF_BYTES = Buffer.from([
|
|
23
|
-
0x25,
|
|
24
|
-
0x50,
|
|
25
|
-
0x44,
|
|
26
|
-
0x46,
|
|
27
|
-
0x2d,
|
|
28
|
-
0x31,
|
|
29
|
-
0x2e,
|
|
30
|
-
0x34
|
|
31
|
-
]);
|
|
32
|
-
const ZIP_BYTES = Buffer.from([
|
|
33
|
-
0x50,
|
|
34
|
-
0x4b,
|
|
35
|
-
0x03,
|
|
36
|
-
0x04,
|
|
37
|
-
0x00,
|
|
38
|
-
0x00
|
|
39
|
-
]);
|
|
40
|
-
const GIF87_BYTES = Buffer.from([
|
|
41
|
-
0x47,
|
|
42
|
-
0x49,
|
|
43
|
-
0x46,
|
|
44
|
-
0x38,
|
|
45
|
-
0x37,
|
|
46
|
-
0x61
|
|
47
|
-
]);
|
|
48
|
-
const UNKNOWN_BYTES = Buffer.from([
|
|
49
|
-
0x01,
|
|
50
|
-
0x02,
|
|
51
|
-
0x03,
|
|
52
|
-
0x04
|
|
53
|
-
]);
|
|
54
|
-
describe('FileValidator', ()=>{
|
|
55
|
-
describe('detectFileType', ()=>{
|
|
56
|
-
it('detects JPEG from magic bytes', ()=>{
|
|
57
|
-
expect(FileValidator.detectFileType(JPEG_BYTES)).toBe('image/jpeg');
|
|
58
|
-
});
|
|
59
|
-
it('detects PNG from magic bytes', ()=>{
|
|
60
|
-
expect(FileValidator.detectFileType(PNG_BYTES)).toBe('image/png');
|
|
61
|
-
});
|
|
62
|
-
it('detects PDF from magic bytes', ()=>{
|
|
63
|
-
expect(FileValidator.detectFileType(PDF_BYTES)).toBe('application/pdf');
|
|
64
|
-
});
|
|
65
|
-
it('detects ZIP-based formats (docx/xlsx/pptx share this signature)', ()=>{
|
|
66
|
-
expect(FileValidator.detectFileType(ZIP_BYTES)).toBe('application/zip');
|
|
67
|
-
});
|
|
68
|
-
it('detects legacy GIF87a signature', ()=>{
|
|
69
|
-
expect(FileValidator.detectFileType(GIF87_BYTES)).toBe('image/gif');
|
|
70
|
-
});
|
|
71
|
-
it('returns null for unknown/unrecognized signatures', ()=>{
|
|
72
|
-
expect(FileValidator.detectFileType(UNKNOWN_BYTES)).toBeNull();
|
|
73
|
-
});
|
|
74
|
-
it('returns null for a buffer shorter than any known signature', ()=>{
|
|
75
|
-
expect(FileValidator.detectFileType(Buffer.from([
|
|
76
|
-
0xff
|
|
77
|
-
]))).toBeNull();
|
|
78
|
-
});
|
|
79
|
-
it('returns null for an empty buffer', ()=>{
|
|
80
|
-
expect(FileValidator.detectFileType(Buffer.alloc(0))).toBeNull();
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
describe('isTextBasedType', ()=>{
|
|
84
|
-
it('recognizes plain text and structured text types', ()=>{
|
|
85
|
-
expect(FileValidator.isTextBasedType('text/plain')).toBe(true);
|
|
86
|
-
expect(FileValidator.isTextBasedType('application/json')).toBe(true);
|
|
87
|
-
});
|
|
88
|
-
it('rejects a non text-based type', ()=>{
|
|
89
|
-
expect(FileValidator.isTextBasedType('image/png')).toBe(false);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
describe('isDangerousTextType', ()=>{
|
|
93
|
-
it('flags text/html as dangerous', ()=>{
|
|
94
|
-
expect(FileValidator.isDangerousTextType('text/html')).toBe(true);
|
|
95
|
-
});
|
|
96
|
-
it('flags image/svg+xml as dangerous (can embed scripts)', ()=>{
|
|
97
|
-
expect(FileValidator.isDangerousTextType('image/svg+xml')).toBe(true);
|
|
98
|
-
});
|
|
99
|
-
it('does not flag a safe type', ()=>{
|
|
100
|
-
expect(FileValidator.isDangerousTextType('image/png')).toBe(false);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
describe('mimeTypesMatch', ()=>{
|
|
104
|
-
it('matches identical types', ()=>{
|
|
105
|
-
expect(FileValidator.mimeTypesMatch('image/png', 'image/png')).toBe(true);
|
|
106
|
-
});
|
|
107
|
-
it('matches known aliases (jpeg <-> jpg)', ()=>{
|
|
108
|
-
expect(FileValidator.mimeTypesMatch('image/jpeg', 'image/jpg')).toBe(true);
|
|
109
|
-
expect(FileValidator.mimeTypesMatch('image/jpg', 'image/jpeg')).toBe(true);
|
|
110
|
-
});
|
|
111
|
-
it('matches same broad category (image/* vs image/*)', ()=>{
|
|
112
|
-
expect(FileValidator.mimeTypesMatch('image/png', 'image/webp')).toBe(true);
|
|
113
|
-
});
|
|
114
|
-
it('matches ZIP-detected content against an office document declared type', ()=>{
|
|
115
|
-
expect(FileValidator.mimeTypesMatch('application/zip', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')).toBe(true);
|
|
116
|
-
});
|
|
117
|
-
it('does not match unrelated categories', ()=>{
|
|
118
|
-
expect(FileValidator.mimeTypesMatch('image/png', 'application/pdf')).toBe(false);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
describe('isTypeAllowed', ()=>{
|
|
122
|
-
it('allows anything when wildcard */* is present', ()=>{
|
|
123
|
-
expect(FileValidator.isTypeAllowed('application/pdf', [
|
|
124
|
-
'*/*'
|
|
125
|
-
])).toBe(true);
|
|
126
|
-
});
|
|
127
|
-
it('allows category wildcard image/*', ()=>{
|
|
128
|
-
expect(FileValidator.isTypeAllowed('image/png', [
|
|
129
|
-
'image/*'
|
|
130
|
-
])).toBe(true);
|
|
131
|
-
});
|
|
132
|
-
it('rejects a type outside the category wildcard', ()=>{
|
|
133
|
-
expect(FileValidator.isTypeAllowed('application/pdf', [
|
|
134
|
-
'image/*'
|
|
135
|
-
])).toBe(false);
|
|
136
|
-
});
|
|
137
|
-
it('allows exact match', ()=>{
|
|
138
|
-
expect(FileValidator.isTypeAllowed('application/pdf', [
|
|
139
|
-
'application/pdf'
|
|
140
|
-
])).toBe(true);
|
|
141
|
-
});
|
|
142
|
-
it('rejects when not in the explicit allow-list', ()=>{
|
|
143
|
-
expect(FileValidator.isTypeAllowed('application/pdf', [
|
|
144
|
-
'image/png',
|
|
145
|
-
'image/jpeg'
|
|
146
|
-
])).toBe(false);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
describe('validateFileContent', ()=>{
|
|
150
|
-
it('passes when magic bytes match declared type and type is allowed', ()=>{
|
|
151
|
-
const result = FileValidator.validateFileContent(JPEG_BYTES, 'image/jpeg', [
|
|
152
|
-
'image/*'
|
|
153
|
-
]);
|
|
154
|
-
expect(result.valid).toBe(true);
|
|
155
|
-
expect(result.detectedType).toBe('image/jpeg');
|
|
156
|
-
});
|
|
157
|
-
it('fails when declared type does not match detected content (MIME spoofing)', ()=>{
|
|
158
|
-
const result = FileValidator.validateFileContent(PNG_BYTES, 'application/pdf', [
|
|
159
|
-
'*/*'
|
|
160
|
-
]);
|
|
161
|
-
expect(result.valid).toBe(false);
|
|
162
|
-
expect(result.message).toContain('does not match declared type');
|
|
163
|
-
});
|
|
164
|
-
it('fails when detected type is not in the allow-list', ()=>{
|
|
165
|
-
const result = FileValidator.validateFileContent(PDF_BYTES, 'application/pdf', [
|
|
166
|
-
'image/*'
|
|
167
|
-
]);
|
|
168
|
-
expect(result.valid).toBe(false);
|
|
169
|
-
expect(result.message).toContain('not allowed');
|
|
170
|
-
});
|
|
171
|
-
it('trusts declared type for safe text-based content without magic bytes', ()=>{
|
|
172
|
-
const result = FileValidator.validateFileContent(Buffer.from('name,age\nJohn,30'), 'text/csv', [
|
|
173
|
-
'text/*'
|
|
174
|
-
]);
|
|
175
|
-
expect(result.valid).toBe(true);
|
|
176
|
-
expect(result.detectedType).toBe('text/csv');
|
|
177
|
-
});
|
|
178
|
-
it('rejects dangerous text/html content unless explicitly allow-listed', ()=>{
|
|
179
|
-
const result = FileValidator.validateFileContent(Buffer.from('<html><body>hi</body></html>'), 'text/html', [
|
|
180
|
-
'*/*'
|
|
181
|
-
]);
|
|
182
|
-
expect(result.valid).toBe(false);
|
|
183
|
-
expect(result.message).toContain('potentially dangerous');
|
|
184
|
-
});
|
|
185
|
-
it('rejects image/svg+xml by default (can contain embedded scripts)', ()=>{
|
|
186
|
-
const result = FileValidator.validateFileContent(Buffer.from('<svg onload="alert(1)"></svg>'), 'image/svg+xml', [
|
|
187
|
-
'*/*'
|
|
188
|
-
]);
|
|
189
|
-
expect(result.valid).toBe(false);
|
|
190
|
-
});
|
|
191
|
-
it('still fails text/html even when explicitly allow-listed, since it is not in TEXT_BASED_TYPES', ()=>{
|
|
192
|
-
// NOTE: validateUndetectedType() skips the "dangerous type" rejection when the type is
|
|
193
|
-
// explicitly allow-listed, but text/html is absent from TEXT_BASED_TYPES so it falls through
|
|
194
|
-
// to the generic "Unable to verify file type" failure. Documenting actual behavior here;
|
|
195
|
-
// see final report for this being flagged as a potential production logic quirk.
|
|
196
|
-
const result = FileValidator.validateFileContent(Buffer.from('<html></html>'), 'text/html', [
|
|
197
|
-
'text/html'
|
|
198
|
-
]);
|
|
199
|
-
expect(result.valid).toBe(false);
|
|
200
|
-
expect(result.message).toContain('Unable to verify file type');
|
|
201
|
-
});
|
|
202
|
-
it('fails for binary content with no recognizable signature and no text fallback', ()=>{
|
|
203
|
-
const result = FileValidator.validateFileContent(UNKNOWN_BYTES, 'application/octet-stream', [
|
|
204
|
-
'*/*'
|
|
205
|
-
]);
|
|
206
|
-
expect(result.valid).toBe(false);
|
|
207
|
-
expect(result.message).toContain('Unable to verify file type');
|
|
208
|
-
});
|
|
209
|
-
it('defaults allowedTypes to */* when not provided', ()=>{
|
|
210
|
-
const result = FileValidator.validateFileContent(JPEG_BYTES, 'image/jpeg');
|
|
211
|
-
expect(result.valid).toBe(true);
|
|
212
|
-
});
|
|
213
|
-
it('returns a failure result instead of throwing on unexpected internal errors', ()=>{
|
|
214
|
-
// Passing a non-buffer-like value to force detectFileType internals to misbehave gracefully
|
|
215
|
-
const brokenBuffer = {
|
|
216
|
-
length: 10
|
|
217
|
-
};
|
|
218
|
-
const result = FileValidator.validateFileContent(brokenBuffer, 'image/jpeg', [
|
|
219
|
-
'*/*'
|
|
220
|
-
]);
|
|
221
|
-
expect(result.valid).toBe(false);
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
describe('sanitizeFilename', ()=>{
|
|
225
|
-
it('strips directory components to prevent path traversal', ()=>{
|
|
226
|
-
expect(FileValidator.sanitizeFilename('../../etc/passwd')).toBe('passwd');
|
|
227
|
-
});
|
|
228
|
-
it('strips Windows-style directory components', ()=>{
|
|
229
|
-
expect(FileValidator.sanitizeFilename('..\\..\\windows\\system32\\evil.exe')).toBe('evil.exe');
|
|
230
|
-
});
|
|
231
|
-
it('removes null bytes', ()=>{
|
|
232
|
-
expect(FileValidator.sanitizeFilename('file\0.txt')).toBe('file.txt');
|
|
233
|
-
});
|
|
234
|
-
it('collapses multiple consecutive dots into one', ()=>{
|
|
235
|
-
expect(FileValidator.sanitizeFilename('archive....tar.gz')).toBe('archive.tar.gz');
|
|
236
|
-
});
|
|
237
|
-
it('replaces special/unsafe characters with underscores', ()=>{
|
|
238
|
-
expect(FileValidator.sanitizeFilename('my file (final)!.txt')).toBe('my_file__final__.txt');
|
|
239
|
-
});
|
|
240
|
-
it('truncates filenames longer than 255 characters', ()=>{
|
|
241
|
-
const longName = `${'a'.repeat(300)}.txt`;
|
|
242
|
-
const sanitized = FileValidator.sanitizeFilename(longName);
|
|
243
|
-
expect(sanitized.length).toBeLessThanOrEqual(255);
|
|
244
|
-
});
|
|
245
|
-
it('preserves a simple safe filename unchanged', ()=>{
|
|
246
|
-
expect(FileValidator.sanitizeFilename('report-2024_final.pdf')).toBe('report-2024_final.pdf');
|
|
247
|
-
});
|
|
248
|
-
it('neutralizes an absolute-path traversal attempt embedded mid-string', ()=>{
|
|
249
|
-
const malicious = '/var/www/../../etc/passwd';
|
|
250
|
-
const sanitized = FileValidator.sanitizeFilename(malicious);
|
|
251
|
-
expect(sanitized).not.toContain('/');
|
|
252
|
-
expect(sanitized).not.toContain('..');
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
});
|