Merge pull request #1023 from sharetribe/sanitize-entity-content

Sanitize example for listing and user entities
This commit is contained in:
Vesa Luusua 2019-02-15 16:57:40 +02:00 committed by GitHub
commit a3239dd1da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 123 additions and 1 deletions

View file

@ -14,6 +14,9 @@ way to update this template, but currently, we follow a pattern:
## Upcoming version 2019-XX-XX
- [add] This adds an example how user-generated content could be sanitized. If you have extended
data you should consider if sanitization is needed for that.
[#1023](https://github.com/sharetribe/flex-template-web/pull/1023)
- [change] A new component `UserDisplayName` is added for showing user display name and also
handling the cases where a user is banned or deleted. When the user name must be a string instead
of a component (e.g. in `Avatar` and in `ListingPage`) you can use a new function

View file

@ -1,5 +1,6 @@
import isArray from 'lodash/isArray';
import reduce from 'lodash/reduce';
import { sanitizeEntity } from './sanitize';
/**
* Combine the given relationships objects
@ -42,9 +43,14 @@ export const updatedEntities = (oldEntities, apiResponse) => {
const newEntities = objects.reduce((entities, curr) => {
const { id, type } = curr;
// Some entities (e.g. listing and user) might include extended data,
// you should check if src/util/sanitize.js needs to be updated.
const current = sanitizeEntity(curr);
entities[type] = entities[type] || {};
const entity = entities[type][id.uuid];
entities[type][id.uuid] = entity ? combinedResourceObjects(entity, curr) : curr;
entities[type][id.uuid] = entity ? combinedResourceObjects(entity, current) : current;
return entities;
}, oldEntities);

113
src/util/sanitize.js Normal file
View file

@ -0,0 +1,113 @@
/**
* By default, React DOM escapes any values embedded in JSX before rendering them,
* but sometimes it is necessary to sanitize the user-generated content of received entities.
* If you use this data in component props without any sanitization or encoding,
* it might create XSS vulnerabilities.
*
* You should especially consider how you are using extended data inside the app.
*/
const ESCAPE_TEXT_REGEXP = /[<>]/g;
const ESCAPE_TEXT_REPLACEMENTS = {
//fullwidth lesser-than character
'<': '\uff1c',
//fullwidth greater-than character
'>': '\uff1e',
};
// An example how you could sanitize text content.
// This swaps some coding related characters to less dangerous ones
const sanitizeText = str =>
str == null
? str
: typeof str === 'string'
? str.replace(ESCAPE_TEXT_REGEXP, ch => ESCAPE_TEXT_REPLACEMENTS[ch])
: '';
/**
* Sanitize user entity.
* If you add public data, you should probably sanitize it here.
* By default, React DOM escapes any values embedded in JSX before rendering them,
* but if you use this data on props, it might create XSS vulnerabilities
* E.g. you should sanitize and encode URI if you are creating links from public data.
*/
export const sanitizeUser = entity => {
const { attributes, ...restEntity } = entity || {};
const { profile, ...restAttributes } = attributes || {};
const { bio, displayName, abbreviatedName, publicData } = profile || {};
const sanitizePublicData = publicData => {
// TODO: If you add public data, you should probably sanitize it here.
return publicData ? { publicData } : {};
};
const profileMaybe = profile
? {
profile: {
abbreviatedName: sanitizeText(abbreviatedName),
displayName: sanitizeText(displayName),
bio: sanitizeText(bio),
...sanitizePublicData(publicData),
},
}
: {};
const attributesMaybe = attributes ? { attributes: { ...profileMaybe, ...restAttributes } } : {};
return { ...attributesMaybe, ...restEntity };
};
/**
* Sanitize listing entity.
* If you add public data, you should probably sanitize it here.
* By default, React DOM escapes any values embedded in JSX before rendering them,
* but if you use this data on props, it might create XSS vulnerabilities
* E.g. you should sanitize and encode URI if you are creating links from public data.
*/
export const sanitizeListing = entity => {
const { attributes, ...restEntity } = entity;
const { title, description, publicData, ...restAttributes } = attributes || {};
const sanitizeLocation = location => {
const { address, building } = location || {};
return { address: sanitizeText(address), building: sanitizeText(building) };
};
const sanitizePublicData = publicData => {
// Here's an example how you could sanitize location and rules from publicData:
// TODO: If you add public data, you should probably sanitize it here.
const { location, rules, ...restPublicData } = publicData || {};
const locationMaybe = location ? { location: sanitizeLocation(location) } : {};
const rulesMaybe = rules ? { rules: sanitizeText(rules) } : {};
return publicData ? { publicData: { ...locationMaybe, ...rulesMaybe, ...restPublicData } } : {};
};
const attributesMaybe = attributes
? {
attributes: {
title: sanitizeText(title),
description: sanitizeText(description),
...sanitizePublicData(publicData),
...restAttributes,
},
}
: {};
return { ...attributesMaybe, ...restEntity };
};
/**
* Sanitize entities if needed.
* Remember to add your own sanitization rules for your extended data
*/
export const sanitizeEntity = entity => {
const { type } = entity;
switch (type) {
case 'listing':
return sanitizeListing(entity);
case 'user':
return sanitizeUser(entity);
default:
return entity;
}
};