I couldn't find a non-public channel on which to communicate the security vulnerability. Here it goes:
Severity
High. The injected code could be executed on any page in which the malicious input is rendered and can affect any visitor, including site admins. This could allow attackers to hijack sessions, execute any Javascript on the browser of the visitor or even include their own HTML into the page.
Problem
Normally Django would take care of escaping HTML tags in templates when including model fields and other variables in the rendered template. But in this case, the React props are simply rendered as a JSON object in a script element.
Since the JSON inside the React.createElement(...)
is not properly escaped, a malicious user is able to inject any custom HTML/Javascript into the rendered templates.
Simple example
- Create any model with fields that can be set by the user, for example:
UserProfile(name: str, bio: str, ...)
- Let the user fill in the data as usual: could be Django form, React component, REST API, etc...
- Populate any React component using this library, passing any of those fields as a prop:
{% load react %}
<html>
<head>...</head>
<body>
{% react_render component="UserProfileComponent" prop_name=user_profile.name %}
</body>
{% react_print %}
</html>
- If a user sets the name to be
</script><script>alert('Foo')</script>
, it will be rendered in the template as follows:
<html>
<head>...</head>
<body>
<div id="UserProfileComponent_UUID"></div>
</body>
<script>
ReactDOM.render(
React.createElement(UserProfileComponent, {"name": "</script><script>alert('Foo')</script>"}),
document.getElementById('UserProfileComponent_UUID')
);
</script>
</html>
- The first
</script>
ends the script element in which the React render snippet is placed, and executes the malicious code.
Proposed solution
I think that one of the following would prevent this vulnerability:
- Preferred: make use of Django's
json_script
to handle escaping of malicious code and render the JSON object into the template.
https://docs.djangoproject.com/en/3.1/ref/templates/builtins/#json-script
<html>
<head>...</head>
<body>
<div id="UserProfileComponent_UUID"></div>
</body>
<script id="UserProfileComponent_props_UUID" type="application/json">{"name": "\\u003C/script\\u003E..."}</script>
<script>
ReactDOM.render(
React.createElement(UserProfileComponent, JSON.parse(document.getElementById('UserProfileComponent_props_UUID').textContent)),
document.getElementById('UserProfileComponent_UUID')
);
</script>
</html>
- Base64 encode the stringified props JSON on render, and decode within the
<script>...</script>
element on the browser. For example:
<html>
<head>...</head>
<body>
<div id="UserProfileComponent_UUID"></div>
</body>
<script>
ReactDOM.render(
React.createElement(UserProfileComponent, JSON.parse(atob("eyJuYW1lIjoiPC9zY3JpcHQ+PHNjcmlwdD5hbGVydCgnRm9vJyk8L3NjcmlwdD4ifQ=="))),
document.getElementById('UserProfileComponent_UUID')
);
</script>
</html>
- Recursively escape the strings in the rendered JSON before rendering the props into the template.
Useful links
- https://docs.djangoproject.com/en/3.1/ref/templates/builtins/#json-script
- https://adamj.eu/tech/2020/02/18/safely-including-data-for-javascript-in-a-django-template/