Understanding XSS Attacks
Cross-Site Scripting (XSS) happens when attackers inject malicious scripts into your web pages. If you display user input without proper escaping, attackers can steal cookies, session tokens, or redirect users to malicious sites.
<%-- DANGEROUS: Never do this --%>
<%- userComment %>
If userComment contains <script>stealCookies()</script>, it will execute in the user's browser.
Escaped vs Unescaped Output
EJS provides two output tags: <%= %> for escaped output and <%- %> for unescaped output. Understanding the difference is critical for security.
<%= user.name %>
This converts special characters to HTML entities. < becomes <, > becomes >, and so on.
<%- user.name %>
This outputs the raw HTML. Only use this when you trust the source completely.
<%= '<script>alert("xss")</script>' %>
<%-- Renders as: <script>alert("xss")</script> --%>
<%- '<script>alert("xss")</script>' %>
<!-- Renders as an actual script tag! -->
User Input Sanitization
Always sanitize user input before displaying it. Use a sanitization library to strip dangerous content.
const sanitizeHtml = require('sanitize-html');
app.post('/comment', (req, res) => {
const clean = sanitizeHtml(req.body.comment, {
allowedTags: ['b', 'i', 'em', 'strong'],
allowedAttributes: {}
});
res.render('comment', { comment: clean });
});
This allows basic formatting while removing potentially dangerous tags and attributes.
<div class="comment">
<%= comment %>
</div>
Try it Yourself →
Content Security Policy
Content Security Policy (CSP) headers add an extra layer of protection by restricting which resources can load on your pages.
const helmet = require('helmet');
app.use(helmet());
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"]
}
}));
Even if an XSS attack succeeds, CSP can prevent the injected script from executing or sending data to external servers.
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">
</head>
Safe Rendering Patterns
Combine multiple safety measures for defense in depth. Never rely on just one protection method.
<%-- Always escape by default --%>
<p><%= userInput %></p>
<%-- Only use unescaped output for trusted content --%>
<%- trustedHtmlContent %>
<%-- Validate before displaying --%>
<% if (typeof userInput === 'string' && userInput.length < 1000) { %>
<p><%= userInput %></p>
<% } %>
<%-- Use attributes carefully --%>
<div data-content="<%= userInput %>"></div>
Never use user input directly in JavaScript, event handlers, or href attributes without proper encoding.
<%-- DANGEROUS --%>
<a href="<%= userUrl %>">Click here</a>
<%-- SAFER --%>
<a href="/redirect?url=<%= encodeURIComponent(userUrl) %>">Click here</a>
Try it Yourself →