Skip to content

Commit

Permalink
Merge pull request #116 from sendbird/fix/html-xss
Browse files Browse the repository at this point in the history
[CLNP-2532] fix: html xss
  • Loading branch information
bang9 authored Mar 12, 2024
2 parents 17c207a + d753ab1 commit 3eb706f
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 31 deletions.
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,19 @@
"dependencies": {
"@sendbird/chat": "^4.10.1",
"@sendbird/uikit-react": "^3.13.1",
"@tanstack/react-query": "^5.17.19",
"dompurify": "^3.0.4",
"polished": "^2.3.1",
"react-code-blocks": "^0.1.0",
"react-popper-tooltip": "^4.4.2",
"styled-components": "^5.3.11",
"@tanstack/react-query": "^5.17.19"
"styled-components": "^5.3.11"
},
"devDependencies": {
"@tanstack/eslint-plugin-query": "^5.17.22",
"@types/dompurify": "^3.0.5",
"@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11",
"@types/styled-components": "^5.1.26",
"@tanstack/eslint-plugin-query": "^5.17.22",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"@vitejs/plugin-react": "^4.0.0",
Expand All @@ -61,8 +62,8 @@
"tslint-config-prettier": "^1.18.0",
"typescript": "5.0.2",
"vite": "^4.3.9",
"vite-plugin-svgr": "^3.2.0",
"vite-plugin-dts": "^3.7.0"
"vite-plugin-dts": "^3.7.0",
"vite-plugin-svgr": "^3.2.0"
},
"peerDependencies": {
"react": "^16.8.6 || ^17.0.0 || ^18.0.0",
Expand Down
5 changes: 4 additions & 1 deletion packages/url-webdemo/src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ interface CustomRefreshComponent {
style: React.CSSProperties;
}

type MatchString = string;
type ReplaceString = string;

interface ChatBottomContent {
text: string;
backgroundColor?: string;
Expand All @@ -42,7 +45,7 @@ export interface DemoConstant {
suggestedMessageContent: SuggestedMessageContent;
createGroupChannelParams: CreateGroupChannelParams;
botNickName: string;
replacementTextList: string[][];
replacementTextList: [MatchString, ReplaceString][];
customRefreshComponent?: Partial<CustomRefreshComponent>;
betaMark: boolean;
customBetaMarkText?: string;
Expand Down
4 changes: 2 additions & 2 deletions src/components/AdminMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UserMessage } from '@sendbird/chat/message';
import { AdminMessage as ChatAdminMessage } from '@sendbird/chat/message';
import styled from 'styled-components';

const Root = styled.div`
Expand Down Expand Up @@ -38,7 +38,7 @@ const TextComponent = styled.div`
`;

type Props = {
message: UserMessage;
message: ChatAdminMessage;
};

export default function AdminMessage(props: Props) {
Expand Down
6 changes: 4 additions & 2 deletions src/components/CustomMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
isNotLocalMessageCustomType,
MessageTextParser,
replaceTextExtractsMultiple,
replaceUrl,
Token,
} from '../utils';
import { isFormMessage } from '../utils/messages';
Expand Down Expand Up @@ -123,11 +122,14 @@ export default function CustomMessage(props: Props) {
const tokens: Token[] = MessageTextParser((message as UserMessage).message);
tokens.forEach((token: Token) => {
if (token.type === 'String') {
token.value = replaceUrl(token.value);
// Redact text to replacementTextList
token.value = replaceTextExtractsMultiple(
token.value,
replacementTextList
);

// Convert url string to component --> handled by ParsedBotMessageBody > RegexText
// token.value = replaceUrl(token.value);
}
});

Expand Down
104 changes: 93 additions & 11 deletions src/components/ParsedBotMessageBody.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { lazy, Suspense } from 'react';
// eslint-disable-next-line import/no-unresolved
import { EveryMessage } from 'SendbirdUIKitGlobal';
import { UserMessage } from '@sendbird/chat/message';
import { lazy, Suspense, ReactNode } from 'react';
import styled from 'styled-components';

import BotMessageBottom from './BotMessageBottom';
import SourceContainer, { Source } from './SourceContainer';
import { useConstantState } from '../context/ConstantContext';
import { Token, TokenType } from '../utils';
import { replaceWithRegex, Token, TokenType } from '../utils';

const LazyCodeBlock = lazy(() =>
import('./CodeBlock').then(({ CodeBlock }) => ({ default: CodeBlock }))
Expand Down Expand Up @@ -35,7 +34,7 @@ const MultipleTokenTypeContainer = styled.div`
`;

type Props = {
message: EveryMessage;
message: UserMessage;
tokens: Token[];
};

Expand All @@ -53,9 +52,7 @@ export default function ParsedBotMessageBody(props: Props) {
const { enableSourceMessage } = useConstantState();
const data: MetaData = JSON.parse(message.data === '' ? '{}' : message.data);
const sources: Source[] = Array.isArray(data['metadatas'])
? data['metadatas']?.filter(
(source: Source) => source.source_type !== 'file'
)
? data['metadatas']?.filter((source) => source.source_type !== 'file')
: [];

// console.log('## sources: ', sources);
Expand All @@ -65,12 +62,54 @@ export default function ParsedBotMessageBody(props: Props) {
{tokens.map((token: Token, i) => {
if (token.type === TokenType.string) {
return (
<Text
<RegexText
key={'token' + i}
dangerouslySetInnerHTML={{ __html: token.value }}
/>
patterns={[
{
regex: markdownBoldRegex,
replacer({ match, groups, index }) {
return <strong key={`${match}-${index}`}>{groups[1]}</strong>;
},
},
{
regex: markdownUrlRegex,
replacer({ match, groups, index }) {
return (
<a
key={`${match}-${index}`}
className="sendbird-word__url"
href={groups[2]}
target="_blank"
rel="noreferrer"
>
{groups[1]}
</a>
);
},
},
{
regex: urlRegex,
replacer({ match, index }) {
return (
<a
key={`${match}-${index}`}
className="sendbird-word__url"
href={match}
target="_blank"
rel="noreferrer"
>
{match}
</a>
);
},
},
]}
>
{token.value}
</RegexText>
);
}

return (
<BlockContainer key={'token' + i}>
<Suspense fallback={<></>}>
Expand All @@ -90,3 +129,46 @@ export default function ParsedBotMessageBody(props: Props) {
}
return <Text style={{ borderRadius: 16 }}>{message.message}</Text>;
}

const urlRegex =
/(?:https?:\/\/|www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.(xn--)?[a-z]{2,20}\b([-a-zA-Z0-9@:%_+[\],.~#?&/=]*[-a-zA-Z0-9@:%_+~#?&/=])*/g;
const markdownUrlRegex = /\[(.*?)\]\((.*?)\)/g;
const markdownBoldRegex = /\*\*(.*?)\*\*/g;

interface RegexTextPattern {
regex: RegExp;
replacer(params: {
match: string;
groups: string[];
index: number;
}): string | ReactNode;
}
const RegexText = ({
children,
patterns,
}: {
children: string;
patterns: RegexTextPattern[];
}) => {
if (patterns.length === 0 || typeof children !== 'string') {
return <>{children}</>;
}

const convertedNodes: Array<string | ReactNode> = [children];
patterns.forEach(({ regex, replacer }) => {
const node = convertedNodes.concat();
let offset = 0;
node.forEach((text, index) => {
if (typeof text === 'string' && text) {
const children = replaceWithRegex(text, regex, replacer);

if (children.length > 1) {
convertedNodes.splice(index + offset, 1, ...children);
offset += children.length - 1;
}
}
});
});

return <Text>{convertedNodes}</Text>;
};
1 change: 1 addition & 0 deletions src/components/SourceContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface Source {
title: string;
description: string;
language: string;
source_type?: string;
}

type Props = {
Expand Down
4 changes: 3 additions & 1 deletion src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ type FirstMessageItem = {
data: MessageData;
message: string;
};
type MatchString = string;
type ReplaceString = string;

export interface Constant {
botNickName: string;
Expand All @@ -113,7 +115,7 @@ export interface Constant {
createGroupChannelParams: CreateGroupChannelParams;
chatBottomContent: ChatBottomContent;
messageBottomContent: MessageBottomContent;
replacementTextList: string[][];
replacementTextList: [MatchString, ReplaceString][];
instantConnect: boolean;
customRefreshComponent: CustomRefreshComponent;
customUserAgentParam: Record<any, any>;
Expand Down
36 changes: 27 additions & 9 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export function isNotLocalMessageCustomType(customType: string | undefined) {

export function replaceTextExtractsMultiple(
input: string,
replacements: Array<[string, string]>
replacements: [string, string][],
): string {
let result = input;
for (let i = 0; i < replacements.length; i++) {
Expand All @@ -173,14 +173,6 @@ export function replaceTextExtracts(
return input.replace(regex, replaceText);
}

export function replaceUrl(input: string): string {
const urlRegex =
/(?:https?:\/\/|www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.(xn--)?[a-z]{2,20}\b([-a-zA-Z0-9@:%_+[\],.~#?&/=]*[-a-zA-Z0-9@:%_+~#?&/=])*/g;
return input.replace(urlRegex, function (url) {
return `<a class="sendbird-word__url" href="${url}" target="_blank">${url}</a>`;
});
}

export function isSpecialMessage(
message: string,
specialMessageList: string[]
Expand Down Expand Up @@ -219,3 +211,29 @@ export function hideChatBottomBanner(sdk: SendbirdChat): boolean {

return false;
}

export const replaceWithRegex = <T,>(
text: string,
regex: RegExp,
replacer: (params: { match: string; groups: string[]; index: number; }) => T,
) => {
const matches = [...text.matchAll(regex)];
const founds = matches.map((value) => {
const text = value[0];
const start = value.index ?? 0;
const end = start + text.length;
return { text, start, end, groups: value, matchIndex: value.index };
});

const items: Array<T | string> = [text];
let cursor = 0;
founds.forEach(({ text, start, end, groups }, index) => {
const restText = items.pop() as string;
const head = restText.slice(0, start - cursor);
const mid = replacer({ match: text, groups, index });
const tail = restText.slice(end - cursor);
items.push(head, mid, tail);
cursor = end;
});
return items;
};

0 comments on commit 3eb706f

Please sign in to comment.