fix(core): Use class-validator with XSS check for survey answers (#10490)
Co-authored-by: Tomi Turtiainen <10324676+tomi@users.noreply.github.com>
This commit is contained in:
@@ -349,10 +349,40 @@ describe('MeController', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw BadRequestError on XSS attempt', async () => {
|
||||
const req = mock<MeRequest.SurveyAnswers>({
|
||||
body: { 'test-answer': '<script>alert("XSS")</script>' },
|
||||
});
|
||||
test.each([
|
||||
'automationGoalDevops',
|
||||
'companyIndustryExtended',
|
||||
'otherCompanyIndustryExtended',
|
||||
'automationGoalSm',
|
||||
'usageModes',
|
||||
])('should throw BadRequestError on XSS attempt for an array field %s', async (fieldName) => {
|
||||
const req = mock<MeRequest.SurveyAnswers>();
|
||||
req.body = {
|
||||
version: 'v4',
|
||||
personalization_survey_n8n_version: '1.0.0',
|
||||
personalization_survey_submitted_at: new Date().toISOString(),
|
||||
[fieldName]: ['<script>alert("XSS")</script>'],
|
||||
};
|
||||
|
||||
await expect(controller.storeSurveyAnswers(req)).rejects.toThrowError(BadRequestError);
|
||||
});
|
||||
|
||||
test.each([
|
||||
'automationGoalDevopsOther',
|
||||
'companySize',
|
||||
'companyType',
|
||||
'automationGoalSmOther',
|
||||
'roleOther',
|
||||
'reportedSource',
|
||||
'reportedSourceOther',
|
||||
])('should throw BadRequestError on XSS attempt for a string field %s', async (fieldName) => {
|
||||
const req = mock<MeRequest.SurveyAnswers>();
|
||||
req.body = {
|
||||
version: 'v4',
|
||||
personalization_survey_n8n_version: '1.0.0',
|
||||
personalization_survey_submitted_at: new Date().toISOString(),
|
||||
[fieldName]: '<script>alert("XSS")</script>',
|
||||
};
|
||||
|
||||
await expect(controller.storeSurveyAnswers(req)).rejects.toThrowError(BadRequestError);
|
||||
});
|
||||
|
||||
@@ -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, validateRecordNoXss } from '@/GenericHelpers';
|
||||
import { validateEntity } from '@/GenericHelpers';
|
||||
import type { User } from '@db/entities/User';
|
||||
import {
|
||||
AuthenticatedRequest,
|
||||
@@ -25,6 +25,7 @@ import { isApiEnabled } from '@/PublicApi';
|
||||
import { EventService } from '@/events/event.service';
|
||||
import { MfaService } from '@/Mfa/mfa.service';
|
||||
import { InvalidMfaCodeError } from '@/errors/response-errors/invalid-mfa-code.error';
|
||||
import { PersonalizationSurveyAnswersV4 } from './survey-answers.dto';
|
||||
|
||||
export const API_KEY_PREFIX = 'n8n_api_';
|
||||
|
||||
@@ -195,7 +196,7 @@ export class MeController {
|
||||
|
||||
if (!personalizationAnswers) {
|
||||
this.logger.debug(
|
||||
'Request to store user personalization survey failed because of empty payload',
|
||||
'Request to store user personalization survey failed because of undefined payload',
|
||||
{
|
||||
userId: req.user.id,
|
||||
},
|
||||
@@ -203,12 +204,18 @@ export class MeController {
|
||||
throw new BadRequestError('Personalization answers are mandatory');
|
||||
}
|
||||
|
||||
await validateRecordNoXss(personalizationAnswers);
|
||||
const validatedAnswers = plainToInstance(
|
||||
PersonalizationSurveyAnswersV4,
|
||||
personalizationAnswers,
|
||||
{ excludeExtraneousValues: true },
|
||||
);
|
||||
|
||||
await validateEntity(validatedAnswers);
|
||||
|
||||
await this.userRepository.save(
|
||||
{
|
||||
id: req.user.id,
|
||||
personalizationAnswers,
|
||||
personalizationAnswers: validatedAnswers,
|
||||
},
|
||||
{ transaction: false },
|
||||
);
|
||||
@@ -217,7 +224,7 @@ export class MeController {
|
||||
|
||||
this.eventService.emit('user-submitted-personalization-survey', {
|
||||
userId: req.user.id,
|
||||
answers: personalizationAnswers,
|
||||
answers: validatedAnswers,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
||||
109
packages/cli/src/controllers/survey-answers.dto.ts
Normal file
109
packages/cli/src/controllers/survey-answers.dto.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { NoXss } from '@/validators/no-xss.validator';
|
||||
import { Expose } from 'class-transformer';
|
||||
import { IsString, IsArray, IsOptional, IsEmail, IsEnum } from 'class-validator';
|
||||
import type { IPersonalizationSurveyAnswersV4 } from 'n8n-workflow';
|
||||
|
||||
export class PersonalizationSurveyAnswersV4 implements IPersonalizationSurveyAnswersV4 {
|
||||
@NoXss()
|
||||
@Expose()
|
||||
@IsEnum(['v4'])
|
||||
version: 'v4';
|
||||
|
||||
@NoXss()
|
||||
@Expose()
|
||||
@IsString()
|
||||
personalization_survey_submitted_at: string;
|
||||
|
||||
@NoXss()
|
||||
@Expose()
|
||||
@IsString()
|
||||
personalization_survey_n8n_version: string;
|
||||
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@NoXss({ each: true })
|
||||
@IsString({ each: true })
|
||||
automationGoalDevops?: string[] | null;
|
||||
|
||||
@NoXss()
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
automationGoalDevopsOther?: string | null;
|
||||
|
||||
@NoXss({ each: true })
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
companyIndustryExtended?: string[] | null;
|
||||
|
||||
@NoXss({ each: true })
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsString({ each: true })
|
||||
otherCompanyIndustryExtended?: string[] | null;
|
||||
|
||||
@NoXss()
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
companySize?: string | null;
|
||||
|
||||
@NoXss()
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
companyType?: string | null;
|
||||
|
||||
@NoXss({ each: true })
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
automationGoalSm?: string[] | null;
|
||||
|
||||
@NoXss()
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
automationGoalSmOther?: string | null;
|
||||
|
||||
@NoXss({ each: true })
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
usageModes?: string[] | null;
|
||||
|
||||
@NoXss()
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string | null;
|
||||
|
||||
@NoXss()
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
role?: string | null;
|
||||
|
||||
@NoXss()
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
roleOther?: string | null;
|
||||
|
||||
@NoXss()
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
reportedSource?: string | null;
|
||||
|
||||
@NoXss()
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
reportedSourceOther?: string | null;
|
||||
}
|
||||
Reference in New Issue
Block a user