51

I'm trying to set html sent from my server to show inside a div using dangerouslySetInnerHTML property in React. I also have script tag inside it and use functions defined in same inside that html. I have made example of error in JSFiddle here.

This is test code:

var x = '<html><scr'+'ipt>alert("this.is.sparta");function pClicked() {console.log("p is clicked");}</scr'+'ipt><body><p onClick="pClicked()">Hello</p></body></html>';

var Hello = React.createClass({
  displayName: 'Hello',
  render: function() {
    return (<div dangerouslySetInnerHTML={{__html: x}} />);
  }
});

I checked and the script tag is added to DOM, but cannot call the functions defined within that script tag. If this is not the correct way is there any other way by which I can inject the script tag's content.

4
  • 1
    You can't put a <html> inside a <div>. Commented Feb 24, 2016 at 22:55
  • @fl0cke you can that's not an issue. If that would have been a problem then the contents would not be visible, but they are properly given any complex html, but the problem is with script tag. And even if you remove the html tag and just put <script> and <p> tag within string and try to put them in div then also it doesn't work. Commented Feb 24, 2016 at 23:04
  • 1
    Maybe this question can help: stackoverflow.com/questions/1197575/… Commented Feb 24, 2016 at 23:20
  • No idea why it doesn't work. It works when you just use document.createElement to create the script tag. Commented Feb 24, 2016 at 23:30

7 Answers 7

56

I created a React component that works pretty much like dangerouslySetInnerHtml but additionally it executes all the js code that it finds on the html string, check it out, it might help you:

https://www.npmjs.com/package/dangerously-set-html-content

Sign up to request clarification or add additional context in comments.

9 Comments

Awesome job Christofer. This to me seemed by far the simplest solutions. I am using typescript (and don't know how to type your library), so I ended up just stealing your code and writing my own component based on yours. Here is a link to the component Christofer wrote: github.com/christo-pr/dangerously-set-html-content/blob/master/…
Very nice! I didn't remember dangerouslySetInnerHTML didn't work with <script> tags. Your component worked perfectly.
Yeah, it's for security reasons, but good to know that it help you!
This should be the accepted answer, as it's the most elegant solution to the problem.
|
25

Here's a bit of a dirty way of getting it done , A bit of an explanation as to whats happening here , you extract the script contents via a regex , and only render html using react , then after the component is mounted the content in script tag is run on a global scope.

var x = '<html><scr'+'ipt>alert("this.is.sparta");function pClicked() {console.log("p is clicked");}</scr'+'ipt><body><p onClick="pClicked()">Hello</p></body></html>';

var extractscript=/<script>(.+)<\/script>/gi.exec(x);
x=x.replace(extractscript[0],"");

var Hello = React.createClass({
  displayName: 'Hello',
  componentDidMount: function() {
    // this runs the contents in script tag on a window/global scope
    window.eval(extractscript[1]);

  },
  render: function() {
    return (<div dangerouslySetInnerHTML={{__html: x}} />);
  }
});

ReactDOM.render(
  React.createElement(Hello),
  document.getElementById('container')
);

2 Comments

What if there are multiple script tags?
for multi line script tag, the reg expression might not works, try var extractscript=/<script>[\s\S]*<\/script>/gi.exec(x);
7

I don't think you need to use concatenation (+) here.

var x = '<html><scr'+'ipt>alert("this.is.sparta");function pClicked() {console.log("p is clicked");}</scr'+'ipt><body><p onClick="pClicked()">Hello</p></body></html>';

I think you can just do:

var x = '<html><script>alert("this.is.sparta");function pClicked() {console.log("p is clicked");}</script><body><p onClick="pClicked()">Hello</p></body></html>';

Since it's passed to dangerouslySetInnerHTML anyway.

But let's get back to the issue. You don't need to use regex to access the script tag's content. If you add id attribute, for example <script id="myId">...</script>, you can easily access the element.

Let's see an example of such implementation.

const x = `
  <html>
    <script id="myScript">
      alert("this.is.sparta");
      function pClicked() {console.log("p is clicked");}
    </script>
    <body>
      <p onClick="pClicked()">Hello</p>
    </body>
  </html>
`;

const Hello = React.createClass({

  displayName: 'Hello',

  componentDidMount() {
    const script = document.getElementById('myScript').innerHTML;
    window.eval(script);
  }

  render() {
    return <div dangerouslySetInnerHTML={{__html: x}} />;
  }

});

If you have multiple scripts, you can add a data attribute [data-my-script] for example, and then access it using jQuery:

const x = `
  <html>
    <script data-my-script="">
      alert("this.is.sparta");
      function pClicked() {console.log("p is clicked");}
    </script>
    <script data-my-script="">
      alert("another script");
    </script>
    <body>
      <p onClick="pClicked()">Hello</p>
    </body>
  </html>
`;

const Hello = React.createClass({

  constructor(props) {
    super(props);

    this.helloElement = null;
  }

  displayName: 'Hello',

  componentDidMount() {
    $(this.helloElement).find('[data-my-script]').each(function forEachScript() {
      const script = $(this).text();
      window.eval(script);
    });
  }

  render() {
    return (
      <div
        ref={helloElement => (this.helloElement = helloElement)} 
        dangerouslySetInnerHTML={{__html: x}} 
      />
    );
  }

});

In any case, it's always good to avoid using eval, so another option is to get the text and append a new script tag with the original's script contents instead of calling eval. This answer suggests such approach

Comments

4

a little extension for Dasith's answer for future views...

I had a very similar issue but the in my case I got the HTML from the server side and it took a while (part of reporting solution where backend will render report to html)

so what I did was very similar only that I handled the script running in the componentWillMount() function:

import React from 'react';
import jsreport from 'jsreport-browser-client-dist'
import logo from './logo.svg';
import './App.css';

class App extends React.Component {
    constructor() {
        super()
        this.state = {
            report: "",
            reportScript: ""
        }
    }

    componentWillMount() {
        jsreport.serverUrl = 'http://localhost:5488';
        let reportRequest = {template: {shortid: 'HJH11D83ce'}}
        // let temp = "this is temp"
        jsreport.renderAsync(reportRequest)
            .then(res => {
                let htmlResponse = res.toString()
                let extractedScript = /<script>[\s\S]*<\/script>/g.exec(htmlResponse)[0];
                // console.log('html is: ',htmlResponse)
                // console.log('script is: ',extractedScript)
                this.setState({report: htmlResponse})
                this.setState({reportScript: extractedScript})
            })
    }

    render() {
        let report = this.state.report
        return (
            <div className="App">
                <div className="App-header">
                    <img src={logo} className="App-logo" alt="logo"/>
                    <h2>Welcome to React</h2>
                </div>
                <div id="reportPlaceholder">
                    <div dangerouslySetInnerHTML={{__html: report}}/>

                </div>
            </div>
        );
    }

    componentDidUpdate() {
        // this runs the contents in script tag on a window/global scope
        let scriptToRun = this.state.reportScript
        if (scriptToRun !== undefined) {
            //remove <script> and </script> tags since eval expects only code without html tags
            let scriptLines = scriptToRun.split("\n")
            scriptLines.pop()
            scriptLines.shift()
            let cleanScript = scriptLines.join("\n")
            console.log('running script ',cleanScript)
            window.eval(cleanScript)
        }

    }
}

export default App;

hope this is helpful...

Comments

2

Just use some known XSS tricks. We just had a case where we had to inject a script and couldn't wait for the release so here goes our loader:

<img src onerror="var script = document.createElement('script');script.src = 'http:';document.body.appendChild(script);"/>

Comments

0

I was facing the same issue while integrating the payment gateway. And I solved by just using the below codes:

  useEffect(() => {
    if (htmlContent) {
      window.eval(document.f1.submit());
    }
  }, [htmlContent]);

I added the same script code inside the window.eval, which worked.

Here are the full codes:

import React, { useState, useEffect } from "react";
import { get } from "../../../services/http-apis";

const OrderMainPage = () => {
  const [htmlContent, setHtmlContent] = useState(null);

  useEffect(() => {
    callPaymentAPI();
  }, [someDependenciy]);

  useEffect(() => {
    if (htmlContent) {
      window.eval(document.f1.submit());
    }
  }, [htmlContent]);

  const callPaymentAPI = async () => {
    try {
      const response = await get(`/api/v1/pay`);
      setHtmlContent(response);


      // This is what I am getting in the response:
      // <html>
      //   <head>
      //     <title>Merchant Checkout Page</title>
      //   </head>
      //   <body>
      //     <center><h1>Please do not refresh this page...</h1></center>
      //     <form
      //       method="post"
      //       action="https://securegw.paytm.in/theia/processTransaction"
      //       name="f1"
      //     >
      //       /* SOME HTML CONTENT */
      //     </form>
      //     <script type="text/javascript">document.f1.submit();</script>
      //   </body>
      // </html>



    } catch (err) {
      setHtmlContent(null);
    }
  };

  if (htmlContent) {
    return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
  }

  return <>Hello</>;
};

export default OrderMainPage;

Comments

0

Try to avoid dangerouslySetInnerHTML, you can then set the html using jQuery in the useEffect hook i.e. once the component is mounted.

    useEffect(() => {
//htmlResponse containing script tag
    $('#testId').html(htmlResponse);
},[]);
return <div id="testId">         
 </div>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.