Email template rendering that happens in the browser exposes your personalization logic, merge-field variable names, template structure, and potentially sensitive business rules to any user who opens DevTools. For SaaS products, this can reveal subscription tier logic, A/B test variants, or internal campaign identifiers embedded in templates. CWE-200 covers exposure of sensitive information to an unauthorized actor — shipping template compilation into the client bundle is the mechanism. OWASP A02 applies because the information asymmetry enables targeted attacks against the application's business logic.
High because client-side template rendering exposes personalization logic and merge-field schemas to any user who inspects the browser bundle.
Move all template rendering to server-side code — worker files, Next.js Server Actions, or API routes. In workers/email.worker.ts:
import Handlebars from 'handlebars'
import fs from 'fs'
import path from 'path'
const template = Handlebars.compile(
fs.readFileSync(path.join(process.cwd(), 'templates', 'welcome.hbs'), 'utf-8')
)
export async function renderEmail(mergeFields: Record<string, string>): Promise<string> {
return template(mergeFields) // Node.js only — never in a browser bundle
}
For React Email, call render() exclusively from 'use server' code or worker processes, never from a component file that could be included in the client bundle.
ID: sending-pipeline-infrastructure.template-engine.server-side-rendering
Severity: high
What to look for: Verify that email HTML is assembled on the server, not in the browser. Check that the template engine (Handlebars, MJML, React Email, EJS, Nunjucks, etc.) is invoked in server-side code (worker files, API routes, server actions), not in browser-facing React components or client-side JavaScript. Look for template compilation or rendering calls that happen in code that runs in a browser context.
Pass criteria: Email HTML is rendered exclusively in server-side code. Count all template rendering call sites — 100% must be in worker files, Node.js scripts, or server functions. No template rendering occurs in client-side bundles. List all files where template rendering is called and confirm each is server-only.
Fail criteria: Email templates are rendered in client-side code, exposing the full email structure, merge field logic, or personalization rules to end users via the browser bundle.
Skip (N/A) when: Never — email template rendering must always happen server-side.
Detail on fail: "Email template rendered in a React component that ships to the browser — template logic visible in client bundle" or "Handlebars template compiled in a browser-facing utility file imported by client components"
Remediation: Move all template rendering to server-side code:
// workers/email.worker.ts — correct: runs only in Node.js process
import Handlebars from 'handlebars'
import fs from 'fs'
import path from 'path'
const templateSource = fs.readFileSync(
path.join(process.cwd(), 'templates', 'welcome.hbs'),
'utf-8'
)
const template = Handlebars.compile(templateSource)
async function renderEmail(mergeFields: Record<string, string>): Promise<string> {
return template(mergeFields) // Runs in Node.js, never in the browser
}
Or using React Email (which renders to HTML in a server context):
// lib/email/render.ts
import { render } from '@react-email/render'
import { WelcomeEmail } from '@/emails/welcome'
// This must be called from server-side code only
export function renderWelcomeEmail(props: WelcomeEmailProps): string {
return render(<WelcomeEmail {...props} />)
}