From ae311b62f0de7054f5bb24c029bf65c2b41ebebf Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Wed, 13 Feb 2019 19:04:11 +0200 Subject: [PATCH 1/2] Sanitize example for listing and user entities --- src/util/data.js | 8 ++- src/util/sanitize.js | 113 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/util/sanitize.js diff --git a/src/util/data.js b/src/util/data.js index 9f279c99..c0ab0574 100644 --- a/src/util/data.js +++ b/src/util/data.js @@ -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); diff --git a/src/util/sanitize.js b/src/util/sanitize.js new file mode 100644 index 00000000..3a3d2212 --- /dev/null +++ b/src/util/sanitize.js @@ -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; + } +}; From 30efce5ac3638ffa65c44222c1f32c9fb326887d Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Fri, 15 Feb 2019 15:02:53 +0200 Subject: [PATCH 2/2] Update Changelog. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a79edf3..6f71e81c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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