0

I am trying to build a React.js SharePoint modern web part, which has the following capabilities:-

  1. Inside the Web Part settings page >> there are 2 fields named "Who We Are" & "Our Value" which allow the user to enter HTML.

  2. 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:-

enter image description here

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:-

  • I added this import import { sanitize } from 'dompurify';
  • An I replaced this unsafe code <p dangerouslySetInnerHTML={{__html:myprops.OurValue}}></p> with this <div dangerouslySetInnerHTML={{ __html: sanitize(myprops.OurValue) }} />

So my questions are:-

  1. is my way of trying to secure the dangerouslySetInnerHTML correct? or I am missing something?

  2. 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>
    );
  }
}
AmerllicA
  • 29,059
  • 15
  • 130
  • 154
John John
  • 1
  • 72
  • 238
  • 501
  • 1
    Seems like you've got two distinct questions, one of which is a code review. If possible, might want to narrow this down a little bit. – CollinD Nov 09 '21 at 15:46
  • @CollinD yes sure So my questions are;1) is my way of trying to secure the dangerouslySetInnerHTML correct? or i am missing something? 2) second question, how i can test that sanitize() method is actually working? – John John Nov 09 '21 at 15:48
  • You have more rep than I do, You know that SO isn't for "could this code be better", and questions are supposed to have a single succinct problem/question statement. Trying to determine if your security-oriented code properly handles all possible data is a really really broadly-scoped question, and likely the only way to start answering that in a way that'll satisfy you (since you don't trust the sanitize function you've chosen) is to start testing. – CollinD Nov 09 '21 at 15:51
  • @CollinD can i know what do u exactly mean by SO? – John John Nov 09 '21 at 15:53
  • StackOverflow [this comment made longer to meet minimum character limits] – CollinD Nov 09 '21 at 15:53
  • @CollinD ok fair enough – John John Nov 09 '21 at 15:54

2 Answers2

2

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.

CollinD
  • 7,304
  • 2
  • 22
  • 45
  • ok i got your point and thanks for your answer.. it is not a code review, but rather i am asking if i am correctly securing the `dangerouslySetInnerHTML` by using `dompurify`? – John John Nov 09 '21 at 15:51
  • 1
    In my opinion, asking about the correctness of securtiy-related code is a code review question, but maybe someone else has better ideas :) – CollinD Nov 09 '21 at 15:53
  • 1
    ok understand your point.. thanks any way for you help – John John Nov 09 '21 at 15:53
2

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>" />
AmerllicA
  • 29,059
  • 15
  • 130
  • 154
  • thanks for your helpful reply.. but is your approach the same as mine? or i am missing something? as in my case i am also using the `import { sanitize } from 'dompurify'` then `
    `? can you advice .. thanks
    – John John Nov 12 '21 at 13:51
  • 1
    @johnGu, Thanks bro for your kind comment, you know, the `dompurify` is a library for DOM, but you need a good tool for reactjs, so I nominated `sanitize-html-react` to you, it has awesome configs and good benefits for reactjs web projects. which is already passes its test. you know, you should save your time – AmerllicA Nov 12 '21 at 15:14
  • ok but what i should define inside the `options: { ...defaultOptions, ...options }`? thanks – John John Nov 12 '21 at 16:03
  • 1
    @johnGu, you should read all the options in [sanitize-html](https://www.npmjs.com/package/sanitize-html) and see what you need, the `sanitize-html-react` has good `defaultOptions`, you can add your `defaultOptions` too, and the second `options` is for getting new options from function argument. but please do not be focused on them, just use it, in the exact time you will understand how to use them definitely. – AmerllicA Nov 13 '21 at 05:30
  • 1
    @johnGu, dear John, if you need more help just tell me or email me on `amerllica@gmail`, I will answer you definitely, another question, is my answer helpful for you? – AmerllicA Nov 18 '21 at 09:14