Skip to content

Commit

Permalink
fix(webchat): escape unsafe html
Browse files Browse the repository at this point in the history
  • Loading branch information
EFF committed Sep 12, 2019
1 parent 22464a6 commit 40bf2d7
Show file tree
Hide file tree
Showing 9 changed files with 50 additions and 18 deletions.
5 changes: 3 additions & 2 deletions modules/channel-web/src/backend/api.ts
@@ -1,6 +1,5 @@
import aws from 'aws-sdk'
import * as sdk from 'botpress/sdk'
import crypto from 'crypto'
import _ from 'lodash'
import moment from 'moment'
import multer from 'multer'
Expand Down Expand Up @@ -88,6 +87,7 @@ export default async (bp: typeof sdk, db: Database) => {
'/botInfo',
asyncApi(async (req, res) => {
const { botId } = req.params
const security = ((await bp.config.getModuleConfig('channel-web')) as Config).security // usage of global because a user could overwrite bot scoped configs
const config = (await bp.config.getModuleConfigForBot('channel-web', botId)) as Config
const botInfo = await bp.bots.getBotById(botId)

Expand All @@ -99,7 +99,8 @@ export default async (bp: typeof sdk, db: Database) => {
showBotInfoPage: (config.infoPage && config.infoPage.enabled) || config.showBotInfoPage,
name: botInfo.name,
description: (config.infoPage && config.infoPage.description) || botInfo.description,
details: botInfo.details
details: botInfo.details,
security
})
})
)
Expand Down
10 changes: 10 additions & 0 deletions modules/channel-web/src/config.ts
Expand Up @@ -56,4 +56,14 @@ export interface Config {
* @default 20
*/
maxMessagesHistory?: number
/**
* Security configurations
*/
security: {
/**
* Weather or not to escape plain html payload
* @default false
*/
escapeHTML: boolean
}
}
@@ -1,12 +1,12 @@
import { inject, observer } from 'mobx-react'
import * as React from 'react'
import { FormattedMessage, InjectedIntlProps, injectIntl } from 'react-intl'
import snarkdown from 'snarkdown'

import EmailIcon from '../../icons/Email'
import PhoneIcon from '../../icons/Phone'
import WebsiteIcon from '../../icons/Website'
import { RootStore, StoreDef } from '../../store'
import { renderUnsafeHTML } from '../../utils'

import Avatar from './Avatar'

Expand All @@ -30,8 +30,7 @@ class BotInfoPage extends React.Component<BotInfoProps> {
}

renderDescription(text) {
let html = snarkdown(text || '')
html = html.replace(/<a href/gi, `<a target="_blank" href`)
const html = renderUnsafeHTML(text, this.props.escapeHTML)

return <div className={'bpw-botinfo-description'} dangerouslySetInnerHTML={{ __html: html }} />
}
Expand Down Expand Up @@ -121,7 +120,8 @@ export default inject(({ store }: { store: RootStore }) => ({
avatarUrl: store.botAvatarUrl,
startConversation: store.startConversation,
toggleBotInfo: store.view.toggleBotInfo,
isConversationStarted: store.isConversationStarted
isConversationStarted: store.isConversationStarted,
escapeHTML: store.escapeHTML
}))(injectIntl(observer(BotInfoPage)))

type BotInfoProps = InjectedIntlProps &
Expand All @@ -134,4 +134,5 @@ type BotInfoProps = InjectedIntlProps &
| 'startConversation'
| 'isConversationStarted'
| 'enableArrowNavigation'
| 'escapeHTML'
>
@@ -1,10 +1,9 @@
import classnames from 'classnames'
import pick from 'lodash/pick'

import React, { Component } from 'react'
import { inject } from 'mobx-react'
import { RootStore, StoreDef } from '../../store'
import React, { Component } from 'react'

import { RootStore, StoreDef } from '../../store'
import { Renderer } from '../../typings'
import * as Keyboard from '../Keyboard'

Expand All @@ -25,7 +24,8 @@ class Message extends Component<MessageProps> {
if (!textMessage && !text) {
return null
}
return <Text markdown={markdown} text={textMessage || text} />

return <Text markdown={markdown} text={textMessage || text} escapeHTML={this.props.escapeHTML} />
}

render_quick_reply() {
Expand Down Expand Up @@ -57,7 +57,7 @@ class Message extends Component<MessageProps> {
}

render_file() {
return <FileMessage file={this.props.payload} />
return <FileMessage file={this.props.payload} escapeTextHTML={this.props.escapeHTML} />
}

render_custom() {
Expand Down Expand Up @@ -167,7 +167,8 @@ class Message extends Component<MessageProps> {

export default inject(({ store }: { store: RootStore }) => ({
intl: store.intl,
config: store.config
config: store.config,
escapeHTML: store.escapeHTML
}))(Message)

type MessageProps = Renderer.Message & Pick<StoreDef, 'intl' | 'config'>
type MessageProps = Renderer.Message & Pick<StoreDef, 'intl' | 'config' | 'escapeHTML'>
Expand Up @@ -18,7 +18,7 @@ export const FileMessage = (props: Renderer.FileMessage) => {
const basename = path.basename(url, extension)

if (text) {
return <Text text={text} markdown={true} />
return <Text text={text} markdown={true} escapeHTML={props.escapeTextHTML} />
}

if (storage === 'local') {
Expand Down
@@ -1,19 +1,19 @@
import * as React from 'react'
import Linkify from 'react-linkify'
import snarkdown from 'snarkdown'

import { Renderer } from '../../../typings'
import { renderUnsafeHTML } from '../../../utils'

/**
* A simple text element with optional markdown
* @param {boolean} markdown Enable markdown parsing for the given text
* @param {string} text The text to display
* @param {boolean} escapeHTML Prevent unsafe HTML rendering when markdown is enabled
*/
export const Text = (props: Renderer.Text) => {
let message = <p>{props.text}</p>
if (props.markdown) {
let html = snarkdown(props.text || '')
html = html.replace(/<a href/gi, `<a target="_blank" href`)
const html = renderUnsafeHTML(props.text, props.escapeHTML)

message = <div dangerouslySetInnerHTML={{ __html: html }} />
}
Expand Down
5 changes: 5 additions & 0 deletions modules/channel-web/src/views/lite/store/index.ts
Expand Up @@ -84,6 +84,11 @@ class RootStore {
)
}

@computed
get escapeHTML(): boolean {
return this.botInfo && this.botInfo.security.escapeHTML
}

@computed
get currentMessages(): Message[] {
return this.currentConversation && this.currentConversation.messages
Expand Down
5 changes: 5 additions & 0 deletions modules/channel-web/src/views/lite/typings.d.ts
Expand Up @@ -55,6 +55,7 @@ export namespace Renderer {
export type Text = {
text: string
markdown: boolean
escapeHTML: boolean
} & Message

export type QuickReply = {
Expand All @@ -74,6 +75,7 @@ export namespace Renderer {
storage: string
text: string
}
escapeTextHTML: boolean
}

export interface FileInput {
Expand Down Expand Up @@ -182,6 +184,9 @@ export interface BotInfo {
description: string
details: BotDetails
showBotInfoPage: boolean
security: {
escapeHTML: boolean
}
}

interface Conversation {
Expand Down
11 changes: 10 additions & 1 deletion modules/channel-web/src/views/lite/utils.tsx
@@ -1,5 +1,5 @@
import React from 'react'
import ReactGA from 'react-ga'
import snarkdown from 'snarkdown'

export const getOverridedComponent = (overrides, componentName) => {
if (overrides && overrides[componentName]) {
Expand Down Expand Up @@ -65,3 +65,12 @@ export const initializeAnalytics = () => {
}
}
}

export const renderUnsafeHTML = (template: string = '', escaped: boolean = false): string => {
if (escaped) {
template = template.replace(/</g, '&lt;').replace(/>/g, '&gt;')
}

const html = snarkdown(template)
return html.replace(/<a href/gi, `<a target="_blank" href`)
}

0 comments on commit 40bf2d7

Please sign in to comment.