@solidxai/core 0.1.17-beta.0 → 0.1.17-beta.2
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 +1330 -0
- package/CLAUDE.md +26 -0
- package/dist/constants/media-file-types.d.ts +2 -2
- package/dist/constants/media-file-types.d.ts.map +1 -1
- package/dist/constants/media-file-types.js +24 -10
- package/dist/constants/media-file-types.js.map +1 -1
- package/dist/controllers/media.controller.d.ts.map +1 -1
- package/dist/controllers/media.controller.js +7 -2
- package/dist/controllers/media.controller.js.map +1 -1
- package/dist/controllers/user.controller.d.ts +1 -0
- package/dist/controllers/user.controller.d.ts.map +1 -1
- package/dist/controllers/user.controller.js +18 -2
- package/dist/controllers/user.controller.js.map +1 -1
- package/dist/services/authentication.service.d.ts +2 -4
- package/dist/services/authentication.service.d.ts.map +1 -1
- package/dist/services/authentication.service.js +8 -16
- package/dist/services/authentication.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 +12 -0
- package/dist/services/settings/default-settings-provider.service.js.map +1 -1
- package/dist/services/user.service.d.ts +2 -0
- package/dist/services/user.service.d.ts.map +1 -1
- package/dist/services/user.service.js +58 -44
- package/dist/services/user.service.js.map +1 -1
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +5 -1
- package/dist/solid-core.module.js.map +1 -1
- package/package.json +1 -1
- package/src/constants/media-file-types.ts +76 -24
- package/src/controllers/media.controller.ts +18 -3
- package/src/controllers/user.controller.ts +37 -3
- package/src/services/authentication.service.ts +21 -19
- package/src/services/settings/default-settings-provider.service.ts +12 -0
- package/src/services/user.service.ts +101 -47
- package/src/solid-core.module.ts +12 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Controller, Post, Body, Param, UploadedFiles, UseInterceptors, Put, Get, Query, Delete, Patch } from '@nestjs/common';
|
|
1
|
+
import { Controller, Post, Body, Param, UploadedFiles, UseInterceptors, Put, Get, Query, Delete, Patch, Logger } from '@nestjs/common';
|
|
2
2
|
import { AnyFilesInterceptor } from "@nestjs/platform-express";
|
|
3
|
-
import { ApiBearerAuth, ApiForbiddenResponse, ApiQuery, ApiTags } from '@nestjs/swagger';
|
|
3
|
+
import { ApiBearerAuth, ApiForbiddenResponse, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
|
4
4
|
import { UserService } from '../services/user.service';
|
|
5
5
|
import { CreateUserDto } from '../dtos/create-user.dto';
|
|
6
6
|
import { UpdateUserDto } from '../dtos/update-user.dto';
|
|
@@ -13,21 +13,55 @@ import { SolidRequestContextDto } from 'src/dtos/solid-request-context.dto';
|
|
|
13
13
|
import { UpdateUserProfileDto } from 'src/dtos/update-user-profile.dto';
|
|
14
14
|
|
|
15
15
|
@ApiTags('Solid Core')
|
|
16
|
-
@Controller('user') //FIXME: Change this to the model plural name
|
|
16
|
+
@Controller('user') //FIXME: Change this to the model plural name
|
|
17
17
|
export class UserController {
|
|
18
|
+
private readonly logger = new Logger(UserController.name);
|
|
19
|
+
|
|
18
20
|
constructor(private readonly service: UserService) { }
|
|
19
21
|
|
|
22
|
+
/**
|
|
23
|
+
* @deprecated Bypasses `AuthenticationService.signUp`, so it never runs
|
|
24
|
+
* `initializeRolesForNewUser` - a user created here gets no role at all, not even
|
|
25
|
+
* the "Internal User" baseline, and none of the SignupIntent dispatch introduced
|
|
26
|
+
* alongside the extension-user provider work applies. Use `POST /iam/register-private`
|
|
27
|
+
* (an authenticated core user, with roles you name) or the extension model's own
|
|
28
|
+
* generated create endpoint instead.
|
|
29
|
+
*
|
|
30
|
+
* A workspace-wide search (frontends and backend callers across every consuming app
|
|
31
|
+
* in this monorepo) found no caller as of 2026-09-11. Kept for one deprecation cycle
|
|
32
|
+
* in case an external caller exists outside this workspace - the warning below is
|
|
33
|
+
* how that would surface - before being removed in a future major version.
|
|
34
|
+
*/
|
|
35
|
+
@ApiOperation({
|
|
36
|
+
deprecated: true,
|
|
37
|
+
description: 'Deprecated: creates a user with no role assignment, not even the default. Use POST /iam/register-private, or the extension model\'s own create endpoint, instead. Scheduled for removal in a future major version.',
|
|
38
|
+
})
|
|
20
39
|
@ApiBearerAuth("jwt")
|
|
21
40
|
@Post()
|
|
22
41
|
@UseInterceptors(AnyFilesInterceptor())
|
|
23
42
|
create(@Body() createDto: CreateUserDto, @UploadedFiles() files: Array<Express.Multer.File>, @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) {
|
|
43
|
+
this.logger.warn(
|
|
44
|
+
`Deprecated endpoint POST /user was called (username="${createDto?.username}"). ` +
|
|
45
|
+
'This bypasses role assignment entirely and is scheduled for removal. ' +
|
|
46
|
+
'Use POST /iam/register-private or the extension model\'s own create endpoint.',
|
|
47
|
+
);
|
|
24
48
|
return this.service.create(createDto, files, solidRequestContext);
|
|
25
49
|
}
|
|
26
50
|
|
|
51
|
+
/** @deprecated See {@link create} - same gap, applied per row. */
|
|
52
|
+
@ApiOperation({
|
|
53
|
+
deprecated: true,
|
|
54
|
+
description: 'Deprecated: creates users with no role assignment, not even the default. Use the extension model\'s own bulk-create endpoint, or repeated calls to POST /iam/register-private, instead. Scheduled for removal in a future major version.',
|
|
55
|
+
})
|
|
27
56
|
@ApiBearerAuth("jwt")
|
|
28
57
|
@Post('/bulk')
|
|
29
58
|
@UseInterceptors(AnyFilesInterceptor())
|
|
30
59
|
insertMany(@Body() createDtos: CreateUserDto[], @UploadedFiles() filesArray: Express.Multer.File[][] = [], @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) {
|
|
60
|
+
this.logger.warn(
|
|
61
|
+
`Deprecated endpoint POST /user/bulk was called (count=${createDtos?.length ?? 0}, ` +
|
|
62
|
+
`usernames=${createDtos?.map(dto => dto.username).join(', ')}). ` +
|
|
63
|
+
'This bypasses role assignment entirely and is scheduled for removal.',
|
|
64
|
+
);
|
|
31
65
|
return this.service.insertMany(createDtos, filesArray, solidRequestContext);
|
|
32
66
|
}
|
|
33
67
|
|
|
@@ -55,7 +55,6 @@ import { MetadataValidationService } from "./metadata-validation.service";
|
|
|
55
55
|
import { UserService } from "./user.service";
|
|
56
56
|
import { SmsFactory } from "src/factories/sms.factory";
|
|
57
57
|
import { WhatsAppFactory } from "src/factories/whatsapp.factory";
|
|
58
|
-
import { SolidRegistry } from "src/helpers/solid-registry";
|
|
59
58
|
|
|
60
59
|
enum LoginProvider {
|
|
61
60
|
LOCAL = "local",
|
|
@@ -150,7 +149,6 @@ export class AuthenticationService {
|
|
|
150
149
|
|
|
151
150
|
@InjectDataSource()
|
|
152
151
|
private readonly dataSource: DataSource,
|
|
153
|
-
private readonly solidRegistry: SolidRegistry,
|
|
154
152
|
) {
|
|
155
153
|
// this.mailService = this.mailServiceFactory.getMailService();
|
|
156
154
|
}
|
|
@@ -281,11 +279,12 @@ export class AuthenticationService {
|
|
|
281
279
|
// roles(), which is where a provider validates its discriminator, and
|
|
282
280
|
// CreateUserDto types roles as UpdateRoleMetadataDto[] where performSignUp
|
|
283
281
|
// expects role-name strings. [] here falls through to `defaultRole`.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
)
|
|
282
|
+
//
|
|
283
|
+
// Delegated to UserService.resolveProviderRoles - the one place that calls
|
|
284
|
+
// provider.roles() - rather than looking up the registry here too, which is
|
|
285
|
+
// what UserService.resolveSelfRegistrationRoles (the OAuth callers' equivalent
|
|
286
|
+
// of this switch) also needs and previously duplicated.
|
|
287
|
+
return this.userService.resolveProviderRoles(dto);
|
|
289
288
|
|
|
290
289
|
case RolesSource.Caller:
|
|
291
290
|
return dto.roles ?? [];
|
|
@@ -730,7 +729,7 @@ export class AuthenticationService {
|
|
|
730
729
|
}
|
|
731
730
|
|
|
732
731
|
try {
|
|
733
|
-
const user = await this.
|
|
732
|
+
const user = await this.resolveUserForOtpRegistration(
|
|
734
733
|
existingUser,
|
|
735
734
|
signUpDto,
|
|
736
735
|
validationSource,
|
|
@@ -787,7 +786,7 @@ export class AuthenticationService {
|
|
|
787
786
|
);
|
|
788
787
|
}
|
|
789
788
|
|
|
790
|
-
private async
|
|
789
|
+
private async resolveUserForOtpRegistration(
|
|
791
790
|
existingUser: User,
|
|
792
791
|
signUpDto: OTPSignUpDto,
|
|
793
792
|
validationSource: string,
|
|
@@ -808,16 +807,19 @@ export class AuthenticationService {
|
|
|
808
807
|
await this.assignRegistrationOtp(validationSource, user);
|
|
809
808
|
await repo.save(user);
|
|
810
809
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
810
|
+
// The provider named none, or there is no provider: fall back to the configured
|
|
811
|
+
// default, matching what performSignUp does on the password paths. Built as a
|
|
812
|
+
// fresh array rather than mutating `roles` - RolesSource.Provider can return
|
|
813
|
+
// whatever reference the provider's roles() handed back.
|
|
814
|
+
const defaultRole = this.settingService.getConfigValue<SolidCoreSetting>("defaultRole");
|
|
815
|
+
const effectiveRoles = roles.length ? roles : [defaultRole].filter(Boolean);
|
|
816
|
+
|
|
817
|
+
// initializeRolesForNewUser always grants "Internal User" - the baseline every
|
|
818
|
+
// other signup path gets via handlePostSignup - which this branch previously
|
|
819
|
+
// skipped entirely by calling addRolesToUser/addRoleToUser directly. Without it
|
|
820
|
+
// an OTP-registered user could not even read their own User record: the
|
|
821
|
+
// "Internal User" role is what carries that security rule.
|
|
822
|
+
await this.userService.initializeRolesForNewUser(effectiveRoles, user);
|
|
821
823
|
return user;
|
|
822
824
|
}
|
|
823
825
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
SettingLevel,
|
|
8
8
|
} from "src/interfaces";
|
|
9
9
|
import { getDefaultThemeKey, getThemesByMode } from "src/theme/theme-registry";
|
|
10
|
+
import { DANGEROUS_EXTENSIONS } from "src/constants/media-file-types";
|
|
10
11
|
|
|
11
12
|
export const DEFAULT_MEDIA_UPLOAD_DIR = "media-uploads";
|
|
12
13
|
export const DEFAULT_MEDIA_FILE_STORAGE_DIR = "media-files-storage";
|
|
@@ -546,6 +547,17 @@ const getSolidCoreSettings = (isProd: boolean) =>
|
|
|
546
547
|
controlType: "numeric",
|
|
547
548
|
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
549
|
},
|
|
550
|
+
{
|
|
551
|
+
moduleName: "solid-core",
|
|
552
|
+
key: "dangerousExtensions",
|
|
553
|
+
value: [...DANGEROUS_EXTENSIONS].join(','),
|
|
554
|
+
level: SettingLevel.SystemAdminReadonly,
|
|
555
|
+
label: "Blocked Upload Extensions",
|
|
556
|
+
group: "storage-settings",
|
|
557
|
+
sortOrder: 46,
|
|
558
|
+
controlType: "shortText",
|
|
559
|
+
helpText: "File extensions rejected on every upload path, regardless of field configuration. Read-only here: it's applied once at process start. To change it, copy this value into AB_DANGEROUS_EXTENSIONS, edit it, and restart - note that variable REPLACES this list rather than adding to it, so any extension you leave out becomes an accepted upload type.",
|
|
560
|
+
},
|
|
549
561
|
|
|
550
562
|
// aws-s3-settings-provider.service.ts
|
|
551
563
|
{
|
|
@@ -145,6 +145,49 @@ export class UserService extends CRUDService<User> {
|
|
|
145
145
|
};
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Asks the registered extension-user provider for role names, treating "no provider
|
|
150
|
+
* registered" and "provider named none for this DTO" identically as [].
|
|
151
|
+
*
|
|
152
|
+
* The one place that actually calls `IExtensionUserCreationProvider.roles()`.
|
|
153
|
+
* `resolveSelfRegistrationRoles` below and `AuthenticationService.resolveSignupRoles`'s
|
|
154
|
+
* `Provider` branch both delegate here rather than each re-deriving "look up the
|
|
155
|
+
* registry, call roles(), default to []". Lives on `UserService` - not on
|
|
156
|
+
* `SolidRegistry`, which stays a pure lookup/storage layer for every provider kind it
|
|
157
|
+
* holds - because `AuthenticationService` already injects `UserService` directly
|
|
158
|
+
* (mirrors `buildSignupTarget`, and the DI cycle runs the other way: `UserService`
|
|
159
|
+
* cannot inject `AuthenticationService` back).
|
|
160
|
+
*/
|
|
161
|
+
resolveProviderRoles(dto: Record<string, any>): string[] {
|
|
162
|
+
return (
|
|
163
|
+
this.moduleRef
|
|
164
|
+
.get(SolidRegistry, { strict: false })
|
|
165
|
+
?.getExtensionUserCreationProvider()
|
|
166
|
+
?.roles(dto as any) ?? []
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Role names for a self-provisioning user, mirroring SignupIntent.SelfRegistration's
|
|
172
|
+
* policy on the password/OTP paths: ask the extension-user provider first, and fall
|
|
173
|
+
* back to the configured `defaultRole` when it names none - either because there is
|
|
174
|
+
* no provider, or the provider declines to name any for this DTO.
|
|
175
|
+
*
|
|
176
|
+
* Safe to call for OAuth even though `OauthUserDto` carries no discriminator field:
|
|
177
|
+
* a provider whose `roles()` requires one already needs to default it for public
|
|
178
|
+
* register/OTP to work at all (see the extending-users docs), and once it does,
|
|
179
|
+
* this resolves the same way automatically - there is no separate obligation OAuth
|
|
180
|
+
* places on providers beyond what self-registration already requires.
|
|
181
|
+
*/
|
|
182
|
+
private resolveSelfRegistrationRoles(dto: Record<string, any>): string[] {
|
|
183
|
+
const roles = this.resolveProviderRoles(dto);
|
|
184
|
+
if (roles.length) {
|
|
185
|
+
return roles;
|
|
186
|
+
}
|
|
187
|
+
const defaultRole = this.settingService.getConfigValue<SolidCoreSetting>("defaultRole");
|
|
188
|
+
return defaultRole ? [defaultRole] : [];
|
|
189
|
+
}
|
|
190
|
+
|
|
148
191
|
async findOneByEmail(email: string): Promise<User> {
|
|
149
192
|
return await this.repo.findOne({
|
|
150
193
|
where: {
|
|
@@ -315,21 +358,26 @@ export class UserService extends CRUDService<User> {
|
|
|
315
358
|
|
|
316
359
|
// if we are unable to find a user then we need to create one.
|
|
317
360
|
if (!user) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
user
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
361
|
+
// Social sign-in provisions an app user, so it is built and roled the same way
|
|
362
|
+
// public signup and OTP registration are: entity through the registered
|
|
363
|
+
// extension-user provider when there is one (see buildSignupTarget), roles
|
|
364
|
+
// through resolveSelfRegistrationRoles below, which asks that same provider
|
|
365
|
+
// first and falls back to `defaultRole`.
|
|
366
|
+
const { entity, repo } = await this.buildSignupTarget(oauthUserDto, true);
|
|
367
|
+
entity.username = oauthUserDto.email;
|
|
368
|
+
entity.email = oauthUserDto.email;
|
|
369
|
+
entity.fullName = oauthUserDto.name;
|
|
370
|
+
entity.lastLoginProvider = oauthUserDto.provider;
|
|
371
|
+
entity.accessCode = oauthUserDto.accessCode;
|
|
372
|
+
entity.googleAccessToken = oauthUserDto.accessToken;
|
|
373
|
+
entity.googleId = oauthUserDto.providerId;
|
|
374
|
+
entity.googleProfilePicture = oauthUserDto.picture;
|
|
375
|
+
|
|
376
|
+
const savedUser = await repo.save(entity);
|
|
329
377
|
|
|
330
378
|
// Initialize the user roles
|
|
331
379
|
await this.initializeRolesForNewUser(
|
|
332
|
-
|
|
380
|
+
this.resolveSelfRegistrationRoles(oauthUserDto),
|
|
333
381
|
savedUser,
|
|
334
382
|
);
|
|
335
383
|
}
|
|
@@ -390,20 +438,22 @@ export class UserService extends CRUDService<User> {
|
|
|
390
438
|
// facebookProviderFallback,
|
|
391
439
|
);
|
|
392
440
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
441
|
+
// See resolveUserOnOauthGoogle for why entity and roles both go through the
|
|
442
|
+
// extension-user provider first, falling back to a plain User / `defaultRole`.
|
|
443
|
+
const { entity, repo } = await this.buildSignupTarget(oauthUserDto, true);
|
|
444
|
+
entity.username = username;
|
|
445
|
+
entity.email = email;
|
|
446
|
+
entity.fullName = oauthUserDto.name;
|
|
447
|
+
entity.lastLoginProvider = oauthUserDto.provider;
|
|
448
|
+
entity.accessCode = oauthUserDto.accessCode;
|
|
449
|
+
entity.facebookAccessToken = oauthUserDto.accessToken;
|
|
450
|
+
entity.facebookId = oauthUserDto.providerId;
|
|
451
|
+
entity.facebookProfilePicture = oauthUserDto.picture;
|
|
402
452
|
|
|
403
|
-
const savedUser = await
|
|
453
|
+
const savedUser = await repo.save(entity);
|
|
404
454
|
|
|
405
455
|
await this.initializeRolesForNewUser(
|
|
406
|
-
|
|
456
|
+
this.resolveSelfRegistrationRoles(oauthUserDto),
|
|
407
457
|
savedUser,
|
|
408
458
|
);
|
|
409
459
|
return savedUser;
|
|
@@ -432,20 +482,22 @@ export class UserService extends CRUDService<User> {
|
|
|
432
482
|
});
|
|
433
483
|
|
|
434
484
|
if (!user) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
485
|
+
// See resolveUserOnOauthGoogle for why entity and roles both go through the
|
|
486
|
+
// extension-user provider first, falling back to a plain User / `defaultRole`.
|
|
487
|
+
const { entity, repo } = await this.buildSignupTarget(oauthUserDto, true);
|
|
488
|
+
entity.username = oauthUserDto.email;
|
|
489
|
+
entity.email = oauthUserDto.email;
|
|
490
|
+
entity.fullName = oauthUserDto.name;
|
|
491
|
+
entity.lastLoginProvider = oauthUserDto.provider;
|
|
492
|
+
entity.accessCode = oauthUserDto.accessCode;
|
|
493
|
+
entity.microsoftAccessToken = oauthUserDto.accessToken;
|
|
494
|
+
entity.microsoftId = oauthUserDto.providerId;
|
|
495
|
+
entity.microsoftProfilePicture = oauthUserDto.picture;
|
|
496
|
+
|
|
497
|
+
const savedUser = await repo.save(entity);
|
|
446
498
|
|
|
447
499
|
await this.initializeRolesForNewUser(
|
|
448
|
-
|
|
500
|
+
this.resolveSelfRegistrationRoles(oauthUserDto),
|
|
449
501
|
savedUser,
|
|
450
502
|
);
|
|
451
503
|
} else {
|
|
@@ -476,20 +528,22 @@ export class UserService extends CRUDService<User> {
|
|
|
476
528
|
});
|
|
477
529
|
|
|
478
530
|
if (!user) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
531
|
+
// See resolveUserOnOauthGoogle for why entity and roles both go through the
|
|
532
|
+
// extension-user provider first, falling back to a plain User / `defaultRole`.
|
|
533
|
+
const { entity, repo } = await this.buildSignupTarget(oauthUserDto, true);
|
|
534
|
+
entity.username = oauthUserDto.email;
|
|
535
|
+
entity.email = oauthUserDto.email;
|
|
536
|
+
entity.fullName = oauthUserDto.name;
|
|
537
|
+
entity.lastLoginProvider = oauthUserDto.provider;
|
|
538
|
+
entity.accessCode = oauthUserDto.accessCode;
|
|
539
|
+
entity.microsoftActiveDirectoryAccessToken = oauthUserDto.accessToken;
|
|
540
|
+
entity.microsoftActiveDirectoryId = oauthUserDto.providerId;
|
|
541
|
+
entity.microsoftActiveDirectoryProfilePicture = oauthUserDto.picture;
|
|
542
|
+
|
|
543
|
+
const savedUser = await repo.save(entity);
|
|
490
544
|
|
|
491
545
|
await this.initializeRolesForNewUser(
|
|
492
|
-
|
|
546
|
+
this.resolveSelfRegistrationRoles(oauthUserDto),
|
|
493
547
|
savedUser,
|
|
494
548
|
);
|
|
495
549
|
} else {
|
package/src/solid-core.module.ts
CHANGED
|
@@ -541,17 +541,27 @@ import { SwitchNode } from './services/workflow/nodes/switch.node';
|
|
|
541
541
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
542
542
|
|
|
543
543
|
// Only a small allowlist of media types is ever safe to render inline. Everything
|
|
544
|
-
// else (including
|
|
544
|
+
// else (including html and any other uploaded file) is forced to download
|
|
545
545
|
// rather than be displayed/executed by the browser, regardless of what mimetype or
|
|
546
546
|
// extension it was uploaded with.
|
|
547
547
|
// basename first: getLowercaseFileExtension takes a file name, not a path, so a
|
|
548
548
|
// directory containing a dot must not be mistaken for the extension. An
|
|
549
549
|
// extensionless file yields undefined, which is not inline-safe - so it correctly
|
|
550
550
|
// falls through to the forced-download branch.
|
|
551
|
-
|
|
551
|
+
const ext = getLowercaseFileExtension(basename(path)) ?? '';
|
|
552
|
+
if (!INLINE_SAFE_EXTENSIONS.has(ext)) {
|
|
552
553
|
res.setHeader("Content-Type", "application/octet-stream");
|
|
553
554
|
res.setHeader("Content-Disposition", "attachment");
|
|
554
555
|
}
|
|
556
|
+
|
|
557
|
+
// svg is only ever inline-safe (see INLINE_SAFE_EXTENSIONS) once it can also render
|
|
558
|
+
// as an active document (<script>, event handlers). <img>/CSS never execute it, but
|
|
559
|
+
// a direct navigation to this URL would - so pin it to a locked-down document: no
|
|
560
|
+
// script, no network, and `sandbox` gives it an opaque origin so even a parser bypass
|
|
561
|
+
// can't reach app cookies or storage.
|
|
562
|
+
if (ext === 'svg') {
|
|
563
|
+
res.setHeader("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; img-src data:; sandbox");
|
|
564
|
+
}
|
|
555
565
|
},
|
|
556
566
|
},
|
|
557
567
|
}),
|