【问题标题】:Secure way to use dangerouslySetInnerHTML inside my react SharePoint Modern web part在我的 React SharePoint Modern Web 部件中使用 dangerouslySetInnerHTML 的安全方式
【发布时间】:2021-11-12 23:15:27
【问题描述】:

我正在尝试构建一个 React.js SharePoint 现代 Web 部件,它具有以下功能:-

  1. 在 Web 部件设置页面内 >> 有 2 个字段名为“我们是谁”和“我们的价值”,允许用户输入 HTML。

  2. Web 部件将呈现 2 个按钮“我们是谁”和“我们的价值”>>,当用户单击任何按钮 >> 将显示一个弹出窗口,其中包含在步骤 1 中输入的 HTML 代码

如下:-

但是为了能够在我的 Web 部件中将 HTML 代码呈现为富文本,我必须在 .tsx 文件中使用 dangerouslySetInnerHTML 属性。如下:-

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,
  },
};

为了保护dangerouslySetInnerHTML我做了以下步骤:-

1- 在我的 Node.Js CMD 中 >> 我在我的项目目录中运行这个命令:-

npm install dompurify eslint-plugin-risxss

2- 然后在我上面的.tsx 中我做了以下修改:-

  • 我添加了这个导入import { sanitize } from 'dompurify';
  • 我用这个&lt;div dangerouslySetInnerHTML={{ __html: sanitize(myprops.OurValue) }} /&gt;替换了这个不安全的代码&lt;p dangerouslySetInnerHTML={{__html:myprops.OurValue}}&gt;&lt;/p&gt;

所以我的问题是:-

  1. 我尝试保护dangerouslySetInnerHTML 的方式是否正确?还是我错过了什么?

  2. 第二个问题,我如何测试sanitize() 方法是否真正有效?

这是我的完整网页代码:-

在 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
                }),
              ]
            }
          ]
        }
      ]
    };
  }
}

在 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>
    );
  }
}

【问题讨论】:

  • 您似乎有两个不同的问题,其中一个是代码审查。如果可能的话,可能想缩小一点。
  • @CollinD 是肯定的所以我的问题是;1)我尝试保护危险SetInnerHTML 的方式是否正确?或者我错过了什么? 2) 第二个问题,我如何测试 sanitize() 方法是否真正有效?
  • 你比我有更多的代表,你知道 SO 不是为了“这个代码可以更好吗”,问题应该有一个简洁的问题/问题陈述。尝试确定您的面向安全的代码是否正确处理所有可能的数据是一个非常广泛的问题,并且可能是开始以让您满意的方式回答该问题的唯一方法(因为您不信任 sanitize 功能您已选择)开始测试。
  • @CollinD 我能知道你所说的 SO 到底是什么意思吗?
  • StackOverflow [此评论加长以满足最小字符限制]

标签: javascript reactjs xss sharepoint-online dompurify


【解决方案1】:

实际上,您可以使用sanitize-html-react 库清理HTML 标记,并将清理后的结果呈现为dangerouslySetInnerHTML 内的字符串:

这是一个示例安全组件(使用 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)} />
);

在下面的示例中,SanitizeHTML 组件将删除 onclick,因为它不在您允许的配置中。

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

【讨论】:

  • 感谢您的帮助。但是您的方法和我的一样吗?或者我错过了什么?就我而言,我也使用import { sanitize } from 'dompurify' 然后&lt;div dangerouslySetInnerHTML={{ __html: sanitize(myprops.OurValue) }} /&gt;?你能建议..谢谢
  • @johnGu,谢谢兄弟的评论,你知道,dompurify 是一个 DOM 库,但是你需要一个好的 reactjs 工具,所以我给你提名了sanitize-html-react,它对 reactjs web 项目有很棒的配置和好处。这已经通过了测试。你知道,你应该节省你的时间
  • 好的,但是我应该在options: { ...defaultOptions, ...options } 中定义什么?谢谢
  • @johnGu,你应该阅读sanitize-html中的所有选项,看看你需要什么,sanitize-html-react有很好的defaultOptions,你也可以添加你的defaultOptions,第二个@ 987654336@ 用于从函数参数中获取新选项。但请不要专注于它们,只要使用它,在确切的时间你就会明白如何使用它们。
  • @johnGu,亲爱的约翰,如果您需要更多帮助,请告诉我或发送电子邮件至amerllica@gmail,我一定会回答您,另一个问题,我的回答对您有帮助吗?
【解决方案2】:

为了测试功能,我建议使用React Testing Library 之类的东西。编写测试应该(相当)简单,可以简单地使用恶意数据渲染您的组件,然后断言它不会做坏事(如渲染脚本元素或您关心的任何其他内容)。

这不仅可以测试sanitize,而且可以更全面地测试您的使用情况。

我无法谈论您解决方案的实际质量/安全性,我认为这更像是一个代码审查问题。

【讨论】:

  • 好的,我明白了你的意思,感谢你的回答。这不是代码审查,而是我在问我是否使用dompurify 正确保护了dangerouslySetInnerHTML
  • 在我看来,询问安全相关代码的正确性是一个代码审查问题,但也许其他人有更好的想法:)
  • 好的明白你的意思..谢谢你的帮助
【解决方案3】:

你在客户端做什么并不重要,如果一个用户想要输入一堆脚本标签并在他们的客户端上执行一堆东西,让他们去做吧。最坏的情况是它只会弄乱他们自己的浏览器。不需要清理任何东西,根本不需要关心,你可以直接设置innerHTML并显示他们输入的内容。

您唯一真正关心的是数据何时发送到您的服务器,在这种情况下,您必须剥离所有脚本标签并确保它们没有向其中添加恶意代码。存在 XSS 的问题,即数据被传递到您的服务器,保存,然后显示在其他人的浏览器上。如果这不是您系统中发生的事情,那么您不必关心。如果这是您系统上发生的情况,那么您需要关心的就是剥离脚本标签。

如果您正在从您无法控制的第 3 方站点执行获取请求,那么您唯一需要关心设置 innerHTML 的其他时间,如果您想将该 html 呈现到您的站点中,那么您需要要小心。但即便如此,除非您单独手动创建脚本并使用createElementappendChild 来实际渲染它们,否则即使如此,react 也不会允许任何脚本执行。即使那样你也很安全。如果您从自己的服务器中提取它,那么您不必关心是否使用 https。

【讨论】:

  • 我现在想不出这个名字,但是有一类社会工程攻击,包括说服用户在他们自己的浏览器中输入恶意数据。我认为这是一个合理和审慎的措施。也就是说,您关于 XSS 是主要关注点的观点是完全正确的,并且如果将此类工作存储在服务器上,则最好在服务器上完成。
猜你喜欢
  • 2016-08-27
  • 2012-01-22
  • 2013-02-23
  • 1970-01-01
  • 2011-04-15
  • 2011-08-15
  • 2018-06-28
  • 2011-03-18
  • 2010-11-12
相关资源
最近更新 更多