@solidxai/core 0.1.13-beta.20 → 0.1.13-beta.21
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/CHANGELOG.md +590 -0
- package/CLAUDE.md +5 -6
- package/dist/constants/media-file-types.d.ts.map +1 -1
- package/dist/constants/media-file-types.js +1 -0
- package/dist/constants/media-file-types.js.map +1 -1
- package/dist/dtos/create-field-metadata.dto.d.ts +1 -0
- package/dist/dtos/create-field-metadata.dto.d.ts.map +1 -1
- package/dist/dtos/create-field-metadata.dto.js +12 -1
- package/dist/dtos/create-field-metadata.dto.js.map +1 -1
- package/dist/entities/field-metadata.entity.d.ts +1 -0
- package/dist/entities/field-metadata.entity.d.ts.map +1 -1
- package/dist/entities/field-metadata.entity.js +5 -1
- package/dist/entities/field-metadata.entity.js.map +1 -1
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts +6 -0
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts.map +1 -1
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js +54 -27
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js.map +1 -1
- package/dist/services/chatter-message.service.d.ts +1 -0
- package/dist/services/chatter-message.service.d.ts.map +1 -1
- package/dist/services/chatter-message.service.js +44 -12
- package/dist/services/chatter-message.service.js.map +1 -1
- package/dist/services/crud.service.d.ts.map +1 -1
- package/dist/services/crud.service.js +1 -0
- package/dist/services/crud.service.js.map +1 -1
- package/dist/services/field-metadata.service.d.ts.map +1 -1
- package/dist/services/field-metadata.service.js +2 -0
- package/dist/services/field-metadata.service.js.map +1 -1
- package/dist/services/media.service.d.ts.map +1 -1
- package/dist/services/media.service.js +1 -0
- package/dist/services/media.service.js.map +1 -1
- package/dist/services/module-metadata.service.d.ts.map +1 -1
- package/dist/services/module-metadata.service.js +7 -0
- package/dist/services/module-metadata.service.js.map +1 -1
- package/dist/services/setting.service.d.ts.map +1 -1
- package/dist/services/setting.service.js +4 -0
- package/dist/services/setting.service.js.map +1 -1
- package/dist/services/settings/default-settings-provider.service.d.ts +20 -0
- package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
- package/dist/services/settings/default-settings-provider.service.js +14 -0
- package/dist/services/settings/default-settings-provider.service.js.map +1 -1
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +6 -0
- package/dist/solid-core.module.js.map +1 -1
- package/package.json +1 -1
- package/src/constants/media-file-types.ts +1 -0
- package/src/dtos/create-field-metadata.dto.ts +14 -1
- package/src/entities/field-metadata.entity.ts +3 -0
- package/src/helpers/field-crud-managers/MediaFieldCrudManager.ts +73 -34
- package/src/services/chatter-message.service.ts +56 -14
- package/src/services/crud.service.ts +1 -0
- package/src/services/field-metadata.service.ts +2 -0
- package/src/services/media.service.ts +1 -0
- package/src/services/module-metadata.service.ts +7 -0
- package/src/services/setting.service.ts +5 -0
- package/src/services/settings/default-settings-provider.service.ts +14 -0
- package/src/solid-core.module.ts +13 -0
- package/postman/mpin.postman_collection.json +0 -1045
|
@@ -12,6 +12,7 @@ export interface MediaFieldOptions {
|
|
|
12
12
|
fieldName: string | undefined | null;
|
|
13
13
|
mediaMaxSizeKb: number | undefined | null;
|
|
14
14
|
mediaTypes: string[];
|
|
15
|
+
mediaAllowedExtensions?: string[] | null;
|
|
15
16
|
isUpdate: boolean | undefined | null;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -86,24 +87,28 @@ export class MediaFieldCrudManager implements FieldCrudManager {
|
|
|
86
87
|
return null;
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
// Extension is authoritative: a declared mimetype is fully attacker-controlled and
|
|
91
|
+
// proved insufficient on its own (a .msi uploaded as image/png previously resolved to
|
|
92
|
+
// 'image', since the mimetype match short-circuited before the extension was ever
|
|
93
|
+
// checked). The extension must independently resolve to a known-safe category before
|
|
94
|
+
// anything is accepted - not merely "not on the denylist".
|
|
89
95
|
const ext = getLowercaseFileExtension(getUploadedFileName(file));
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
92
|
-
return
|
|
96
|
+
const extType = ext ? EXT_TO_MEDIA_TYPE[ext] : undefined;
|
|
97
|
+
if (!extType) {
|
|
98
|
+
return null;
|
|
93
99
|
}
|
|
94
100
|
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return EXT_TO_MEDIA_TYPE[ext];
|
|
101
|
+
// The mimetype may only confirm or veto what the extension already established, never
|
|
102
|
+
// grant a category on its own. A mimetype that doesn't resolve to anything (e.g.
|
|
103
|
+
// application/octet-stream) has no opinion and is treated as agreeing.
|
|
104
|
+
const mt = (file.mimetype || '').toLowerCase().trim();
|
|
105
|
+
const mimeType = MIME_TO_MEDIA_TYPE[mt]
|
|
106
|
+
?? (mt.startsWith('image/') ? 'image' : mt.startsWith('audio/') ? 'audio' : mt.startsWith('video/') ? 'video' : undefined);
|
|
107
|
+
if (mimeType && mimeType !== extType) {
|
|
108
|
+
return null;
|
|
104
109
|
}
|
|
105
110
|
|
|
106
|
-
return
|
|
111
|
+
return extType;
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
validate(dto: any, files: Array<Express.Multer.File>): ValidationError[] {
|
|
@@ -112,18 +117,17 @@ export class MediaFieldCrudManager implements FieldCrudManager {
|
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
private applyValidations(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
const errors = this.validateDangerousFiles(fieldFiles);
|
|
120
|
+
// A dangerous upload is never acceptable, regardless of admin config, so this is the
|
|
121
|
+
// only check that sits outside validateCommon() and can't be skipped either way below.
|
|
122
|
+
const dangerousFileErrors = this.validateDangerousFiles(fieldFiles);
|
|
119
123
|
|
|
120
124
|
switch (this.options.type) {
|
|
121
125
|
case SolidMediaType.mediaSingle:
|
|
122
|
-
return [...
|
|
126
|
+
return [...dangerousFileErrors, ...this.validateMediaSingle(fieldFiles)];
|
|
123
127
|
case SolidMediaType.mediaMultiple:
|
|
124
|
-
return [...
|
|
128
|
+
return [...dangerousFileErrors, ...this.validateMediaMultiple(fieldFiles)];
|
|
125
129
|
default:
|
|
126
|
-
return
|
|
130
|
+
return dangerousFileErrors;
|
|
127
131
|
}
|
|
128
132
|
}
|
|
129
133
|
|
|
@@ -137,7 +141,18 @@ export class MediaFieldCrudManager implements FieldCrudManager {
|
|
|
137
141
|
}));
|
|
138
142
|
}
|
|
139
143
|
|
|
140
|
-
|
|
144
|
+
// Every optional, admin-configured, per-field check - no-ops when its setting is unset -
|
|
145
|
+
// shared between mediaSingle and mediaMultiple so neither can drift out of sync with it.
|
|
146
|
+
private validateCommon(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
|
|
147
|
+
return [
|
|
148
|
+
...this.validateRequired(fieldFiles),
|
|
149
|
+
...this.validateSize(fieldFiles),
|
|
150
|
+
...this.validateTypes(fieldFiles),
|
|
151
|
+
...this.validateAllowedExtensions(fieldFiles),
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private validateRequired(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
|
|
141
156
|
const errors: ValidationError[] = [];
|
|
142
157
|
if (!this.options.isUpdate && this.options.required && fieldFiles.length === 0) {
|
|
143
158
|
errors.push({
|
|
@@ -145,13 +160,11 @@ export class MediaFieldCrudManager implements FieldCrudManager {
|
|
|
145
160
|
error: `${this.options.fieldName} is required`
|
|
146
161
|
});
|
|
147
162
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
// validate size
|
|
163
|
+
return errors;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private validateSize(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
|
|
167
|
+
const errors: ValidationError[] = [];
|
|
155
168
|
if (this.options.mediaMaxSizeKb) {
|
|
156
169
|
for (let i = 0; i < fieldFiles.length; i++) {
|
|
157
170
|
const fieldFile = fieldFiles[i];
|
|
@@ -164,7 +177,11 @@ export class MediaFieldCrudManager implements FieldCrudManager {
|
|
|
164
177
|
}
|
|
165
178
|
}
|
|
166
179
|
}
|
|
167
|
-
|
|
180
|
+
return errors;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private validateTypes(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
|
|
184
|
+
const errors: ValidationError[] = [];
|
|
168
185
|
if (this.options.mediaTypes && this.options.mediaTypes.length > 0) {
|
|
169
186
|
const allowedFileTypes = this.options.mediaTypes as MediaType[];
|
|
170
187
|
|
|
@@ -182,21 +199,43 @@ export class MediaFieldCrudManager implements FieldCrudManager {
|
|
|
182
199
|
}
|
|
183
200
|
}
|
|
184
201
|
}
|
|
185
|
-
|
|
186
202
|
return errors;
|
|
187
203
|
}
|
|
188
204
|
|
|
189
|
-
private
|
|
190
|
-
const
|
|
191
|
-
if (!
|
|
205
|
+
private validateAllowedExtensions(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
|
|
206
|
+
const allowed = this.options.mediaAllowedExtensions;
|
|
207
|
+
if (!allowed || allowed.length === 0) {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const normalizedAllowed = new Set(allowed.map(ext => ext.toLowerCase().replace(/^\./, '')));
|
|
212
|
+
return fieldFiles
|
|
213
|
+
.filter(fieldFile => {
|
|
214
|
+
const ext = getLowercaseFileExtension(getUploadedFileName(fieldFile));
|
|
215
|
+
return !ext || !normalizedAllowed.has(ext);
|
|
216
|
+
})
|
|
217
|
+
.map(fieldFile => ({
|
|
218
|
+
field: this.options.fieldName,
|
|
219
|
+
error: `${this.options.fieldName} file type not allowed. ` +
|
|
220
|
+
`Allowed extensions: ${[...normalizedAllowed].join(', ')}.`
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private validateMediaSingle(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
|
|
225
|
+
const errors = this.validateCommon(fieldFiles);
|
|
226
|
+
if (fieldFiles.length > 1) {
|
|
192
227
|
errors.push({
|
|
193
228
|
field: this.options.fieldName,
|
|
194
|
-
error: `${this.options.fieldName}
|
|
229
|
+
error: `${this.options.fieldName} must be a single file`
|
|
195
230
|
});
|
|
196
231
|
}
|
|
197
232
|
return errors;
|
|
198
233
|
}
|
|
199
234
|
|
|
235
|
+
private validateMediaMultiple(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
|
|
236
|
+
return this.validateCommon(fieldFiles);
|
|
237
|
+
}
|
|
238
|
+
|
|
200
239
|
transformForCreate(dto: any): any {
|
|
201
240
|
return dto;
|
|
202
241
|
}
|
|
@@ -8,6 +8,7 @@ import { classify } from '@angular-devkit/core/src/utils/strings';
|
|
|
8
8
|
import { CHATTER_MESSAGE_STATUS, CHATTER_MESSAGE_SUBTYPE, CHATTER_MESSAGE_TYPE, CHATTER_MESSAGE_USER_FIELDS } from 'src/constants/chatter-message.constants';
|
|
9
9
|
import { isDangerousMediaFile } from 'src/constants/media-file-types';
|
|
10
10
|
import { ERROR_MESSAGES } from 'src/constants/error-messages';
|
|
11
|
+
import { MediaFieldCrudManager, SolidMediaType } from 'src/helpers/field-crud-managers/MediaFieldCrudManager';
|
|
11
12
|
import { PostChatterMessageDto } from 'src/dtos/post-chatter-message.dto';
|
|
12
13
|
import { UpdateChatterNoteMessageDto } from 'src/dtos/update-chatter-note-message.dto';
|
|
13
14
|
import { ModelMetadataHelperService } from 'src/helpers/model-metadata-helper.service';
|
|
@@ -196,6 +197,34 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Validates newly-uploaded attachments against each declared media field's real config
|
|
202
|
+
* (mediaTypes/mediaMaxSizeKb/mediaAllowedExtensions), grouped by fieldname - the same rules
|
|
203
|
+
* MediaFieldCrudManager already enforces everywhere else. Shared between postMessage and
|
|
204
|
+
* updateCustomNoteMessage so the two entry points can't drift apart; isUpdate is the only
|
|
205
|
+
* thing that differs between them.
|
|
206
|
+
*/
|
|
207
|
+
private validateChatterAttachmentsForFields(files: Express.Multer.File[], mediaFields: any[], isUpdate: boolean): void {
|
|
208
|
+
for (const mediaField of mediaFields) {
|
|
209
|
+
const media = files.filter(multerFile => multerFile.fieldname === mediaField.name);
|
|
210
|
+
if (media.length === 0) continue;
|
|
211
|
+
|
|
212
|
+
const validator = new MediaFieldCrudManager({
|
|
213
|
+
type: mediaField.type as SolidMediaType,
|
|
214
|
+
required: mediaField.required,
|
|
215
|
+
fieldName: mediaField.name,
|
|
216
|
+
mediaMaxSizeKb: mediaField.mediaMaxSizeKb,
|
|
217
|
+
mediaTypes: mediaField.mediaTypes,
|
|
218
|
+
mediaAllowedExtensions: mediaField.mediaAllowedExtensions,
|
|
219
|
+
isUpdate,
|
|
220
|
+
});
|
|
221
|
+
const validationErrors = validator.validate({}, media);
|
|
222
|
+
if (validationErrors.length > 0) {
|
|
223
|
+
throw new BadRequestException(validationErrors.map(error => error.error).join(', '));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
199
228
|
private async publishChatterMentionNotifications(message: ChatterMessage, model: any) {
|
|
200
229
|
if (message.messageType !== CHATTER_MESSAGE_TYPE.CUSTOM || message.messageSubType !== CHATTER_MESSAGE_SUBTYPE.NOTE) {
|
|
201
230
|
return;
|
|
@@ -329,6 +358,24 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
329
358
|
|
|
330
359
|
this.validateChatterMediaFiles(files);
|
|
331
360
|
|
|
361
|
+
let chatterModel: any = null;
|
|
362
|
+
let mediaFields: any[] = [];
|
|
363
|
+
if (removeAttachmentIds.length > 0 || hasNewFiles) {
|
|
364
|
+
chatterModel = await this.modelMetadataService.findOneBySingularName('chatterMessage', {
|
|
365
|
+
fields: {
|
|
366
|
+
model: true,
|
|
367
|
+
mediaStorageProvider: true,
|
|
368
|
+
},
|
|
369
|
+
module: true,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
mediaFields = chatterModel.fields.filter(field => field.type === 'mediaSingle' || field.type === 'mediaMultiple');
|
|
373
|
+
|
|
374
|
+
if (hasNewFiles) {
|
|
375
|
+
this.validateChatterAttachmentsForFields(files, mediaFields, true);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
332
379
|
if (hasMessageBody) {
|
|
333
380
|
message.messageBody = trimmedMessageBody;
|
|
334
381
|
}
|
|
@@ -345,15 +392,6 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
345
392
|
const savedMessage = await this.repo.save(message);
|
|
346
393
|
|
|
347
394
|
if (removeAttachmentIds.length > 0 || hasNewFiles) {
|
|
348
|
-
const model = await this.modelMetadataService.findOneBySingularName('chatterMessage', {
|
|
349
|
-
fields: {
|
|
350
|
-
model: true,
|
|
351
|
-
mediaStorageProvider: true,
|
|
352
|
-
},
|
|
353
|
-
module: true,
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
const mediaFields = model.fields.filter(field => field.type === 'mediaSingle' || field.type === 'mediaMultiple');
|
|
357
395
|
const attachmentFieldIds = mediaFields.map(field => field.id);
|
|
358
396
|
|
|
359
397
|
if (removeAttachmentIds.length > 0 && attachmentFieldIds.length > 0) {
|
|
@@ -361,7 +399,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
361
399
|
where: {
|
|
362
400
|
id: In(removeAttachmentIds),
|
|
363
401
|
entityId: savedMessage.id,
|
|
364
|
-
modelMetadata: { id:
|
|
402
|
+
modelMetadata: { id: chatterModel.id },
|
|
365
403
|
fieldMetadata: { id: In(attachmentFieldIds) },
|
|
366
404
|
},
|
|
367
405
|
relations: {
|
|
@@ -434,10 +472,9 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
434
472
|
|
|
435
473
|
this.stampMessageAuditFields(chatterMessage);
|
|
436
474
|
|
|
437
|
-
|
|
438
|
-
|
|
475
|
+
let mediaFields: any[] = [];
|
|
439
476
|
if (files && files.length > 0) {
|
|
440
|
-
const
|
|
477
|
+
const chatterModel = await this.modelMetadataService.findOneBySingularName('chatterMessage', {
|
|
441
478
|
fields: {
|
|
442
479
|
model: true,
|
|
443
480
|
mediaStorageProvider: true,
|
|
@@ -445,8 +482,13 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
445
482
|
module: true,
|
|
446
483
|
});
|
|
447
484
|
|
|
448
|
-
|
|
485
|
+
mediaFields = chatterModel.fields.filter(field => field.type === 'mediaSingle' || field.type === 'mediaMultiple');
|
|
486
|
+
this.validateChatterAttachmentsForFields(files, mediaFields, false);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const savedMessage = await this.repo.save(chatterMessage);
|
|
449
490
|
|
|
491
|
+
if (files && files.length > 0) {
|
|
450
492
|
for (const mediaField of mediaFields) {
|
|
451
493
|
const media = files.filter(multerFile => multerFile.fieldname === mediaField.name);
|
|
452
494
|
if (media.length > 0) {
|
|
@@ -446,6 +446,7 @@ private async prepareManyToManyAuditSnapshot(entity: T,id: number,modelSingularN
|
|
|
446
446
|
...commonOptions,
|
|
447
447
|
mediaMaxSizeKb: fieldMetadata.mediaMaxSizeKb,
|
|
448
448
|
mediaTypes: fieldMetadata.mediaTypes,
|
|
449
|
+
mediaAllowedExtensions: fieldMetadata.mediaAllowedExtensions,
|
|
449
450
|
type: fieldMetadata.type as unknown as SolidMediaType
|
|
450
451
|
};
|
|
451
452
|
return new MediaFieldCrudManager(options);
|
|
@@ -945,6 +945,7 @@ export class FieldMetadataService implements OnApplicationBootstrap {
|
|
|
945
945
|
"isSystem",
|
|
946
946
|
"mediaTypes",
|
|
947
947
|
"mediaMaxSizeKb",
|
|
948
|
+
"mediaAllowedExtensions",
|
|
948
949
|
"mediaStorageProviderId",
|
|
949
950
|
"required",
|
|
950
951
|
"unique",
|
|
@@ -966,6 +967,7 @@ export class FieldMetadataService implements OnApplicationBootstrap {
|
|
|
966
967
|
"isSystem",
|
|
967
968
|
"mediaTypes",
|
|
968
969
|
"mediaMaxSizeKb",
|
|
970
|
+
"mediaAllowedExtensions",
|
|
969
971
|
"mediaStorageProviderId",
|
|
970
972
|
"required",
|
|
971
973
|
"unique",
|
|
@@ -113,6 +113,7 @@ export class MediaService extends CRUDService<Media> {
|
|
|
113
113
|
fieldName: 'files',
|
|
114
114
|
mediaMaxSizeKb: createDto['fieldMetadata']?.mediaMaxSizeKb,
|
|
115
115
|
mediaTypes: createDto['fieldMetadata']?.mediaTypes || [],
|
|
116
|
+
mediaAllowedExtensions: createDto['fieldMetadata']?.mediaAllowedExtensions || [],
|
|
116
117
|
isUpdate: false,
|
|
117
118
|
});
|
|
118
119
|
const validationErrors = validator.validate(createDto, files);
|
|
@@ -13,6 +13,7 @@ import { ConfigService } from '@nestjs/config';
|
|
|
13
13
|
import * as fs from 'fs/promises'; // Use the Promise-based version of fs for async/await
|
|
14
14
|
import * as path from 'path'; // To handle file paths
|
|
15
15
|
import { ERROR_MESSAGES } from 'src/constants/error-messages';
|
|
16
|
+
import { isDangerousMediaFile } from 'src/constants/media-file-types';
|
|
16
17
|
import { DisallowInProduction } from 'src/decorators/disallow-in-production.decorator';
|
|
17
18
|
import { ModuleMetadataHelperService } from 'src/helpers/module-metadata-helper.service';
|
|
18
19
|
import { ModuleMetadataRepository } from 'src/repository/module-metadata.repository';
|
|
@@ -137,6 +138,9 @@ export class ModuleMetadataService {
|
|
|
137
138
|
|
|
138
139
|
async createInDB(manager: EntityManager, createDto: CreateModuleMetadataDto, files: Express.Multer.File[] = []) {
|
|
139
140
|
if (files.length > 0) {
|
|
141
|
+
if (isDangerousMediaFile(files[0])) {
|
|
142
|
+
throw new BadRequestException('Dangerous file types are not allowed for the menu icon.');
|
|
143
|
+
}
|
|
140
144
|
const fileStoragePath = this.getFullFilePathForDisk(this.getFileName(files[0]));
|
|
141
145
|
this.fileService.copy(files[0].path, fileStoragePath);
|
|
142
146
|
this.fileService.delete(files[0].path);
|
|
@@ -241,6 +245,9 @@ export class ModuleMetadataService {
|
|
|
241
245
|
throw new NotFoundException(ERROR_MESSAGES.MODULE_ID_NOT_FOUND(id));
|
|
242
246
|
}
|
|
243
247
|
if (files.length > 0) {
|
|
248
|
+
if (isDangerousMediaFile(files[0])) {
|
|
249
|
+
throw new BadRequestException('Dangerous file types are not allowed for the menu icon.');
|
|
250
|
+
}
|
|
244
251
|
|
|
245
252
|
const fileStoragePath = this.getFullFilePathForDisk(this.getFileName(files[0]));
|
|
246
253
|
this.fileService.copy(files[0].path, fileStoragePath);
|
|
@@ -17,6 +17,7 @@ import type { SolidCoreSetting } from './settings/default-settings-provider.serv
|
|
|
17
17
|
import { Logger } from '@nestjs/common';
|
|
18
18
|
import { EncryptionService } from './encryption.service';
|
|
19
19
|
import { sanitizeStoredMediaFileName } from './media-storage.utils';
|
|
20
|
+
import { isDangerousMediaFile } from 'src/constants/media-file-types';
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
@Injectable()
|
|
@@ -358,6 +359,10 @@ export class SettingService {
|
|
|
358
359
|
// Handle uploaded files
|
|
359
360
|
if (uploadedFiles?.length) {
|
|
360
361
|
for (const file of uploadedFiles) {
|
|
362
|
+
if (isDangerousMediaFile(file)) {
|
|
363
|
+
throw new BadRequestException('Dangerous file types are not allowed for settings.');
|
|
364
|
+
}
|
|
365
|
+
|
|
361
366
|
const settingKey = file.fieldname;
|
|
362
367
|
// originalname is attacker-controlled and feeds straight into a storage path.
|
|
363
368
|
const relativeFileName = `${file.filename}-${sanitizeStoredMediaFileName(file.originalname)}`;
|
|
@@ -532,6 +532,20 @@ const getSolidCoreSettings = (isProd: boolean) =>
|
|
|
532
532
|
],
|
|
533
533
|
helpText: "Default storage implementation used for file handling, such as local disk or S3.",
|
|
534
534
|
},
|
|
535
|
+
{
|
|
536
|
+
moduleName: "solid-core",
|
|
537
|
+
key: "maxMediaFileSizeMb",
|
|
538
|
+
value: (() => {
|
|
539
|
+
const parsedMb = Number(process.env.AB_MEDIA_MAX_FILE_SIZE_MB);
|
|
540
|
+
return Number.isFinite(parsedMb) && parsedMb > 0 ? parsedMb : 100;
|
|
541
|
+
})(),
|
|
542
|
+
level: SettingLevel.SystemAdminReadonly,
|
|
543
|
+
label: "Max Media File Size (MB)",
|
|
544
|
+
group: "storage-settings",
|
|
545
|
+
sortOrder: 45,
|
|
546
|
+
controlType: "numeric",
|
|
547
|
+
helpText: "Global upload size limit enforced by Multer under every per-field mediaMaxSizeKb restriction. Read-only here: it's applied once at process start, so changing AB_MEDIA_MAX_FILE_SIZE_MB requires an app restart to take effect - editing this in the admin UI would not do that.",
|
|
548
|
+
},
|
|
535
549
|
|
|
536
550
|
// aws-s3-settings-provider.service.ts
|
|
537
551
|
{
|
package/src/solid-core.module.ts
CHANGED
|
@@ -557,6 +557,19 @@ import { SwitchNode } from './services/workflow/nodes/switch.node';
|
|
|
557
557
|
imports: [ConfigModule],
|
|
558
558
|
useFactory: async (configService: ConfigService) => ({
|
|
559
559
|
dest: process.env.AB_MEDIA_UPLOAD_DIR ?? "media-uploads",
|
|
560
|
+
// A global floor under whatever per-field mediaMaxSizeKb restrictions apply, not a
|
|
561
|
+
// replacement for them - some upload paths (e.g. chatter attachments) had no size cap
|
|
562
|
+
// enforced anywhere before this. Must stay >= the largest legitimate per-field limit
|
|
563
|
+
// already configured (chatter's messageAttachments is 102400 KB / 100MB), since Multer
|
|
564
|
+
// enforces this before any of our own field-level checks ever run. Mirrored (read-only,
|
|
565
|
+
// for visibility only - see comment there for why) as the "maxMediaFileSizeMb" setting
|
|
566
|
+
// in default-settings-provider.service.ts; keep both parsing expressions in sync.
|
|
567
|
+
limits: {
|
|
568
|
+
fileSize: (() => {
|
|
569
|
+
const parsedMb = Number(process.env.AB_MEDIA_MAX_FILE_SIZE_MB);
|
|
570
|
+
return (Number.isFinite(parsedMb) && parsedMb > 0 ? parsedMb : 100) * 1024 * 1024;
|
|
571
|
+
})(),
|
|
572
|
+
},
|
|
560
573
|
}),
|
|
561
574
|
inject: [ConfigService],
|
|
562
575
|
}),
|