TL;DR
When building HTML strings manually in Phoenix, use Phoenix.HTML.html_escape/1 then Phoenix.HTML.safe_to_string/1. Prevents XSS from user content.
The Problem
You’re generating HTML dynamically - maybe parsing markdown-like syntax or building custom tags. User content goes into your HTML string.
# DANGEROUS - XSS vulnerability
defp format_heading(text) do
~s(<h2>#{text}</h2>)
end
If text is <script>alert('xss')</script>, you’ve got a problem.
The Solution
defp format_heading(text) do
escaped = text |> Phoenix.HTML.html_escape() |> Phoenix.HTML.safe_to_string()
~s(<h2>#{escaped}</h2>)
end
The two-step process:
html_escape/1returns a safe tuple{:safe, "escaped content"}safe_to_string/1extracts the escaped string
Real Example
Parsing simple markdown-like content:
defp format_block("## " <> heading) do
escaped = heading |> Phoenix.HTML.html_escape() |> Phoenix.HTML.safe_to_string()
~s(<h3 class="detail__h3">#{escaped}</h3>)
end
defp format_block("- " <> item) do
escaped = item |> Phoenix.HTML.html_escape() |> Phoenix.HTML.safe_to_string()
~s(<li>#{escaped}</li>)
end
defp format_block(paragraph) do
escaped = paragraph |> Phoenix.HTML.html_escape() |> Phoenix.HTML.safe_to_string()
~s(<p class="detail__p">#{escaped}</p>)
end
When Phoenix Does It Automatically
In HEEx templates, interpolation is auto-escaped:
<h2>{@user_content}</h2> <%# Automatically escaped %>
Manual escaping is only needed when building HTML strings outside templates.