that_individual
that_individual

Reputation: 105

Proper way of server-side rendering JavaScript

I have a multipage website that uses React as a glorified widget toolkit (different bundles on different pages). However, some widget state persists inside of a login session When the user reopens the page, the components should reload their initial state. I currently do this by:

  1. Serving the page along with its JS bundle
  2. AJAX fetching the state from the server when everything is mounted

This seemed wasteful as the backend already know both what the page is, and the initial state of the widgets. So I thought about rendering the initial state, as a plain object inside the served page and have the object fetch it upon mounting.

My initial try rendered the inital state as:

[some other HTML ... ]
<script>
var WIDGET_INITIAL_STATE = {
    startDate: '...',
    filters: ['filter1', 'filter2', ... ]
}
</script>

And then using this global in the component setup. Of course, this is dangerously unsafe as filters is controlled by a text input, so if one were to enter </script> [ arbitrary HTML ] <script>, the above would become:

<script>
var WIDGET_INITIAL_STATE = {
    startDate: '...',
    filters['<script>

    [arbitrary HTML]

</script>', 'filter2']
}
</script>

which is catastrophically bad.

However, if I do my due diligence and sanitize it server-side, there's no easy way of rendering it in React, as passing a string of the form &lt;script&gt; would render that literally, as per this page.

The only solution that I see to getting this to work this way is to escape the offending characters as unicode escapes.

Note: I am aware this is highly unorthodox, but I want to see any better takes, if nothing else than a thought exercise.

Upvotes: 1

Views: 448

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1073998

When outputting the contents of your filters array to the script element, do two things:

  1. Ensure that they're properly-formed string literals. An easy way to do that is to pass the filter string into JSON.stringify.

  2. Ensure that the sequence </script> never occurs literally, which is fairly easily done with a regular expression. (This is only necessary because you're outputting these strings in the body of a script tag; if they were going in an external .js file that was loaded via src, this wouldn't matter.)

So for instance, for each filter string

stringToOutputToFiltersArray = JSON.stringify(filterString.replace(/<\s*\/\s*script\s*>/gm, "<\\/script>"))

// "server side" code
var filters = [
  'filter1',
  '<script> [arbitrary HTML] </sc' + 'ript>',
  'filter3'
];
var inlineScript =
  "<script>\n" +
  "var WIDGET_INITIAL_STATE = {\n" +
  "  startDate: '...',\n" +
  "  filters: [" + filters.map(filter =>
    JSON.stringify(filter.replace(/<\s*\/\s*script\s*>/gm, "<\\/script>"))
  ) + "]\n" +
  "};\n" +
  "</sc" + "ript>";
document.write(inlineScript);
console.log("filters:", filters);
console.log("inlineScript:", inlineScript);
console.log("WIDGET_INITIAL_STATE.filters[1] = ", WIDGET_INITIAL_STATE.filters[1]);
.as-console-wrapper {
  max-height: 100% !important;
}

Upvotes: 1

Related Questions