javascript - Secure way to use dangerouslySetInnerHTML inside my react SharePoint Modern web part - Stack Overflow

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

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

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 mand 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 './ponents/MyModalPopup';
import { IMyModalPopupProps } from './ponents/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>
    );
  }
}

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

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 mand 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 './ponents/MyModalPopup';
import { IMyModalPopupProps } from './ponents/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>
    );
  }
}
Share Improve this question edited Nov 12, 2021 at 11:58 AmerllicA 32.7k17 gold badges144 silver badges167 bronze badges asked Nov 9, 2021 at 15:39 John JohnJohn John 1 6
  • 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 Commented Nov 9, 2021 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 Commented Nov 9, 2021 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 Commented Nov 9, 2021 at 15:51
  • @CollinD can i know what do u exactly mean by SO? – John John Commented Nov 9, 2021 at 15:53
  • StackOverflow [this ment made longer to meet minimum character limits] – CollinD Commented Nov 9, 2021 at 15:53
 |  Show 1 more ment

2 Answers 2

Reset to default 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 ponent (using JavaScript):

const defaultOptions = {
  allowedTags: [ 'a', 'div', 'span', ],
  allowedAttributes: {
    'a': [ 'href' ]
  },
  allowedIframeHostnames: ['www.example.'],
  // 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 ponent will remove onclick because it is not in your allowed configurations.

<SanitizeHTML html="<div><a href="youtube." onclick="alert('@')">link</a></div>" />

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 ponent 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.

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1745351670a4623869.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信