# Templates rendered server-side

- **Pattern:** `ab-002466` (`sending-pipeline-infrastructure.template-engine.server-side-rendering`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002466
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002466)

## Why it matters

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.

## Severity rationale

High because client-side template rendering exposes personalization logic and merge-field schemas to any user who inspects the browser bundle.

## Remediation

Move all template rendering to server-side code — worker files, Next.js Server Actions, or API routes. In `workers/email.worker.ts`:

```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.

## Detection

- **ID:** `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:

  ```ts
  // 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):

  ```ts
  // 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} />)
  }
  ```

## External references

- cwe CWE-200
- owasp A02

Taxons: access-control

HTML version: https://auditbuffet.com/patterns/ab-002466
