Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "Remove flight chunk" #1278

Closed
wants to merge 3 commits into from

Conversation

jplhomer
Copy link
Contributor

@jplhomer jplhomer commented May 16, 2022

Reverts Shopify/hydrogen#1272

This PR adds logic to escape script tags which would put the embedded flight script chunks in SSR payloads in a bad or vulnerable state.

Note: This simply prevents the script tag from being terminated too early. Developers who accept untrusted data via inputs should still sanitize the data before sending it to the client.

Inspired by facebook/react#24385

Copy link
Contributor

@blittle blittle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides what you have covered in the test, I've tried a few other ways to get it to break out of the script. So far so good.

Comment on lines +901 to +910
function escapeScriptContent(scriptText: string) {
return ('' + scriptText).replace(scriptRegex, scriptReplacer);
}
const scriptRegex = /(<\/|<)(s)(cript)/gi;
const scriptReplacer = (
match: string,
prefix: string,
s: string,
suffix: string
) => `${prefix}${s === 's' ? '\\u0073' : '\\u0053'}${suffix}`;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. It just replaces s with a unicode version

Copy link

@sabulikia sabulikia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To summarize my understanding:

  1. We are not using template literals for the value that gets passed to push() anymore and instead have replaced that with JSON.stringify() so the case of ${alert(1)} is eliminated
  2. We are preventing script tags being injected by unicode-encoding the s in the script tags to avoid payloads like > <script> alert(1) </script>

Since potential user provided data is being loaded inside a script tag, this solution makes sense. However, IMO a more general and holistic approach would be to avoid such block-listings and instead loading hydrating data within a meta div tag through the data field which then allows for consistently html-encoding all of the user provided data. This way, the global object can be initialized by reading the meta tag, and then the new hydrating data can be appended to it if needed.

However, I'm not clear whether this solution is possible within this context or whether there would be some performance issues so please let me know if that doesn't work. 👍🏻

}

if (chunk) {
script += `__flight.push(${JSON.stringify(escapeScriptContent(chunk))})`;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So just to clarify, JSON.stringify()
eliminates the need for using backticks for the string that is passed to push() i.e. for

`__flight.push(\`${normalizedChunk}\`)`

Correct? This way the case where this would be abused by passing a payload like ${alert(1)} would be no longer be a concern.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct!

function escapeScriptContent(scriptText: string) {
return ('' + scriptText).replace(scriptRegex, scriptReplacer);
}
const scriptRegex = /(<\/|<)(s)(cript)/gi;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is a concern here, but since this is a regex with a global flag, it's stateful and therefore calling RegExp.prototype.test() or RegExp.prototype.exec() on it for two different inputs would be problematic. Do you know if String.prototype.replace() has the same issue or not? I was not able to find anything. 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me that only RegExp methods are stateful, so something like String.match() or String.replace() should be fine 👍 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec

@jplhomer
Copy link
Contributor Author

Closing in favor of #1293

@jplhomer jplhomer closed this May 19, 2022
@jplhomer jplhomer deleted the revert-1272-html-encode-flight branch May 19, 2022 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants