Handlebars: use SafeString helpers instead of triple mustache for user content
Why triple-stash raw output risks XSS and how SafeString helpers centralize sanitization in Handlebars 4.x templates.
10 Dec 2025, 04:39 UTC

A user bio that turns into an XSS payload
You are rendering a profile page with Handlebars, and a user submitted this as their bio:
<img src=x onerror=alert(1)>
Your template uses {{{bio}}} because the product team wants bold text and links to render. The triple stash tells Handlebars not to HTML-escape the value, so the browser executes the image tag’s error handler. The same template with {{bio}} would have neutralized it by converting < and > into entities.
Takeaway: default escaping protects you from untrusted input. Raw output is acceptable only after explicit sanitization, and a SafeString helper is the better place to enforce that rule than scattered triple-stash tags.
In Handlebars 4.x, {{variable}} escapes <, >, &, ", and ' by default. {{{variable}}} and {{&variable}} render the value raw. This behavior is stable across the 4.x line, though minor releases can adjust edge cases around null, undefined, and SafeString interop.
Worked example: a bold-name helper that escapes before marking safe
Instead of writing {{{name}}} in every template, register a helper that owns the sanitization decision.
const Handlebars = require('handlebars');
function boldName(name) {
if (typeof name !== 'string') {
return '';
}
const escaped = Handlebars.Utils.escapeExpression(name);
return new Handlebars.SafeString('' + escaped + '');
}
Handlebars.registerHelper('boldName', boldName);
Run this in your terminal at the project root. No special permissions are required.
# Substitute the minor version your project already uses.
npm install handlebars@^4.7.7
node -e "
const Handlebars = require('handlebars');
Handlebars.registerHelper('boldName', (name) => {
if (typeof name !== 'string') return '';
return new Handlebars.SafeString(
'' + Handlebars.Utils.escapeExpression(name) + ''
);
});
const tpl = Handlebars.compile('{{boldName user}}');
console.log(tpl({ user: '<script>alert(1)</script>' }));
"
This command writes to package.json and package-lock.json. Revert by restoring those files from version control.
Expected check: the printed markup contains <strong><script>alert(1)</script></strong>. The tags are visible on the page, but the script cannot execute because the angle brackets were escaped before the result was marked safe. If you omit escapeExpression inside the helper, SafeString still suppresses Handlebars escaping and you reintroduce the vulnerability.
When raw output is defensible
Use {{{ }}} only for content you fully trust or that has passed through a dedicated sanitizer. Examples include help text written by your team or a CMS body processed by a server-side HTML sanitizer. Never pass raw form input, comment text, or profile fields straight to a triple stash.
Trade-offs and limitations
SafeStringdoes not sanitize. It only tells Handlebars not to escape what the helper returns.- Helper names can collide with built-in keywords, and re-registering an existing helper silently overwrites it in some environments.
- Centralizing markup logic in helpers adds indirection. A small site may prefer explicit escaping plus a template audit for every
{{{occurrence.
Verify before you deploy
- Check the installed version:
npm list handlebars. - Render
{{bad}}and{{{bad}}}with{ bad: '<img src=x onerror=alert(1)>' }and confirm the first output is escaped while the second is raw. - Search your templates for
{{{and map each one to a trusted source or a sanitizing helper.
Closing
Handlebars makes the safe choice the default. Keep it that way by reserving raw output for sanitized or trusted content, and put that decision inside a SafeString helper where it can be reviewed in one place.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.