-
Notifications
You must be signed in to change notification settings - Fork 17
Email Template Escaping Followed The Data
Every automated email FreeITSM sends is a template with [merge_codes] in it. Whether that template is treated as HTML β and therefore whether its contents are escaped β was decided by looking at the finished email. Since merge codes are substituted first, a piece of data could make that decision.
Found while adding the Note shared with requester trigger for issue #103. It affected all four triggers that already existed.
Fixed in 66885702, released as update #1203.
An administrator writes a plain-text template:
Hi [requester_first_name],
[note_text]
Thanks.
No HTML. Plain words. FreeITSM's job is to escape it and turn the line breaks into <br> before sending.
Now an analyst writes a note reading:
use the
<table>in room 2
The email that goes out is not escaped at all. Not the note, and not the rest of the template either.
Two steps, in this order:
// includes/template_email.php β sendTemplateEmail()
$body = resolveMergeCodes($template['body_template'], $mergeData); // 1. substitute
β¦
$fullBody = buildTemplateEmailBody($body, $ticketNumber); // 2. assembleAnd step two asked the question by looking at what it was handed:
// before
if (strip_tags($bodyContent) === $bodyContent) {
$bodyContent = nl2br(htmlspecialchars($bodyContent, ENT_QUOTES, 'UTF-8'));
}"If this body contains no tags, it is plain text, so escape it."
Perfectly reasonable β except that by the time it runs, the merge codes are already in. So:
template : "Hi,\n[note_text]" β genuinely plain text
data : "use the <table> in room 2"
merged : "Hi,\nuse the <table> in room 2" β now contains a tag
verdict : "this is HTML"
result : nothing is escaped
A value decided the rule that was supposed to govern it.
The existing merge codes make it very unlikely. [ticket_reference], [requester_name], [created_date], [ticket_url] β short, structured, drawn from database columns that do not normally contain angle brackets.
[note_text] is different in kind: free-form, multi-line prose typed by an analyst who has no idea it is about to be merged into an email template. It is the first merge code likely to contain a < at all, which is how the flaw surfaced the moment one was added.
Whether a template is HTML is a property of what the administrator wrote, not of today's data. So the question is asked of the template, before anything is merged:
$bodyIsHtml = strip_tags($template['body_template']) !== $template['body_template'];and the answer is carried forward rather than re-derived:
$fullBody = buildTemplateEmailBody($body, $ticketNumber, $bodyIsHtml);buildTemplateEmailBody() keeps the old sniff behind a null default, because one caller β the workflow engine β has no template to inspect.
Caller-supplied merge data is free-form text, so it is escaped when it is about to land inside HTML:
if ($bodyIsHtml) {
foreach ($extraMergeData as $k => $v) {
$mergeData[$k] = nl2br(htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'));
}
}Only when the template is HTML. For a plain-text template the whole body is escaped afterwards, and escaping first as well would show the customer literal <br> and &lt; β a double-escaping bug in place of the original one.
Values built from the ticket are left as they are. They render correctly today, and widening the change to them was not this fix's job.
| File | |
|---|---|
includes/template_email.php |
the verdict moved to the template; buildTemplateEmailBody() takes it as an argument |
The failing case is demonstrated directly rather than described β the same merged body, judged both ways:
sniffing the merged body escapes it : NO - unescaped
telling it the template was plain : yes - PASS
And a value chosen to fight back β Use the <table> in room 2\nLine two & "quoted", carrying a tag, an ampersand, a quote and a newline β checked in both directions:
| HTML template β the tag is neutralised | PASS |
HTML template β the newline survives as <br>
|
PASS |
HTML template β no live <table> reaches the mail |
PASS |
| plain template β escaped exactly once, not twice | PASS |
That last row is the one worth keeping. It is the check that catches an over-eager fix.
Nothing to do. Templates behave the way they always appeared to.
If you write plain-text templates, they now stay plain text regardless of what turns up in a ticket. If you write HTML templates, a merge value can no longer break your layout β or arrive as markup you did not write.
- Email template sender rules
- Ticket notes β developer guide β Β§5 covers this rule for anyone adding a trigger
- "Share with Requester" did less than it said
- Bugs resolved
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)