CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/hex-phoenix-html

Building blocks for working with HTML in Phoenix - provides HTML safety mechanisms, form abstractions, and JavaScript enhancements

Overview
Eval results
Files

html-safety.mddocs/

HTML Safety and Escaping

Phoenix.HTML's security foundation provides comprehensive protection against XSS attacks through automatic HTML escaping, safe content marking, and proper attribute handling. All user data is considered unsafe by default and must be explicitly marked as safe.

Capabilities

Content Safety Marking

Marks content as safe HTML that should not be escaped, allowing raw HTML to be rendered directly in templates.

def raw(content) :: Phoenix.HTML.safe
  # Parameters:
  # - content: iodata | Phoenix.HTML.safe | nil - Content to mark as safe
  #
  # Returns:
  # - Phoenix.HTML.safe - Safe content tuple {:safe, iodata}

Usage Examples:

# Mark HTML string as safe
safe_html = raw("<p>Welcome <strong>User</strong></p>")
# Returns: {:safe, "<p>Welcome <strong>User</strong></p>"}

# Safe content passes through unchanged
already_safe = raw({:safe, "<div>Already safe</div>"})
# Returns: {:safe, "<div>Already safe</div>"}

# Nil becomes empty safe content
empty = raw(nil)
# Returns: {:safe, ""}

HTML Entity Escaping

Escapes HTML entities in content to prevent XSS attacks, converting potentially dangerous characters to their safe HTML entity equivalents.

def html_escape(content) :: Phoenix.HTML.safe
  # Parameters:
  # - content: Phoenix.HTML.unsafe - Content that may contain HTML entities
  #
  # Returns:
  # - Phoenix.HTML.safe - Escaped content as safe iodata

Usage Examples:

# Escape user input
user_input = "<script>alert('XSS')</script>"
safe_output = html_escape(user_input)
# Returns: {:safe, [[[] | "&lt;"], "script", [] | "&gt;", "alert(", ...]}

# Safe content passes through
already_safe = html_escape({:safe, "<p>Safe content</p>"})
# Returns: {:safe, "<p>Safe content</p>"}

# Numbers and other data types are converted
number_escaped = html_escape(123)
# Returns: {:safe, "123"}

Safe Content Conversion

Converts safe iodata to regular strings, ensuring the content was properly marked as safe before conversion.

def safe_to_string(safe_content) :: String.t()
  # Parameters:
  # - safe_content: Phoenix.HTML.safe - Content marked as safe
  #
  # Returns:
  # - String.t() - Regular string representation
  #
  # Raises:
  # - If content is not marked as safe

Usage Examples:

# Convert safe iodata to string
safe_content = {:safe, ["<p>", "Hello", "</p>"]}
result = safe_to_string(safe_content)
# Returns: "<p>Hello</p>"

# Use with html_escape for complete safety
user_data = "<script>alert('XSS')</script>"
safe_string = user_data |> html_escape() |> safe_to_string()
# Returns: "&lt;script&gt;alert('XSS')&lt;/script&gt;"

HTML Attribute Escaping

Escapes HTML attributes with special handling for common attribute patterns, supporting nested data structures and boolean attributes.

def attributes_escape(attrs) :: Phoenix.HTML.safe
  # Parameters:
  # - attrs: list | map - Enumerable of HTML attributes
  #
  # Returns:
  # - Phoenix.HTML.safe - Escaped attributes as iodata

Special Attribute Behaviors:

  • :class: Accepts list of classes, filters out nil and false values
  • :data, :aria, :phx: Accepts keyword lists, converts to dash-separated attributes
  • :id: Validates that numeric IDs are not used (raises ArgumentError)
  • Boolean attributes: true values render as bare attributes, false/nil are omitted
  • Atom keys: Automatically converted to dash-case (:phx_value_idphx-value-id)

Usage Examples:

# Basic attributes
attrs = [title: "Click me", id: "my-button", disabled: true]
escaped = attributes_escape(attrs)
# Returns: {:safe, [" title=\"Click me\" id=\"my-button\" disabled"]}

# Class list handling
attrs = [class: ["btn", "btn-primary", nil, "active"]]
escaped = attributes_escape(attrs)
# Returns: {:safe, [" class=\"btn btn-primary active\""]}

# Data attributes
attrs = [data: [confirm: "Are you sure?", method: "delete"]]
escaped = attributes_escape(attrs)
# Returns: {:safe, [" data-confirm=\"Are you sure?\" data-method=\"delete\""]}

# Phoenix-specific attributes
attrs = [phx: [value: [user_id: 123]]]
escaped = attributes_escape(attrs)
# Returns: {:safe, [" phx-value-user-id=\"123\""]}

# Combined usage
attrs = [
  class: ["btn", "btn-danger"],
  data: [confirm: "Delete user?"],
  phx: [click: "delete_user"]
]
escaped_str = attributes_escape(attrs) |> safe_to_string()
# Returns: " class=\"btn btn-danger\" data-confirm=\"Delete user?\" phx-click=\"delete_user\""

JavaScript Content Escaping

Escapes HTML content for safe inclusion in JavaScript strings, handling special characters that could break JavaScript syntax or enable XSS attacks.

def javascript_escape(content) :: binary | Phoenix.HTML.safe
  # Parameters:
  # - content: binary | Phoenix.HTML.safe - Content to escape for JavaScript
  #
  # Returns:
  # - binary - Escaped string (for binary input)
  # - Phoenix.HTML.safe - Escaped safe content (for safe input)

Escaped Characters:

  • Quotes: "\", '\'
  • Backslashes: \\\
  • Newlines: \n, \r, \r\n\n
  • Script tags: </<\/
  • Unicode separators: \u2028\\u2028, \u2029\\u2029
  • Null bytes: \u0000\\u0000
  • Backticks: ` → `````

Usage Examples:

# Escape user content for JavaScript
user_content = ~s(<script>alert("XSS")</script>)
escaped = javascript_escape(user_content)
# Returns: "<\\/script>alert(\\\"XSS\\\")<\\/script>"

# Use in template JavaScript
html_content = render("user_profile.html", user: @user)
javascript_code = """
$("#container").html("#{javascript_escape(html_content)}");
"""

# Safe content handling
safe_content = {:safe, ~s(<div class="user">'John'</div>)}
escaped_safe = javascript_escape(safe_content)
# Returns: {:safe, "<div class=\\\"user\\\">\\'John\\'</div>"}

CSS Identifier Escaping

Escapes strings for safe use as CSS identifiers, following CSS specification for character escaping in selectors and property values.

def css_escape(value) :: String.t()
  # Parameters:
  # - value: String.t() - String to escape for CSS usage
  #
  # Returns:
  # - String.t() - CSS-safe identifier string

Usage Examples:

# Escape problematic CSS characters
css_class = css_escape("user-123 name")
# Returns: "user-123\\ name"

# Handle numeric prefixes
css_id = css_escape("123-user")
# Returns: "\\31 23-user"

# Use in dynamic CSS generation
user_id = "user@domain.com"
safe_selector = "##{css_escape(user_id)}"
# Returns: "#user\\@domain\\.com"

Template Engine Functions

Core functions from the Phoenix.HTML.Engine module that handle template processing and HTML safety in EEx templates.

Content Encoding for Templates

Converts various content types to HTML-safe iodata for use in template rendering.

def encode_to_iodata!(content) :: iodata
  # Parameters:
  # - content: term - Content to encode (safe tuples, binaries, lists, etc.)
  #
  # Returns:
  # - iodata - HTML-safe iodata representation

Content Type Handling:

  • {:safe, body}: Returns the body directly without encoding
  • nil or "": Returns empty string
  • Binary strings: HTML-escapes the content
  • Lists: Processes through Phoenix.HTML.Safe.List.to_iodata/1
  • Other types: Processes through Phoenix.HTML.Safe.to_iodata/1

Usage Examples:

# Safe content passes through
Phoenix.HTML.Engine.encode_to_iodata!({:safe, "<p>Safe content</p>"})
# Returns: "<p>Safe content</p>"

# Binaries are HTML-escaped
Phoenix.HTML.Engine.encode_to_iodata!("<script>alert('XSS')</script>")
# Returns: HTML-escaped iodata

# Empty values become empty strings
Phoenix.HTML.Engine.encode_to_iodata!(nil)
# Returns: ""

Phoenix.HTML.Engine.encode_to_iodata!("")
# Returns: ""

# Lists are processed via Safe protocol
Phoenix.HTML.Engine.encode_to_iodata!(["<p>", "content", "</p>"])
# Returns: HTML-safe iodata

# Other types use Safe protocol
Phoenix.HTML.Engine.encode_to_iodata!(123)
# Returns: "123"

Phoenix.HTML.Engine.encode_to_iodata!(:hello)
# Returns: "hello" (HTML-escaped)

Direct HTML Escaping

Performs direct HTML escaping on binary strings with optimized performance.

def html_escape(binary) :: iodata
  # Parameters:
  # - binary: binary - String to HTML-escape
  #
  # Returns:
  # - iodata - HTML-escaped iodata structure

Escaped Characters:

  • <&lt;
  • >&gt;
  • &&amp;
  • "&quot;
  • '&#39;

Usage Examples:

# Basic HTML escaping
Phoenix.HTML.Engine.html_escape("<script>alert('XSS')</script>")
# Returns: HTML-escaped iodata

# Preserves non-HTML content
Phoenix.HTML.Engine.html_escape("Hello World")
# Returns: "Hello World"

# Handles quotes and ampersands
Phoenix.HTML.Engine.html_escape(~s(The "quick" & 'brown' fox))
# Returns: HTML-escaped iodata with quotes and ampersand escaped

Template Variable Access

Fetches template assigns with comprehensive error handling and debugging information.

def fetch_assign!(assigns, key) :: term
  # Parameters:
  # - assigns: map - Template assigns map
  # - key: atom - Assign key to fetch
  #
  # Returns:
  # - term - The assign value
  #
  # Raises:
  # - ArgumentError - With detailed error message if assign not found

Usage Examples:

# Successful assign access
assigns = %{user: %{name: "John"}, title: "Welcome"}
user = Phoenix.HTML.Engine.fetch_assign!(assigns, :user)
# Returns: %{name: "John"}

# Missing assign raises informative error
Phoenix.HTML.Engine.fetch_assign!(assigns, :missing)
# Raises: ArgumentError with message:
# "assign @missing not available in template.
#  Available assigns: [:user, :title]"

Protocol: Phoenix.HTML.Safe

The Phoenix.HTML.Safe protocol defines how different data types are converted to HTML-safe iodata. This protocol is automatically implemented for common Elixir types.

defprotocol Phoenix.HTML.Safe do
  @spec to_iodata(t) :: iodata
  def to_iodata(data)
end

Built-in Protocol Implementations

All implementations ensure data is properly escaped for HTML context:

# Atom implementation - converts to escaped string
defimpl Phoenix.HTML.Safe, for: Atom do
  def to_iodata(nil) :: ""
  def to_iodata(atom) :: iodata  # HTML-escaped string conversion
end

# BitString implementation - HTML escapes binary content
defimpl Phoenix.HTML.Safe, for: BitString do
  def to_iodata(binary) :: iodata  # HTML-escaped content
end

# Date/Time implementations - ISO8601 conversion
defimpl Phoenix.HTML.Safe, for: Date do
  def to_iodata(date) :: binary  # ISO8601 date string
end

defimpl Phoenix.HTML.Safe, for: DateTime do
  def to_iodata(datetime) :: iodata  # HTML-escaped ISO8601 string
end

# Numeric implementations - string conversion
defimpl Phoenix.HTML.Safe, for: Integer do
  def to_iodata(integer) :: binary  # String representation
end

# List implementation - recursive HTML escaping
defimpl Phoenix.HTML.Safe, for: List do
  def to_iodata(list) :: iodata  # Recursively escaped list content
end

# Tuple implementation - handles {:safe, content} tuples
defimpl Phoenix.HTML.Safe, for: Tuple do
  def to_iodata({:safe, data}) :: iodata  # Extracts safe data
  def to_iodata(other) :: no_return  # Raises Protocol.UndefinedError
end

Custom Protocol Implementation:

# Example: Custom struct implementation
defmodule User do
  defstruct [:name, :email]
end

defimpl Phoenix.HTML.Safe, for: User do
  def to_iodata(%User{name: name, email: email}) do
    Phoenix.HTML.Engine.html_escape("#{name} (#{email})")
  end
end

# Usage
user = %User{name: "John <script>", email: "john@example.com"}
safe_output = Phoenix.HTML.Safe.to_iodata(user)
# Returns HTML-escaped: "John &lt;script&gt; (john@example.com)"

Error Handling

Common Safety Errors

# ArgumentError: Numeric ID values
attributes_escape([id: 123])
# Raises: "attempting to set id attribute to 123, but setting the DOM ID to a number..."

# ArgumentError: Invalid list content in templates
Phoenix.HTML.Safe.to_iodata([1000])  # Integer > 255 in list
# Raises: "lists in Phoenix.HTML templates only support iodata..."

# Protocol.UndefinedError: Unsupported tuple format
Phoenix.HTML.Safe.to_iodata({:unsafe, "content"})
# Raises: Protocol.UndefinedError for Phoenix.HTML.Safe protocol

Security Best Practices

  1. Default to Escaping: Never use raw/1 with user-provided content
  2. Validate IDs: Use string prefixes for numeric ID values
  3. Protocol Implementation: Always escape content in custom Safe protocol implementations
  4. Template Safety: Let Phoenix.HTML.Engine handle automatic escaping in templates
  5. JavaScript Context: Use javascript_escape/1 for content inserted into JavaScript strings
  6. CSS Context: Use css_escape/1 for dynamic CSS identifier generation

Install with Tessl CLI

npx tessl i tessl/hex-phoenix-html

docs

forms.md

html-safety.md

index.md

javascript.md

tile.json