Skip to content

Commit

Permalink
[PROD-40813] Allow any tag on safeHtml (#379)
Browse files Browse the repository at this point in the history
* [PROD-40813] Allow any tag on safeHtml
  • Loading branch information
klesgidis authored Apr 30, 2024
1 parent 1dcf2e9 commit e0cff18
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 49 deletions.
98 changes: 49 additions & 49 deletions src/initializers/joi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as _Joi from 'joi';
import {isString} from 'lodash';
import { isString } from 'lodash';
import * as sanitizeHtml from 'sanitize-html';
import {URL} from 'url';
import { URL } from 'url';
import { getLogger } from './log4js';

const logger = getLogger('orka.initializers.joi');
Expand All @@ -15,13 +15,9 @@ export const isOwnS3Path = (bucket: string, val: string): boolean => {
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);
Expand All @@ -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();
Expand All @@ -57,8 +56,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 & {
Expand Down Expand Up @@ -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);

Expand All @@ -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: [
{
Expand All @@ -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() {
Expand All @@ -170,43 +169,43 @@ 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')
};
}
}),
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',
Expand All @@ -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: [
{
Expand All @@ -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');
}
Expand All @@ -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) {
Expand All @@ -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: [
{
Expand All @@ -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: [
{
Expand All @@ -283,7 +282,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',
Expand All @@ -293,12 +293,12 @@ 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']
};
return {
value: sanitizeHtml(value, {allowedTags, allowedAttributes})
value: sanitizeHtml(value, { allowedTags, allowedAttributes })
};
}
})
Expand Down
4 changes: 4 additions & 0 deletions test/initializers/joi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ describe('joi extensions', function () {
Joi.safeHtml().allowedAttributes({}).allowedTags([])
.validate('<p>banana</p>').value.should.equal('banana');
});
it('allow any attribute', function () {
Joi.safeHtml().allowedAttributes(false)
.validate('<p class="asd" rel="asd">banana</p>').value.should.equal('<p class="asd" rel="asd">banana</p>');
});
});

describe('objectid', function () {
Expand Down

0 comments on commit e0cff18

Please sign in to comment.