fix(core): Prevent XSS in user update endpoints (#10338)

This commit is contained in:
Iván Ovejero 2024-08-12 10:06:15 +02:00 committed by GitHub
parent 4f392b5e3e
commit 78984986a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 69 additions and 4 deletions

View file

@ -1,10 +1,15 @@
import { validate } from 'class-validator';
import { ValidationError, validate } from 'class-validator';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
import type { TagEntity } from '@db/entities/TagEntity';
import type { User } from '@db/entities/User';
import type { UserRoleChangePayload, UserUpdatePayload } from '@/requests';
import type {
UserRoleChangePayload,
UserSettingsUpdatePayload,
UserUpdatePayload,
} from '@/requests';
import { BadRequestError } from './errors/response-errors/bad-request.error';
import { NoXss } from './databases/utils/customValidators';
export async function validateEntity(
entity:
@ -13,7 +18,8 @@ export async function validateEntity(
| TagEntity
| User
| UserUpdatePayload
| UserRoleChangePayload,
| UserRoleChangePayload
| UserSettingsUpdatePayload,
): Promise<void> {
const errors = await validate(entity);
@ -31,3 +37,37 @@ export async function validateEntity(
}
export const DEFAULT_EXECUTIONS_GET_ALL_LIMIT = 20;
class StringWithNoXss {
@NoXss()
value: string;
constructor(value: string) {
this.value = value;
}
}
// Temporary solution until we implement payload validation middleware
export async function validateRecordNoXss(record: Record<string, string>) {
const errors: ValidationError[] = [];
for (const [key, value] of Object.entries(record)) {
const stringWithNoXss = new StringWithNoXss(value);
const validationErrors = await validate(stringWithNoXss);
if (validationErrors.length > 0) {
const error = new ValidationError();
error.property = key;
error.constraints = validationErrors[0].constraints;
errors.push(error);
}
}
if (errors.length > 0) {
const errorMessages = errors
.map((error) => `${error.property}: ${Object.values(error.constraints ?? {}).join(', ')}`)
.join(' | ');
throw new BadRequestError(errorMessages);
}
}

View file

@ -225,6 +225,26 @@ describe('MeController', () => {
new BadRequestError('Personalization answers are mandatory'),
);
});
it('should throw BadRequestError on XSS attempt', async () => {
const req = mock<MeRequest.SurveyAnswers>({
body: { 'test-answer': '<script>alert("XSS")</script>' },
});
await expect(controller.storeSurveyAnswers(req)).rejects.toThrowError(BadRequestError);
});
});
describe('updateCurrentUserSettings', () => {
it('should throw BadRequestError on XSS attempt', async () => {
const req = mock<AuthenticatedRequest>({
body: {
userActivated: '<script>alert("XSS")</script>',
},
});
await expect(controller.updateCurrentUserSettings(req)).rejects.toThrowError(BadRequestError);
});
});
describe('API Key methods', () => {

View file

@ -6,7 +6,7 @@ import { randomBytes } from 'crypto';
import { AuthService } from '@/auth/auth.service';
import { Delete, Get, Patch, Post, RestController } from '@/decorators';
import { PasswordUtility } from '@/services/password.utility';
import { validateEntity } from '@/GenericHelpers';
import { validateEntity, validateRecordNoXss } from '@/GenericHelpers';
import type { User } from '@db/entities/User';
import {
AuthenticatedRequest,
@ -176,6 +176,8 @@ export class MeController {
throw new BadRequestError('Personalization answers are mandatory');
}
await validateRecordNoXss(personalizationAnswers);
await this.userRepository.save(
{
id: req.user.id,
@ -237,6 +239,9 @@ export class MeController {
const payload = plainToInstance(UserSettingsUpdatePayload, req.body, {
excludeExtraneousValues: true,
});
await validateEntity(payload);
const { id } = req.user;
await this.userService.updateSettings(id, payload);