From ec7155252d25e336ae070114b39419a171f71939 Mon Sep 17 00:00:00 2001 From: Kyriakos Lesgidis Date: Mon, 29 Apr 2024 22:38:25 +0300 Subject: [PATCH 1/2] [PROD-40813] Allow any tag on safeHtml --- src/initializers/joi.ts | 19 ++++++++++--------- test/initializers/joi.test.ts | 4 ++++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/initializers/joi.ts b/src/initializers/joi.ts index a965646d..ec4d8eeb 100644 --- a/src/initializers/joi.ts +++ b/src/initializers/joi.ts @@ -2,7 +2,7 @@ import * as _Joi from 'joi'; import {isString} from 'lodash'; import * as sanitizeHtml from 'sanitize-html'; import {URL} from 'url'; -import { getLogger } from './log4js'; +import {getLogger} from './log4js'; const logger = getLogger('orka.initializers.joi'); @@ -11,7 +11,7 @@ export const clearNullByte = (val: string): string => (val && isString(val) ? va export const isValidHexColor = (val: string): boolean => /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(val); export const isOwnS3Path = (bucket: string, val: string): boolean => { try { - const { host, protocol, pathname } = new URL(val); + const {host, protocol, pathname} = new URL(val); const matchingProtocol = protocol === 'https:'; const s3Host = host === `${bucket}.s3.amazonaws.com`; const s3HostBucketInPath = @@ -19,9 +19,9 @@ export const isOwnS3Path = (bucket: string, val: string): boolean => { host.endsWith('.amazonaws.com') && pathname.startsWith(`/${bucket}/`); const s3HostContainsRegion = - host.startsWith(`${bucket}.s3`) && - host.endsWith('.amazonaws.com') && - host.split('.').length === 5; + host.startsWith(`${bucket}.s3`) && + host.endsWith('.amazonaws.com') && + host.split('.').length === 5; return matchingProtocol && (s3Host || s3HostBucketInPath || s3HostContainsRegion); } catch (e) { logger.error(`Failed to parse url: ${val}`, e); @@ -57,8 +57,8 @@ export const isExpiredUrl = (val: string): boolean => { }; type SafeHtml = _Joi.StringSchema & { - allowedTags: (tags: string[]) => SafeHtml; - allowedAttributes: (attributes: { [key: string]: string[] }) => SafeHtml; + allowedTags: (tags: string[] | false) => SafeHtml; + allowedAttributes: (attributes: { [key: string]: string[] } | false) => SafeHtml; }; type UrlInOwnS3 = _Joi.StringSchema & { @@ -283,7 +283,8 @@ const Joi: JoiWithExtensions = _Joi.extend( } }, prepare: (value, helpers) => { - const allowedTags = helpers.schema.$_getRule('allowedTags')?.args?.allowedTags || [ + if (value === null || value === undefined || typeof value !== 'string') return { value }; + const allowedTags = helpers.schema.$_getRule('allowedTags')?.args?.allowedTags ?? [ 'b', 'i', 'u', @@ -293,7 +294,7 @@ const Joi: JoiWithExtensions = _Joi.extend( 'a', 'font' ]; - const allowedAttributes = helpers.schema.$_getRule('allowedAttributes')?.args?.allowedAttributes || { + const allowedAttributes = helpers.schema.$_getRule('allowedAttributes')?.args?.allowedAttributes ?? { a: ['href', 'target', 'rel'], font: ['color'] }; diff --git a/test/initializers/joi.test.ts b/test/initializers/joi.test.ts index 29f4169c..865186a0 100644 --- a/test/initializers/joi.test.ts +++ b/test/initializers/joi.test.ts @@ -226,6 +226,10 @@ describe('joi extensions', function () { Joi.safeHtml().allowedAttributes({}).allowedTags([]) .validate('

banana

').value.should.equal('banana'); }); + it('allow any attribute', function () { + Joi.safeHtml().allowedAttributes(false) + .validate('

banana

').value.should.equal('banana'); + }); }); describe('objectid', function () { From 87536def6fcaf9db9049fe3fb082ee2229b8c2b6 Mon Sep 17 00:00:00 2001 From: Kyriakos Lesgidis Date: Tue, 30 Apr 2024 10:34:46 +0300 Subject: [PATCH 2/2] Fix test & apply prettier --- src/initializers/joi.ts | 93 +++++++++++++++++------------------ test/initializers/joi.test.ts | 2 +- 2 files changed, 47 insertions(+), 48 deletions(-) diff --git a/src/initializers/joi.ts b/src/initializers/joi.ts index ec4d8eeb..25d76244 100644 --- a/src/initializers/joi.ts +++ b/src/initializers/joi.ts @@ -1,8 +1,8 @@ import * as _Joi from 'joi'; -import {isString} from 'lodash'; +import { isString } from 'lodash'; import * as sanitizeHtml from 'sanitize-html'; -import {URL} from 'url'; -import {getLogger} from './log4js'; +import { URL } from 'url'; +import { getLogger } from './log4js'; const logger = getLogger('orka.initializers.joi'); @@ -11,17 +11,13 @@ export const clearNullByte = (val: string): string => (val && isString(val) ? va export const isValidHexColor = (val: string): boolean => /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(val); export const isOwnS3Path = (bucket: string, val: string): boolean => { try { - const {host, protocol, pathname} = new URL(val); + const { host, protocol, pathname } = new URL(val); const matchingProtocol = protocol === 'https:'; const s3Host = host === `${bucket}.s3.amazonaws.com`; const s3HostBucketInPath = - host.startsWith('s3.') && - host.endsWith('.amazonaws.com') && - pathname.startsWith(`/${bucket}/`); + host.startsWith('s3.') && host.endsWith('.amazonaws.com') && pathname.startsWith(`/${bucket}/`); const s3HostContainsRegion = - host.startsWith(`${bucket}.s3`) && - host.endsWith('.amazonaws.com') && - host.split('.').length === 5; + host.startsWith(`${bucket}.s3`) && host.endsWith('.amazonaws.com') && host.split('.').length === 5; return matchingProtocol && (s3Host || s3HostBucketInPath || s3HostContainsRegion); } catch (e) { logger.error(`Failed to parse url: ${val}`, e); @@ -44,7 +40,10 @@ export const isExpiredUrl = (val: string): boolean => { if (isNaN(xAmzExpires)) return false; const xAmzDate = parsed.searchParams.get('X-Amz-Date'); // The URL contains date in format YYYYMMDDTHHMMSSZ. We format it to ISO (YYYY-MM-DDTHH:MM:SSZ) - const formattedDate = `${xAmzDate!.slice(0, 4)}-${xAmzDate!.slice(4, 6)}-${xAmzDate!.slice(6, 8)}T${xAmzDate!.slice(9, 11)}:${xAmzDate!.slice(11, 13)}:${xAmzDate!.slice(13, 15)}Z`; + const formattedDate = `${xAmzDate!.slice(0, 4)}-${xAmzDate!.slice(4, 6)}-${xAmzDate!.slice( + 6, + 8 + )}T${xAmzDate!.slice(9, 11)}:${xAmzDate!.slice(11, 13)}:${xAmzDate!.slice(13, 15)}Z`; const expirationDate = new Date(formattedDate); expirationDate.setTime(expirationDate.getTime() + xAmzExpires * 1000); return Date.now() > expirationDate.getTime(); @@ -93,20 +92,20 @@ export interface JoiWithExtensions extends _Joi.Root { const Joi: JoiWithExtensions = _Joi.extend( joi => ({ type: 'objectId', - base: joi.string().meta({baseType: 'string'}), - messages: {'objectId.invalid': 'Invalid objectId'}, + base: joi.string().meta({ baseType: 'string' }), + messages: { 'objectId.invalid': 'Invalid objectId' }, validate: (value, helpers) => { return require('mongoose').isValidObjectId(value) - ? {value} + ? { value } : { - value, - errors: helpers.error('objectId.invalid') - }; + value, + errors: helpers.error('objectId.invalid') + }; } }), joi => ({ type: 'string', - base: joi.string().meta({baseType: 'string'}), + base: joi.string().meta({ baseType: 'string' }), prepare: (val, helpers) => { let newVal = val === null || val === undefined ? val : clearNullByte(val); @@ -127,7 +126,7 @@ const Joi: JoiWithExtensions = _Joi.extend( defaultIfEmpty: { convert: true, method(v) { - return this.$_addRule({name: 'defaultIfEmpty', args: {v}}); + return this.$_addRule({ name: 'defaultIfEmpty', args: { v } }); }, args: [ { @@ -144,22 +143,22 @@ const Joi: JoiWithExtensions = _Joi.extend( type: 'url', base: joi .string() - .uri({domain: {minDomainSegments: 2}}) - .meta({baseType: 'string'}) + .uri({ domain: { minDomainSegments: 2 } }) + .meta({ baseType: 'string' }) }), joi => ({ type: 'urlWithEmpty', base: joi .string() - .uri({domain: {minDomainSegments: 2}}) + .uri({ domain: { minDomainSegments: 2 } }) .allow('') .allow(null) - .meta({baseType: 'string'}) + .meta({ baseType: 'string' }) }), joi => ({ type: 'phone', - base: joi.string().meta({baseType: 'string'}), - messages: {'string.phone': 'The provided phone is invalid'}, + base: joi.string().meta({ baseType: 'string' }), + messages: { 'string.phone': 'The provided phone is invalid' }, rules: { stripIfInvalid: { method() { @@ -170,10 +169,10 @@ const Joi: JoiWithExtensions = _Joi.extend( validate: (value, helpers) => { // From: https://github.com/Workable/workable/blob/master/app/validators/phone_validator.rb#L3 return isValidPhone(value) - ? {value} + ? { value } : helpers.schema.$_getFlag('stripIfInvalid') - ? {value: undefined} - : { + ? { value: undefined } + : { value, errors: helpers.error('string.phone') }; @@ -181,32 +180,32 @@ const Joi: JoiWithExtensions = _Joi.extend( }), joi => ({ type: 'hexColor', - base: joi.string().meta({baseType: 'string'}), - messages: {'string.hexcolor': 'The provided color is invalid'}, + base: joi.string().meta({ baseType: 'string' }), + messages: { 'string.hexcolor': 'The provided color is invalid' }, validate: (value, helpers) => { return isValidHexColor(value) - ? {value} + ? { value } : { - value, - errors: helpers.error('string.hexcolor') - }; + value, + errors: helpers.error('string.hexcolor') + }; } }), joi => ({ type: 'stringWithEmpty', - base: joi.string().allow('').allow(null).meta({baseType: 'string'}) + base: joi.string().allow('').allow(null).meta({ baseType: 'string' }) }), joi => ({ type: 'booleanWithEmpty', - base: joi.boolean().allow(null).empty().falsy('').meta({baseType: 'boolean'}) + base: joi.boolean().allow(null).empty().falsy('').meta({ baseType: 'boolean' }) }), joi => ({ type: 'dateInThePast', - base: joi.date().iso().max(Date.now()).message('Date must be in the past').meta({baseType: 'date'}) + base: joi.date().iso().max(Date.now()).message('Date must be in the past').meta({ baseType: 'date' }) }), joi => ({ type: 'urlInOwnS3', - base: joi.string().uri().meta({baseType: 'string'}), + base: joi.string().uri().meta({ baseType: 'string' }), messages: { 'string.notInS3': 'Invalid path provided', 'string.bucketRuleMissing': 'You need to provide a bucket rule', @@ -215,7 +214,7 @@ const Joi: JoiWithExtensions = _Joi.extend( rules: { bucket: { method(bucket: string) { - return this.$_addRule({name: 'bucket', args: {bucket}}); + return this.$_addRule({ name: 'bucket', args: { bucket } }); }, args: [ { @@ -224,7 +223,7 @@ const Joi: JoiWithExtensions = _Joi.extend( message: 'must be a non empty string' } ], - validate(value, helpers, {bucket}) { + validate(value, helpers, { bucket }) { if (!isOwnS3Path(bucket, value)) { return helpers.error('string.notInS3'); } @@ -233,7 +232,7 @@ const Joi: JoiWithExtensions = _Joi.extend( }, errorOnExpiredUrl: { method() { - return this.$_addRule({name: 'errorOnExpiredUrl'}); + return this.$_addRule({ name: 'errorOnExpiredUrl' }); }, args: [], validate(value, helpers) { @@ -244,18 +243,18 @@ const Joi: JoiWithExtensions = _Joi.extend( }, validate(value, helpers) { if (!helpers.schema.$_getRule('bucket')) { - return {errors: helpers.error('string.bucketRuleMissing')}; + return { errors: helpers.error('string.bucketRuleMissing') }; } - return {value}; + return { value }; } }), joi => ({ type: 'safeHtml', - base: joi.string().meta({baseType: 'string'}), + base: joi.string().meta({ baseType: 'string' }), rules: { allowedTags: { method(allowedTags: string) { - return this.$_addRule({name: 'allowedTags', args: {allowedTags}}); + return this.$_addRule({ name: 'allowedTags', args: { allowedTags } }); }, args: [ { @@ -269,7 +268,7 @@ const Joi: JoiWithExtensions = _Joi.extend( }, allowedAttributes: { method(allowedAttributes: string) { - return this.$_addRule({name: 'allowedAttributes', args: {allowedAttributes}}); + return this.$_addRule({ name: 'allowedAttributes', args: { allowedAttributes } }); }, args: [ { @@ -299,7 +298,7 @@ const Joi: JoiWithExtensions = _Joi.extend( font: ['color'] }; return { - value: sanitizeHtml(value, {allowedTags, allowedAttributes}) + value: sanitizeHtml(value, { allowedTags, allowedAttributes }) }; } }) diff --git a/test/initializers/joi.test.ts b/test/initializers/joi.test.ts index 865186a0..f2aee3e4 100644 --- a/test/initializers/joi.test.ts +++ b/test/initializers/joi.test.ts @@ -228,7 +228,7 @@ describe('joi extensions', function () { }); it('allow any attribute', function () { Joi.safeHtml().allowedAttributes(false) - .validate('

banana

').value.should.equal('banana'); + .validate('

banana

').value.should.equal('

banana

'); }); });