【问题标题】:Insert HTML into user string and render it in React (and avoid XSS threat)将 HTML 插入用户字符串并在 React 中呈现(并避免 XSS 威胁)
【发布时间】:2019-10-21 03:21:33
【问题描述】:

用户正在向我们的 React 应用程序提供一个字符串,并且它正在显示给其他用户。我想搜索一些字符,并用一些 HTML 替换它们,就像我要搜索“特殊”这个词一样,我会把它变成:

My <span class="special-formatting">special</span> word in a user string

以前我执行此替换,然后使用危险的SetInnerHTML 将结果插入到 DOM 中。这当然给我带来了用户能够在应用程序中键入和输入他们喜欢的任何 HTML/Javascript 并将其呈现给所有人查看的问题。

我尝试将 HTML 字符转义为它们的实体,但危险的 SetInnerHTML 似乎正确地呈现 HTML 实体,而不是实际的字符串。 (编辑:见下文,这是实际的解决方案)

有什么方法可以将他们的消息转换为纯字符串,仍然保留那些特殊字符的显示,而且还可以将我自己的 HTML 插入到字符串中?尽量避免在将每个字符串插入 DOM 后运行脚本。


以下是有关当前流程的更多信息。所有示例都经过优化,只显示相关代码。

用户文本通过这个函数提交到数据库:

handleSubmit(event) {
        event.preventDefault();

        var messageText = this.state.messageValue;

        //bold font is missing some common characters, fake way of making the normal font look bold
        if (this.state.bold == true) {
            messageText = messageText.replace(/\'/g, "<span class='bold-apostrophe'>'</span>");
            messageText = messageText.replace(/\"/g, "<span class='bold-quote'>&quot;</span>");
            messageText = messageText.replace(/\?/g, "<span class='bold-question'>?</span>");
            messageText = messageText.replace(/\*/g, "<span class='bold-asterisk'>*</span>");
            messageText = messageText.replace(/\+/g, "<span class='bold-plus'>+</span>");
            messageText = messageText.replace(/\./g, "<span class='bold-period'>.</span>");
            messageText = messageText.replace(/\,/g, "<span class='bold-comma'>,</span>");
        }

        Messages.insert({
            text: messageText,
            createdAt: new Date(),
            userId: user._id,
            bold: this.state.bold,
        });

    }

所以,我进行了替换没有问题,但是此时,messageText 字符串仍可能包含不需要的用户输入 HTML 代码。

然后,我们带有消息列表的主应用程序会尝试呈现所有用户消息:

render() {
    return (
        <div ref="messagesList">
            {this.renderMessages()}
        </div>
    );
}
renderMessages() {
    return [].concat(this.props.messages).reverse().map((message) => {
        return <Message
            key={message._id}
            message={message} />;
        }
    });
}

在 Message.jsx 中,我对消息字符串进行最后润色(某些更改我不想保存到消息数据库中)并将其插入到要返回的元素中:

export default class Message extends React.Component {
    render() {

        var processedMessageText = this.props.message.text;

        //another find and replace to insert images for :image_name: strings, similar to how Discord inputs its emoji
        processedMessageText = processedMessageText.replace(/:([\w]+):/g, function (text) {
            text = text.replace(/:/g, "");
            if (text.indexOf("_s") !== -1) {
                text = text.replace(/_s/g, "");
                text = "<img class='small-smiley' src='/smileys/small/" + text + ".png'>";
                return text;
            }
            else {
                text = "<img class='smiley' src='/smileys/" + text + ".png'>";
                return text;
            }
        });

        return (
            <div>
                <div className='username'>{this.props.message.username}: </div>
                <div className='text' dangerouslySetInnerHTML={{ __html: processedMessageText }}></div>
            </div>
        );
    }
}

同样,如果用户在他们的输入字符串中包含恶意 HTML,它将遍历所有这些并输出到消息列表,这真的很糟糕。我希望有某种方法可以将这些所需的 HTML 插入到他们的字符串中,同时也不会将他们可能输入的 HTML 呈现为实际的 HTML。我还想显示 HTML 中常用的字符,比如尖括号 (),所以我想避免直接剥离它们输入的常见 HTML 字符字符串。


由于接受的答案没有太多细节,我将在这里发布我最终所做的事情。在添加我自己的 HTML 并将其呈现为 HTML 元素的内容之前,我对 OWASP 建议的字符进行了 HTML 编码。我想避免使用另一个库,所以我这样做了:

messageText = messageText.replace(/\&/g, "&amp;");
messageText = messageText.replace(/</g, "&lt;");
messageText = messageText.replace(/>/g, "&gt;");
messageText = messageText.replace(/\//g, "&#x2F;");
messageText = messageText.replace(/\'/g, "&#x27;");
messageText = messageText.replace(/\"/g, "&quot;");

这样做后,我不再能够插入任何恶意内容,并且使用来自 OWASP 的各种测试字符串进行了测试,没有问题。

【问题讨论】:

  • 来自服务器的字符串中是否有 html 标记?如果不是,为什么不能将字符串拆分为单词并有条件地呈现匹配的单词。
  • 我没有任何问题有条件地呈现匹配的单词。我的问题是用户可以输入“hello”,由于我使用的是危险的SetInnerHTML,它实际上会呈现该 HTML。
  • 我的意思是,如果您不想维护来自服务器的任何 html 标签,您可以使用普通的旧反应。 return response.split(" ").map((w) =&gt; w === 'special' ? &lt;span className='special-formatting'&gt;{w}&lt;/span&gt; ? w);` .这对 XSS 来说是安全的
  • 嗯,当我们渲染消息时,它是一个映射消息并将它们作为相当大的组件返回的函数。这些组件的缩写形式是:&lt;div className='text' dangerouslySetInnerHTML={{ __html: processedMessageText }}&gt;&lt;/div&gt; 其中 processesMessageText 是我拉入其自己的变量并对其执行各种替换的用户字符串。这又给我留下了一个字符串,我可以将其呈现为带有潜在危险代码的 HTML,也可以呈现为没有 HTML 的纯字符串
  • 添加了我们完整工作流程的示例。鉴于当前的结构,我不确定我在哪里实现了类似的东西。

标签: javascript html reactjs


【解决方案1】:

当您在将用户输入文本保存到数据库之前将 HTML 注入其中时,问题就开始了。 这让事情变得很困难,因为现在你必须对其进行消毒,但不是那么多。

作为补救措施,您可以使用 dompurifysanitize-html 删除除您注入的 html 之外的任何 html。下面是一个使用 dompurify 的示例:

import DOMPurify from "dompurify";

const dangerousString =
"<img onError='alert(\"h4ck3r\")' src='will throw error' /><span class='bold-apostrophe'>'</span>";

<div
  dangerouslySetInnerHTML={{
    __html: DOMPurify.sanitize(dangerousString, {
      ALLOWED_TAGS: ["span"],
      ALLOWED_ATTR: ["class"]
    })
  }}
/>
  • 请记住sanitizer libs needs to be updated as frequently as possible,因为黑客一直在寻找创造性的方法来绕过它们。
  • 前面的陈述暗示您仍然可能会受到 XSS 攻击。避免它的唯一方法是在将其保存到数据库之前停止使用 HTML 调整字符串,因此您可以使用 一种类似于 Ferrybig 提出的解决方案,用于动态添加特殊格式,而不是 dangerouslySetInnerHTML

【讨论】:

    【解决方案2】:

    你就不能

    1. HTML 编码来自用户的污染字符串。
    2. 进行搜索/替换并插入 HTML。
    3. 然后执行dangerouslySetInnerHTML()

    这应该可以安全地转义用户输入的任何内容,并单独留下您插入的 HTML 元素,不是吗?

    【讨论】:

    • 我尝试使用查找替换(如 < for
    • 原来如此。无论我以前做过什么测试,一定是有问题的。您可以非常轻松地先对所有问题字符进行编码,然后再将其注入元素内容,尤其是因为我们不会在元素属性或类似的任何奇怪的东西中注入。
    • @addMitt 因为这个问题引起了一些关注,我认为如果您可以使用最终使用的代码编辑您的帖子,或者发布另一个答案,那就太好了。
    • @ebessa 完成!虽然不多。有点尴尬哈哈。
    【解决方案3】:

    这将是我的方法,我希望它不会来得太晚。

    import React, { render } from "react";
    import ReactDOM from "react-dom";
    import sanitizeHtml from "sanitize-html";
    
    // This is the place where you need to do all the magic you want to do
    let SpecialTextOutPut = ({ text }) => {
      const newText = text.replace("World", "<b>Transforming Elements</b>");
      return React.createElement("div", {
        dangerouslySetInnerHTML: { __html: `${newText}` }
      });
    };
    
    // You can sanitize and clean up the user input here
    let UserTextInput = text =>
      React.createElement(SpecialTextOutPut, {
        text: sanitizeHtml(text)
      });
    
    function App() {
      return <div>{UserTextInput("~Hello World <span>Poll</span>")}</div>;
    }
    
    const rootElement = document.getElementById("root");
    ReactDOM.render(<App />, rootElement);
    
    

    【讨论】:

      【解决方案4】:

      另一种解决方案是将搜索词手动转换为 JSX 元素。由于典型的搜索不使用正则表达式,我们可以只使用.indexof 来分割字符串(虽然支持正则表达式并不难,因为它也有匹配索引。)

      function highlightText(input/*: string */, searchTerm/*: string*/)/*: ReactNode */ {
          let index = input.indexOf(searchTerm);
          let lastIndex = 0;
          let result/*: ReactNode[] */ = []
          while(index >= 0) {
              result.push(<span key={result.length}>{input.substring(lastIndex, index)}<\span>);
              result.push(<mark key={result.length}>{input.substring(index, index + searchTerm.length)}<\span>);
              lastIndex = index + searchTerm.length;
              index = input.indexOf(searchTerm, lastIndex);
          }
          result.push(<span key={result.length}>{input.substring(lastIndex, input.length)}<\span>);
          return result;
      }
      

      然后您可以在组件的渲染部分中调用它,例如:

      function MyComponent(props) {
          return <p>
              {highlightText(props.input, props.searchTerm)}
          <\p>;
      }
      

      【讨论】:

      • 我刚刚意识到我误读了市长的部分问题,这使得这个答案可能毫无用处,我目前无法删除帖子。因为我使用的是 Android SE 应用程序,它在浏览器中打开的按钮坏了。
      • 我也更新了我的问题,以提供我们当前工作流程的一些示例。
      • 在我看来,您需要一个可以处理图像和特殊字符格式等问题的解析器,因为简单的搜索 + 替换系统可能会给保持安全带来很多麻烦
      【解决方案5】:

      这很棘手,在一个字符串中呈现 HTML,但不将整个字符串呈现为 HTML...

      我会采取不同的方法,如果可以的话,我会在最后替换你,这可能会使它更简单。下面是一个示例,说明如何使用 textContent 在 DOM 中获取整个字符串,然后使用 innerHTML 仅呈现您想要的部分。

      var ele = document.getElementById('message');
      
      // User entered string will not be rendered as HTML
      ele.textContent = '<div onclick="maliciousCode()">*</div>'; 
      
      // Do replacement using innerHTML to render only some parts
      ele.innerHTML = ele.innerHTML.replace(/\*/g, '<span class="bold">*</span>') 
      .bold { font-weight: 700 }
      &lt;div id="message"&gt;&lt;/div&gt;

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-08-17
        • 2012-03-18
        相关资源
        最近更新 更多