Reputation: 1
I am trying to build a React.js SharePoint modern web part, which has the following capabilities:-
Inside the Web Part settings page >> there are 2 fields named "Who We Are" & "Our Value" which allow the user to enter HTML.
The web part will render 2 buttons "Who We Are" & "Our Value" >> and when the user clicks on any button >> a Popup will be shown with the entered HTML code in step-1
Something as follows:-
But to be able to render HTML code as Rich-Text inside my Web Part, I have to use the dangerouslySetInnerHTML
attribute inside the .tsx file. as follow:-
import * as React from 'react';
import { useId, useBoolean } from '@fluentui/react-hooks';
import {
getTheme,
mergeStyleSets,
FontWeights,
Modal,
IIconProps,
IStackProps,
} from '@fluentui/react';
import { IconButton, IButtonStyles } from '@fluentui/react/lib/Button';
export const MYModal2 = (myprops) => {
const [isModalOpen, { setTrue: showModal, setFalse: hideModal }] = useBoolean(false);
const [isPopup, setisPopup] = React.useState(true);
const titleId = useId('title');
React.useEffect(() => {
showModal();
}, [isPopup]);
function ExitHandler() {
hideModal();
setisPopup(current => !current)
myprops.handler();
}
return (
<div>
<Modal
titleAriaId={titleId}
isOpen={isModalOpen}
onDismiss={ExitHandler}
isBlocking={true}
containerClassName={contentStyles.container}
>
<div className={contentStyles.header}>
<span id={titleId}>Modal Popup</span>
<IconButton
styles={iconButtonStyles}
iconProps={cancelIcon}
ariaLabel="Close popup modal"
onClick={ExitHandler}
/>
</div>
<div className={contentStyles.body}>
<p dangerouslySetInnerHTML={{__html:myprops.OurValue}}>
</p>
</div>
</Modal>
</div>
);
};
const cancelIcon: IIconProps = { iconName: 'Cancel' };
const theme = getTheme();
const contentStyles = mergeStyleSets({
container: {
display: 'flex',
flexFlow: 'column nowrap',
alignItems: 'stretch',
},
header: [
// eslint-disable-next-line deprecation/deprecation
theme.fonts.xLarge,
{
flex: '1 1 auto',
borderTop: '4px solid ${theme.palette.themePrimary}',
color: theme.palette.neutralPrimary,
display: 'flex',
alignItems: 'center',
fontWeight: FontWeights.semibold,
padding: '12px 12px 14px 24px',
},
],
body: {
flex: '4 4 auto',
padding: '0 24px 24px 24px',
overflowY: 'hidden',
selectors: {
p: { margin: '14px 0' },
'p:first-child': { marginTop: 0 },
'p:last-child': { marginBottom: 0 },
},
},
});
const stackProps: Partial<IStackProps> = {
horizontal: true,
tokens: { childrenGap: 40 },
styles: { root: { marginBottom: 20 } },
};
const iconButtonStyles: Partial<IButtonStyles> = {
root: {
color: theme.palette.neutralPrimary,
marginLeft: 'auto',
marginTop: '4px',
marginRight: '2px',
},
rootHovered: {
color: theme.palette.neutralDark,
},
};
And to secure the dangerouslySetInnerHTML
, i did the following steps:-
1- Inside my Node.Js CMD >> i run this command inside my project directory:-
npm install dompurify eslint-plugin-risxss
2- Then inside my above .tsx
i made the following modifications:-
import { sanitize } from 'dompurify';
<p dangerouslySetInnerHTML={{__html:myprops.OurValue}}></p>
with this <div dangerouslySetInnerHTML={{ __html: sanitize(myprops.OurValue) }} />
So my questions are:-
is my way of trying to secure the dangerouslySetInnerHTML
correct? or I am missing something?
second question, how I can test that sanitize()
method is actually working?
Here is my Full web part code:-
inside the MyModalPopupWebPart.ts:-
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as strings from 'MyModalPopupWebPartStrings';
import MyModalPopup from './components/MyModalPopup';
import { IMyModalPopupProps } from './components/IMyModalPopupProps';
export interface IMyModalPopupWebPartProps {
description: string;
WhoWeAre: string;
OurValue:string;
}
export default class MyModalPopupWebPart extends BaseClientSideWebPart<IMyModalPopupWebPartProps> {
public render(): void {
const element: React.ReactElement<IMyModalPopupProps> = React.createElement(
MyModalPopup,
{
description: this.properties.description,
WhoWeAre: this.properties.WhoWeAre,
OurValue: this.properties.OurValue
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('WhoWeAre', {
label: "who We Are",
multiline: true
}),
PropertyPaneTextField('OurValue', {
label: "Our value"
}), PropertyPaneTextField('description', {
label: "Description",
multiline: true
}),
]
}
]
}
]
};
}
}
inside the MyModalPopup.tsx:-
import * as React from 'react';
import { IMyModalPopupProps } from './IMyModalPopupProps';
import { DefaultButton } from '@fluentui/react/lib/Button';
import { MYModal } from './MYModal';
import { MYModal2 } from './MYModal2';
interface IPopupState {
showModal: string;
}
export default class MyModalPopup extends React.Component<IMyModalPopupProps, IPopupState> {
constructor(props: IMyModalPopupProps, state: IPopupState) {
super(props);
this.state = {
showModal: ''
};
this.handler = this.handler.bind(this);
this.Buttonclick = this.Buttonclick.bind(this);
}
handler() {
this.setState({
showModal: ''
})
}
private Buttonclick(e, whichModal) {
e.preventDefault();
this.setState({ showModal: whichModal });
}
public render(): React.ReactElement<IMyModalPopupProps> {
const { showModal } = this.state;
return (
<div>
<DefaultButton onClick={(e) => this.Buttonclick(e, 'our-value')} text="Our Value" />
{ showModal === 'our-value' && <MYModal2 OurValue={this.props.OurValue} myprops={this.state} handler={this.handler} />}
<DefaultButton onClick={(e) => this.Buttonclick(e, 'who-we-are')} text="Who We Are" />
{ showModal === 'who-we-are' && <MYModal WhoWeAre={this.props.WhoWeAre} myprops={this.state} handler={this.handler} />}
</div>
);
}
}
Upvotes: 0
Views: 6855
Reputation: 32532
Actually, you can sanitize the HTML markup by using the sanitize-html-react library, and render the sanitized result as a string inside the dangerouslySetInnerHTML
:
Here is a sample safe component (using JavaScript):
const defaultOptions = {
allowedTags: [ 'a', 'div', 'span', ],
allowedAttributes: {
'a': [ 'href' ]
},
allowedIframeHostnames: ['www.example.com'],
// and many extra configurations
};
const sanitize = (dirty, options) => ({
__html: sanitizeHtml(
dirty,
options: { ...defaultOptions, ...options }
)
});
const SanitizeHTML = ({ html, options }) => (
<div dangerouslySetInnerHTML={sanitize(html, options)} />
);
In the below example the SanitizeHTML
component will remove onclick
because it is not in your allowed configurations.
<SanitizeHTML html="<div><a href="youtube.com" onclick="alert('@')">link</a></div>" />
Upvotes: 2
Reputation: 7573
For testing the functionality, I'd suggest using something like React Testing Library. It should be (fairly) simple to write tests that can simply render your component with malicious data and then assert that it doesn't do bad stuff (like render Script elements or whatever else you're concerned about).
This has the benefit of not only testing sanitize
but also your usage thereof in a much more holistic way.
I can't speak to the actual quality/security of your solution, that would be more of a Code Review question I think.
Upvotes: 2