@flusys/nestjs-storage 0.1.0-beta.3 → 1.0.0-rc
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/README.md +131 -19
- package/cjs/config/storage-config.service.js +5 -0
- package/cjs/config/storage.constants.js +0 -8
- package/cjs/controllers/file-manager.controller.js +50 -5
- package/cjs/controllers/folder.controller.js +46 -4
- package/cjs/controllers/storage-config.controller.js +46 -4
- package/cjs/controllers/upload.controller.js +6 -12
- package/cjs/dtos/file-manager.dto.js +8 -5
- package/cjs/dtos/storage-config.dto.js +41 -1
- package/cjs/dtos/upload.dto.js +7 -0
- package/cjs/entities/storage-config-base.entity.js +31 -2
- package/cjs/interfaces/index.js +0 -1
- package/cjs/middlewares/file-serve.middleware.js +6 -0
- package/cjs/modules/storage.module.js +2 -4
- package/cjs/providers/local-provider.js +52 -2
- package/cjs/providers/storage-factory.service.js +2 -2
- package/cjs/services/file-manager.service.js +37 -24
- package/cjs/services/folder.service.js +18 -52
- package/cjs/services/storage-datasource.provider.js +10 -16
- package/cjs/services/storage-provider-config.service.js +28 -63
- package/cjs/services/upload.service.js +39 -27
- package/cjs/utils/file-validator.util.js +470 -0
- package/cjs/utils/image-compressor.util.js +1 -3
- package/config/storage-config.service.d.ts +5 -2
- package/config/storage.constants.d.ts +0 -2
- package/controllers/file-manager.controller.d.ts +1 -1
- package/controllers/upload.controller.d.ts +2 -6
- package/dtos/file-manager.dto.d.ts +2 -4
- package/dtos/folder.dto.d.ts +2 -4
- package/dtos/storage-config.dto.d.ts +9 -6
- package/entities/storage-config-base.entity.d.ts +2 -0
- package/fesm/config/storage-config.service.js +5 -0
- package/fesm/config/storage.constants.js +0 -2
- package/fesm/controllers/file-manager.controller.js +51 -6
- package/fesm/controllers/folder.controller.js +49 -7
- package/fesm/controllers/storage-config.controller.js +49 -7
- package/fesm/controllers/upload.controller.js +7 -13
- package/fesm/dtos/file-manager.dto.js +8 -5
- package/fesm/dtos/storage-config.dto.js +45 -11
- package/fesm/dtos/upload.dto.js +8 -1
- package/fesm/entities/index.js +1 -5
- package/fesm/entities/storage-config-base.entity.js +33 -7
- package/fesm/interfaces/index.js +0 -1
- package/fesm/interfaces/storage-config.interface.js +1 -3
- package/fesm/middlewares/file-serve.middleware.js +7 -1
- package/fesm/modules/storage.module.js +2 -4
- package/fesm/providers/local-provider.js +52 -2
- package/fesm/providers/storage-factory.service.js +2 -2
- package/fesm/services/file-manager.service.js +38 -25
- package/fesm/services/folder.service.js +19 -53
- package/fesm/services/storage-datasource.provider.js +10 -16
- package/fesm/services/storage-provider-config.service.js +28 -63
- package/fesm/services/upload.service.js +40 -28
- package/fesm/utils/file-validator.util.js +463 -0
- package/fesm/utils/image-compressor.util.js +1 -3
- package/interfaces/file-manager.interface.d.ts +7 -4
- package/interfaces/index.d.ts +0 -1
- package/interfaces/storage-config.interface.d.ts +2 -20
- package/package.json +6 -6
- package/providers/local-provider.d.ts +2 -0
- package/services/file-manager.service.d.ts +2 -2
- package/services/folder.service.d.ts +1 -2
- package/services/storage-provider-config.service.d.ts +1 -2
- package/utils/file-validator.util.d.ts +16 -0
- package/cjs/interfaces/file-upload-response.interface.js +0 -4
- package/fesm/interfaces/file-upload-response.interface.js +0 -1
- package/interfaces/file-upload-response.interface.d.ts +0 -6
|
@@ -25,25 +25,41 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import {
|
|
28
|
+
import { ErrorHandler, validateCompanyOwnership } from '@flusys/nestjs-shared/utils';
|
|
29
|
+
import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Scope } from '@nestjs/common';
|
|
29
30
|
import { StorageConfigService } from '../config';
|
|
30
31
|
import { FileLocationEnum } from '../enums/file-location.enum';
|
|
31
32
|
import { StorageFactoryService } from '../providers/storage-factory.service';
|
|
32
33
|
import { StorageProviderConfigService } from './storage-provider-config.service';
|
|
34
|
+
import { FileValidator } from '../utils/file-validator.util';
|
|
33
35
|
export class UploadService {
|
|
34
36
|
/**
|
|
35
|
-
* Validate file before upload
|
|
37
|
+
* Validate file before upload - includes size, type, and content validation.
|
|
38
|
+
* Uses magic bytes to verify file content matches declared MIME type.
|
|
36
39
|
*/ validateFile(file) {
|
|
37
40
|
// Validate file size
|
|
38
41
|
const sizeValidation = this.storageConfigService.validateFileSize(file.size);
|
|
39
42
|
if (!sizeValidation.valid) {
|
|
40
43
|
throw new BadRequestException(sizeValidation.message);
|
|
41
44
|
}
|
|
42
|
-
// Validate file type
|
|
45
|
+
// Validate declared file type (MIME)
|
|
43
46
|
const typeValidation = this.storageConfigService.validateFileType(file.mimetype);
|
|
44
47
|
if (!typeValidation.valid) {
|
|
45
48
|
throw new BadRequestException(typeValidation.message);
|
|
46
49
|
}
|
|
50
|
+
// Validate file content matches declared type (magic bytes check)
|
|
51
|
+
// This prevents MIME type spoofing attacks
|
|
52
|
+
const allowedTypes = this.storageConfigService.getAllowedFileTypes();
|
|
53
|
+
const contentValidation = FileValidator.validateFileContent(file.buffer, file.mimetype, allowedTypes);
|
|
54
|
+
if (!contentValidation.valid) {
|
|
55
|
+
this.logger.warn(`File content validation failed: ${contentValidation.message}`, {
|
|
56
|
+
declaredType: file.mimetype,
|
|
57
|
+
detectedType: contentValidation.detectedType
|
|
58
|
+
});
|
|
59
|
+
throw new BadRequestException(contentValidation.message || 'File content validation failed');
|
|
60
|
+
}
|
|
61
|
+
// Sanitize filename to prevent path traversal attacks
|
|
62
|
+
file.originalname = FileValidator.sanitizeFilename(file.originalname);
|
|
47
63
|
}
|
|
48
64
|
/**
|
|
49
65
|
* Get storage provider and config info based on storage config ID
|
|
@@ -57,13 +73,8 @@ export class UploadService {
|
|
|
57
73
|
if (!config) {
|
|
58
74
|
throw new NotFoundException('Storage configuration not found');
|
|
59
75
|
}
|
|
60
|
-
// Validate company ownership
|
|
61
|
-
|
|
62
|
-
const configWithCompany = config;
|
|
63
|
-
if (configWithCompany.companyId && configWithCompany.companyId !== user.companyId) {
|
|
64
|
-
throw new BadRequestException('Storage configuration belongs to another company');
|
|
65
|
-
}
|
|
66
|
-
}
|
|
76
|
+
// Validate company ownership using shared utility
|
|
77
|
+
validateCompanyOwnership(config, user, this.storageConfigService.isCompanyFeatureEnabled(), 'Storage configuration');
|
|
67
78
|
storageConfig = config;
|
|
68
79
|
} else {
|
|
69
80
|
// Use default config (scoped to user's company/branch)
|
|
@@ -168,9 +179,9 @@ export class UploadService {
|
|
|
168
179
|
location,
|
|
169
180
|
storageConfigId: configId
|
|
170
181
|
};
|
|
171
|
-
} catch (
|
|
172
|
-
this.logger
|
|
173
|
-
|
|
182
|
+
} catch (error) {
|
|
183
|
+
ErrorHandler.logError(this.logger, error, 'uploadSingleFile');
|
|
184
|
+
ErrorHandler.rethrowError(error);
|
|
174
185
|
}
|
|
175
186
|
}
|
|
176
187
|
async uploadMultipleFiles(files, options, user) {
|
|
@@ -187,31 +198,31 @@ export class UploadService {
|
|
|
187
198
|
location,
|
|
188
199
|
storageConfigId: configId
|
|
189
200
|
}));
|
|
190
|
-
} catch (
|
|
191
|
-
this.logger
|
|
192
|
-
|
|
201
|
+
} catch (error) {
|
|
202
|
+
ErrorHandler.logError(this.logger, error, 'uploadMultipleFiles');
|
|
203
|
+
ErrorHandler.rethrowError(error);
|
|
193
204
|
}
|
|
194
205
|
}
|
|
195
206
|
async deleteSingleFile(key, storageConfigId, user, locationHint) {
|
|
196
207
|
try {
|
|
197
|
-
if (!key) throw new
|
|
208
|
+
if (!key) throw new BadRequestException('No file path provided');
|
|
198
209
|
const provider = await this.getStorageProviderForDelete(storageConfigId, user, locationHint);
|
|
199
210
|
await provider.deleteFile(key);
|
|
200
211
|
return true;
|
|
201
|
-
} catch (
|
|
202
|
-
this.logger
|
|
203
|
-
|
|
212
|
+
} catch (error) {
|
|
213
|
+
ErrorHandler.logError(this.logger, error, 'deleteSingleFile');
|
|
214
|
+
ErrorHandler.rethrowError(error);
|
|
204
215
|
}
|
|
205
216
|
}
|
|
206
217
|
async deleteMultipleFile(keys, storageConfigId, user, locationHint) {
|
|
207
218
|
try {
|
|
208
|
-
if (!keys || !keys.length) throw new
|
|
219
|
+
if (!keys || !keys.length) throw new BadRequestException('No file paths provided');
|
|
209
220
|
const provider = await this.getStorageProviderForDelete(storageConfigId, user, locationHint);
|
|
210
221
|
await provider.deleteMultipleFiles(keys);
|
|
211
222
|
return true;
|
|
212
|
-
} catch (
|
|
213
|
-
this.logger
|
|
214
|
-
|
|
223
|
+
} catch (error) {
|
|
224
|
+
ErrorHandler.logError(this.logger, error, 'deleteMultipleFiles');
|
|
225
|
+
ErrorHandler.rethrowError(error);
|
|
215
226
|
}
|
|
216
227
|
}
|
|
217
228
|
bytesToKb(bytes) {
|
|
@@ -241,7 +252,8 @@ export class UploadService {
|
|
|
241
252
|
}
|
|
242
253
|
return null;
|
|
243
254
|
} catch (error) {
|
|
244
|
-
|
|
255
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
256
|
+
this.logger.warn(`Failed to get local storage basePath: ${errorMessage}`);
|
|
245
257
|
return null;
|
|
246
258
|
}
|
|
247
259
|
}
|
|
@@ -258,9 +270,9 @@ export class UploadService {
|
|
|
258
270
|
}
|
|
259
271
|
// For SFTP or other providers without presigned URLs
|
|
260
272
|
return key;
|
|
261
|
-
} catch (
|
|
262
|
-
this.logger
|
|
263
|
-
|
|
273
|
+
} catch (error) {
|
|
274
|
+
ErrorHandler.logError(this.logger, error, 'makeFileUrl');
|
|
275
|
+
ErrorHandler.rethrowError(error);
|
|
264
276
|
}
|
|
265
277
|
}
|
|
266
278
|
// NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
function _define_property(obj, key, value) {
|
|
2
|
+
if (key in obj) {
|
|
3
|
+
Object.defineProperty(obj, key, {
|
|
4
|
+
value: value,
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true
|
|
8
|
+
});
|
|
9
|
+
} else {
|
|
10
|
+
obj[key] = value;
|
|
11
|
+
}
|
|
12
|
+
return obj;
|
|
13
|
+
}
|
|
14
|
+
import { Logger } from '@nestjs/common';
|
|
15
|
+
/**
|
|
16
|
+
* Magic byte signatures for common file types.
|
|
17
|
+
* Each entry maps a hex signature pattern to its MIME type.
|
|
18
|
+
*/ const MAGIC_BYTES = [
|
|
19
|
+
// Images
|
|
20
|
+
{
|
|
21
|
+
signature: [
|
|
22
|
+
0xff,
|
|
23
|
+
0xd8,
|
|
24
|
+
0xff
|
|
25
|
+
],
|
|
26
|
+
offset: 0,
|
|
27
|
+
mimeType: 'image/jpeg'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
signature: [
|
|
31
|
+
0x89,
|
|
32
|
+
0x50,
|
|
33
|
+
0x4e,
|
|
34
|
+
0x47,
|
|
35
|
+
0x0d,
|
|
36
|
+
0x0a,
|
|
37
|
+
0x1a,
|
|
38
|
+
0x0a
|
|
39
|
+
],
|
|
40
|
+
offset: 0,
|
|
41
|
+
mimeType: 'image/png'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
signature: [
|
|
45
|
+
0x47,
|
|
46
|
+
0x49,
|
|
47
|
+
0x46,
|
|
48
|
+
0x38,
|
|
49
|
+
0x37,
|
|
50
|
+
0x61
|
|
51
|
+
],
|
|
52
|
+
offset: 0,
|
|
53
|
+
mimeType: 'image/gif'
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
signature: [
|
|
57
|
+
0x47,
|
|
58
|
+
0x49,
|
|
59
|
+
0x46,
|
|
60
|
+
0x38,
|
|
61
|
+
0x39,
|
|
62
|
+
0x61
|
|
63
|
+
],
|
|
64
|
+
offset: 0,
|
|
65
|
+
mimeType: 'image/gif'
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
signature: [
|
|
69
|
+
0x42,
|
|
70
|
+
0x4d
|
|
71
|
+
],
|
|
72
|
+
offset: 0,
|
|
73
|
+
mimeType: 'image/bmp'
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
signature: [
|
|
77
|
+
0x52,
|
|
78
|
+
0x49,
|
|
79
|
+
0x46,
|
|
80
|
+
0x46
|
|
81
|
+
],
|
|
82
|
+
offset: 0,
|
|
83
|
+
mimeType: 'image/webp'
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
signature: [
|
|
87
|
+
0x00,
|
|
88
|
+
0x00,
|
|
89
|
+
0x01,
|
|
90
|
+
0x00
|
|
91
|
+
],
|
|
92
|
+
offset: 0,
|
|
93
|
+
mimeType: 'image/x-icon'
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
signature: [
|
|
97
|
+
0x00,
|
|
98
|
+
0x00,
|
|
99
|
+
0x02,
|
|
100
|
+
0x00
|
|
101
|
+
],
|
|
102
|
+
offset: 0,
|
|
103
|
+
mimeType: 'image/x-icon'
|
|
104
|
+
},
|
|
105
|
+
// Documents
|
|
106
|
+
{
|
|
107
|
+
signature: [
|
|
108
|
+
0x25,
|
|
109
|
+
0x50,
|
|
110
|
+
0x44,
|
|
111
|
+
0x46
|
|
112
|
+
],
|
|
113
|
+
offset: 0,
|
|
114
|
+
mimeType: 'application/pdf'
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
signature: [
|
|
118
|
+
0x50,
|
|
119
|
+
0x4b,
|
|
120
|
+
0x03,
|
|
121
|
+
0x04
|
|
122
|
+
],
|
|
123
|
+
offset: 0,
|
|
124
|
+
mimeType: 'application/zip'
|
|
125
|
+
},
|
|
126
|
+
// Audio
|
|
127
|
+
{
|
|
128
|
+
signature: [
|
|
129
|
+
0x49,
|
|
130
|
+
0x44,
|
|
131
|
+
0x33
|
|
132
|
+
],
|
|
133
|
+
offset: 0,
|
|
134
|
+
mimeType: 'audio/mpeg'
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
signature: [
|
|
138
|
+
0xff,
|
|
139
|
+
0xfb
|
|
140
|
+
],
|
|
141
|
+
offset: 0,
|
|
142
|
+
mimeType: 'audio/mpeg'
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
signature: [
|
|
146
|
+
0xff,
|
|
147
|
+
0xfa
|
|
148
|
+
],
|
|
149
|
+
offset: 0,
|
|
150
|
+
mimeType: 'audio/mpeg'
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
signature: [
|
|
154
|
+
0x4f,
|
|
155
|
+
0x67,
|
|
156
|
+
0x67,
|
|
157
|
+
0x53
|
|
158
|
+
],
|
|
159
|
+
offset: 0,
|
|
160
|
+
mimeType: 'audio/ogg'
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
signature: [
|
|
164
|
+
0x66,
|
|
165
|
+
0x4c,
|
|
166
|
+
0x61,
|
|
167
|
+
0x43
|
|
168
|
+
],
|
|
169
|
+
offset: 0,
|
|
170
|
+
mimeType: 'audio/flac'
|
|
171
|
+
},
|
|
172
|
+
// Video
|
|
173
|
+
{
|
|
174
|
+
signature: [
|
|
175
|
+
0x00,
|
|
176
|
+
0x00,
|
|
177
|
+
0x00,
|
|
178
|
+
0x1c,
|
|
179
|
+
0x66,
|
|
180
|
+
0x74,
|
|
181
|
+
0x79,
|
|
182
|
+
0x70
|
|
183
|
+
],
|
|
184
|
+
offset: 0,
|
|
185
|
+
mimeType: 'video/mp4'
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
signature: [
|
|
189
|
+
0x00,
|
|
190
|
+
0x00,
|
|
191
|
+
0x00,
|
|
192
|
+
0x20,
|
|
193
|
+
0x66,
|
|
194
|
+
0x74,
|
|
195
|
+
0x79,
|
|
196
|
+
0x70
|
|
197
|
+
],
|
|
198
|
+
offset: 0,
|
|
199
|
+
mimeType: 'video/mp4'
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
signature: [
|
|
203
|
+
0x1a,
|
|
204
|
+
0x45,
|
|
205
|
+
0xdf,
|
|
206
|
+
0xa3
|
|
207
|
+
],
|
|
208
|
+
offset: 0,
|
|
209
|
+
mimeType: 'video/webm'
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
signature: [
|
|
213
|
+
0x52,
|
|
214
|
+
0x49,
|
|
215
|
+
0x46,
|
|
216
|
+
0x46
|
|
217
|
+
],
|
|
218
|
+
offset: 0,
|
|
219
|
+
mimeType: 'video/avi'
|
|
220
|
+
},
|
|
221
|
+
// Archives
|
|
222
|
+
{
|
|
223
|
+
signature: [
|
|
224
|
+
0x1f,
|
|
225
|
+
0x8b
|
|
226
|
+
],
|
|
227
|
+
offset: 0,
|
|
228
|
+
mimeType: 'application/gzip'
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
signature: [
|
|
232
|
+
0x37,
|
|
233
|
+
0x7a,
|
|
234
|
+
0xbc,
|
|
235
|
+
0xaf,
|
|
236
|
+
0x27,
|
|
237
|
+
0x1c
|
|
238
|
+
],
|
|
239
|
+
offset: 0,
|
|
240
|
+
mimeType: 'application/x-7z-compressed'
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
signature: [
|
|
244
|
+
0x52,
|
|
245
|
+
0x61,
|
|
246
|
+
0x72,
|
|
247
|
+
0x21,
|
|
248
|
+
0x1a,
|
|
249
|
+
0x07
|
|
250
|
+
],
|
|
251
|
+
offset: 0,
|
|
252
|
+
mimeType: 'application/x-rar-compressed'
|
|
253
|
+
}
|
|
254
|
+
];
|
|
255
|
+
/**
|
|
256
|
+
* MIME type aliases - types that are equivalent.
|
|
257
|
+
*/ const MIME_ALIASES = {
|
|
258
|
+
'image/jpeg': [
|
|
259
|
+
'image/jpg'
|
|
260
|
+
],
|
|
261
|
+
'image/jpg': [
|
|
262
|
+
'image/jpeg'
|
|
263
|
+
],
|
|
264
|
+
'video/mp4': [
|
|
265
|
+
'video/quicktime'
|
|
266
|
+
],
|
|
267
|
+
'application/zip': [
|
|
268
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
269
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
270
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
|
271
|
+
]
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* File types that are text-based and don't have magic bytes.
|
|
275
|
+
* SECURITY NOTE: Dangerous types (HTML, JS, SVG) are excluded as they can contain scripts.
|
|
276
|
+
* These types require explicit allowlisting and additional content scanning.
|
|
277
|
+
*/ const TEXT_BASED_TYPES = [
|
|
278
|
+
'text/plain',
|
|
279
|
+
'text/csv',
|
|
280
|
+
'text/markdown',
|
|
281
|
+
'application/json',
|
|
282
|
+
'application/xml',
|
|
283
|
+
'application/typescript',
|
|
284
|
+
'text/css'
|
|
285
|
+
];
|
|
286
|
+
/**
|
|
287
|
+
* Dangerous text-based types that can execute scripts.
|
|
288
|
+
* These bypass magic-bytes validation but require explicit allowlisting.
|
|
289
|
+
*/ const DANGEROUS_TEXT_TYPES = [
|
|
290
|
+
'text/html',
|
|
291
|
+
'application/javascript',
|
|
292
|
+
'text/javascript',
|
|
293
|
+
'image/svg+xml',
|
|
294
|
+
'application/xhtml+xml'
|
|
295
|
+
];
|
|
296
|
+
/**
|
|
297
|
+
* Utility class for validating file content using magic bytes.
|
|
298
|
+
* Prevents file type spoofing by checking actual file content.
|
|
299
|
+
*/ export class FileValidator {
|
|
300
|
+
/**
|
|
301
|
+
* Detect file type from buffer using magic bytes.
|
|
302
|
+
* @param buffer - File buffer to analyze
|
|
303
|
+
* @returns Detected MIME type or null if unknown
|
|
304
|
+
*/ static detectFileType(buffer) {
|
|
305
|
+
for (const { signature, offset, mimeType } of MAGIC_BYTES){
|
|
306
|
+
if (buffer.length < offset + signature.length) continue;
|
|
307
|
+
let matches = true;
|
|
308
|
+
for(let i = 0; i < signature.length; i++){
|
|
309
|
+
if (buffer[offset + i] !== signature[i]) {
|
|
310
|
+
matches = false;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (matches) {
|
|
315
|
+
return mimeType;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Check if a MIME type is text-based (doesn't have magic bytes).
|
|
322
|
+
*/ static isTextBasedType(mimeType) {
|
|
323
|
+
return TEXT_BASED_TYPES.some((t)=>mimeType.startsWith(t) || mimeType === t);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Check if a MIME type is a dangerous text type that can execute scripts.
|
|
327
|
+
*/ static isDangerousTextType(mimeType) {
|
|
328
|
+
return DANGEROUS_TEXT_TYPES.some((t)=>mimeType === t);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Check if two MIME types are compatible (exact match or aliases).
|
|
332
|
+
*/ static mimeTypesMatch(detected, declared) {
|
|
333
|
+
// Exact match
|
|
334
|
+
if (detected === declared) return true;
|
|
335
|
+
// Check aliases
|
|
336
|
+
const aliases = MIME_ALIASES[detected];
|
|
337
|
+
if (aliases?.includes(declared)) return true;
|
|
338
|
+
// Check reverse aliases
|
|
339
|
+
const reverseAliases = MIME_ALIASES[declared];
|
|
340
|
+
if (reverseAliases?.includes(detected)) return true;
|
|
341
|
+
// Check if both are in same category (e.g., both images)
|
|
342
|
+
const detectedCategory = detected.split('/')[0];
|
|
343
|
+
const declaredCategory = declared.split('/')[0];
|
|
344
|
+
// For ZIP-based formats, allow any ZIP-detected file if declared is a ZIP variant
|
|
345
|
+
if (detected === 'application/zip') {
|
|
346
|
+
const zipVariants = [
|
|
347
|
+
'application/vnd.openxmlformats-officedocument',
|
|
348
|
+
'application/x-zip',
|
|
349
|
+
'application/x-compressed'
|
|
350
|
+
];
|
|
351
|
+
if (zipVariants.some((v)=>declared.startsWith(v))) {
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return detectedCategory === declaredCategory;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Check if a MIME type is in the allowed list.
|
|
359
|
+
*/ static isTypeAllowed(mimeType, allowedTypes) {
|
|
360
|
+
// Wildcard allows all
|
|
361
|
+
if (allowedTypes.includes('*/*')) return true;
|
|
362
|
+
return allowedTypes.some((allowed)=>{
|
|
363
|
+
// Category wildcard (e.g., "image/*")
|
|
364
|
+
if (allowed.endsWith('/*')) {
|
|
365
|
+
const category = allowed.slice(0, -2);
|
|
366
|
+
return mimeType.startsWith(category);
|
|
367
|
+
}
|
|
368
|
+
return allowed === mimeType;
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Validate file content matches declared MIME type using magic bytes.
|
|
373
|
+
* @param buffer - File buffer
|
|
374
|
+
* @param declaredMimeType - MIME type declared by client
|
|
375
|
+
* @param allowedTypes - List of allowed MIME types/patterns
|
|
376
|
+
* @returns Validation result
|
|
377
|
+
*/ static validateFileContent(buffer, declaredMimeType, allowedTypes = [
|
|
378
|
+
'*/*'
|
|
379
|
+
]) {
|
|
380
|
+
try {
|
|
381
|
+
// Detect actual file type from magic bytes
|
|
382
|
+
const detectedType = this.detectFileType(buffer);
|
|
383
|
+
// If no type detected, check if it's a text-based type
|
|
384
|
+
if (!detectedType) {
|
|
385
|
+
// Check for dangerous text types first (HTML, JS, SVG)
|
|
386
|
+
if (this.isDangerousTextType(declaredMimeType)) {
|
|
387
|
+
// Only allow dangerous types if explicitly in allowedTypes (not via wildcard)
|
|
388
|
+
const explicitlyAllowed = allowedTypes.some((t)=>t === declaredMimeType && t !== '*/*' && !t.endsWith('/*'));
|
|
389
|
+
if (!explicitlyAllowed) {
|
|
390
|
+
this.logger.warn(`Blocked dangerous file type: ${declaredMimeType} - requires explicit allowlisting`);
|
|
391
|
+
return {
|
|
392
|
+
valid: false,
|
|
393
|
+
detectedType: declaredMimeType,
|
|
394
|
+
declaredType: declaredMimeType,
|
|
395
|
+
message: `File type "${declaredMimeType}" is potentially dangerous and not explicitly allowed`
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
this.logger.warn(`Allowing explicitly permitted dangerous file type: ${declaredMimeType}`);
|
|
399
|
+
}
|
|
400
|
+
if (this.isTextBasedType(declaredMimeType)) {
|
|
401
|
+
// Safe text-based files don't have magic bytes, trust the declared type
|
|
402
|
+
const isAllowed = this.isTypeAllowed(declaredMimeType, allowedTypes);
|
|
403
|
+
return {
|
|
404
|
+
valid: isAllowed,
|
|
405
|
+
detectedType: declaredMimeType,
|
|
406
|
+
declaredType: declaredMimeType,
|
|
407
|
+
message: isAllowed ? undefined : `File type "${declaredMimeType}" is not allowed`
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
// For binary files without recognized signatures, be cautious
|
|
411
|
+
this.logger.warn(`Unable to detect file type for declared type: ${declaredMimeType}`);
|
|
412
|
+
return {
|
|
413
|
+
valid: false,
|
|
414
|
+
declaredType: declaredMimeType,
|
|
415
|
+
message: 'Unable to verify file type. File may be corrupted or unsupported.'
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
// Check if detected type matches declared type
|
|
419
|
+
if (!this.mimeTypesMatch(detectedType, declaredMimeType)) {
|
|
420
|
+
this.logger.warn(`MIME type mismatch: declared=${declaredMimeType}, detected=${detectedType}`);
|
|
421
|
+
return {
|
|
422
|
+
valid: false,
|
|
423
|
+
detectedType,
|
|
424
|
+
declaredType: declaredMimeType,
|
|
425
|
+
message: `File content does not match declared type. Detected: ${detectedType}, Declared: ${declaredMimeType}`
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
// Check if detected type is allowed
|
|
429
|
+
if (!this.isTypeAllowed(detectedType, allowedTypes)) {
|
|
430
|
+
return {
|
|
431
|
+
valid: false,
|
|
432
|
+
detectedType,
|
|
433
|
+
declaredType: declaredMimeType,
|
|
434
|
+
message: `File type "${detectedType}" is not allowed`
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
valid: true,
|
|
439
|
+
detectedType,
|
|
440
|
+
declaredType: declaredMimeType
|
|
441
|
+
};
|
|
442
|
+
} catch (error) {
|
|
443
|
+
this.logger.error('File validation error:', error);
|
|
444
|
+
return {
|
|
445
|
+
valid: false,
|
|
446
|
+
message: 'File validation failed'
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Sanitize filename to prevent path traversal and special character issues.
|
|
452
|
+
* @param filename - Original filename
|
|
453
|
+
* @returns Sanitized filename
|
|
454
|
+
*/ static sanitizeFilename(filename) {
|
|
455
|
+
return filename// Remove path components (prevent traversal)
|
|
456
|
+
.replace(/^.*[\\\/]/, '')// Remove null bytes
|
|
457
|
+
.replace(/\0/g, '')// Replace multiple dots with single
|
|
458
|
+
.replace(/\.{2,}/g, '.')// Remove special characters except allowed ones
|
|
459
|
+
.replace(/[^a-zA-Z0-9._-]/g, '_')// Limit length
|
|
460
|
+
.substring(0, 255);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
_define_property(FileValidator, "logger", new Logger(FileValidator.name));
|
|
@@ -107,9 +107,7 @@ const sharp = sharpModule.default || sharpModule;
|
|
|
107
107
|
buffer: data,
|
|
108
108
|
format: `image/${targetFormat}`
|
|
109
109
|
};
|
|
110
|
-
} catch
|
|
111
|
-
// Fallback to original if processing fails
|
|
112
|
-
console.warn(`Image processing failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
110
|
+
} catch {
|
|
113
111
|
return {
|
|
114
112
|
buffer,
|
|
115
113
|
format: mimetype
|
|
@@ -3,12 +3,15 @@ export interface IFileManager extends IIdentity {
|
|
|
3
3
|
name?: string;
|
|
4
4
|
key?: string;
|
|
5
5
|
url?: string;
|
|
6
|
-
folder?:
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
folder?: {
|
|
7
|
+
id: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
} | null;
|
|
10
|
+
size?: string;
|
|
11
|
+
expiresAt?: number | null;
|
|
9
12
|
contentType?: string;
|
|
10
13
|
location?: string;
|
|
11
|
-
storageConfigId?: string;
|
|
14
|
+
storageConfigId?: string | null;
|
|
12
15
|
providerName?: string;
|
|
13
16
|
isPrivate?: boolean;
|
|
14
17
|
companyId?: string | null;
|
package/interfaces/index.d.ts
CHANGED
|
@@ -4,25 +4,7 @@ export interface IStorageConfig extends IIdentity {
|
|
|
4
4
|
name: string;
|
|
5
5
|
storage: FileLocationEnum;
|
|
6
6
|
config: Record<string, any>;
|
|
7
|
+
isActive: boolean;
|
|
8
|
+
isDefault: boolean;
|
|
7
9
|
companyId?: string | null;
|
|
8
10
|
}
|
|
9
|
-
export interface IAwsS3Config {
|
|
10
|
-
region: string;
|
|
11
|
-
bucket: string;
|
|
12
|
-
accessKeyId: string;
|
|
13
|
-
secretAccessKey: string;
|
|
14
|
-
endpoint?: string;
|
|
15
|
-
}
|
|
16
|
-
export interface IAzureBlobConfig {
|
|
17
|
-
accountName: string;
|
|
18
|
-
accountKey: string;
|
|
19
|
-
containerName: string;
|
|
20
|
-
}
|
|
21
|
-
export interface ISftpConfig {
|
|
22
|
-
host: string;
|
|
23
|
-
port: number;
|
|
24
|
-
username: string;
|
|
25
|
-
password?: string;
|
|
26
|
-
privateKey?: string;
|
|
27
|
-
basePath: string;
|
|
28
|
-
}
|