0
# HTML Safety and Escaping
1
2
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.
3
4
## Capabilities
5
6
### Content Safety Marking
7
8
Marks content as safe HTML that should not be escaped, allowing raw HTML to be rendered directly in templates.
9
10
```elixir { .api }
11
def raw(content) :: Phoenix.HTML.safe
12
# Parameters:
13
# - content: iodata | Phoenix.HTML.safe | nil - Content to mark as safe
14
#
15
# Returns:
16
# - Phoenix.HTML.safe - Safe content tuple {:safe, iodata}
17
```
18
19
**Usage Examples:**
20
21
```elixir
22
# Mark HTML string as safe
23
safe_html = raw("<p>Welcome <strong>User</strong></p>")
24
# Returns: {:safe, "<p>Welcome <strong>User</strong></p>"}
25
26
# Safe content passes through unchanged
27
already_safe = raw({:safe, "<div>Already safe</div>"})
28
# Returns: {:safe, "<div>Already safe</div>"}
29
30
# Nil becomes empty safe content
31
empty = raw(nil)
32
# Returns: {:safe, ""}
33
```
34
35
### HTML Entity Escaping
36
37
Escapes HTML entities in content to prevent XSS attacks, converting potentially dangerous characters to their safe HTML entity equivalents.
38
39
```elixir { .api }
40
def html_escape(content) :: Phoenix.HTML.safe
41
# Parameters:
42
# - content: Phoenix.HTML.unsafe - Content that may contain HTML entities
43
#
44
# Returns:
45
# - Phoenix.HTML.safe - Escaped content as safe iodata
46
```
47
48
**Usage Examples:**
49
50
```elixir
51
# Escape user input
52
user_input = "<script>alert('XSS')</script>"
53
safe_output = html_escape(user_input)
54
# Returns: {:safe, [[[] | "<"], "script", [] | ">", "alert(", ...]}
55
56
# Safe content passes through
57
already_safe = html_escape({:safe, "<p>Safe content</p>"})
58
# Returns: {:safe, "<p>Safe content</p>"}
59
60
# Numbers and other data types are converted
61
number_escaped = html_escape(123)
62
# Returns: {:safe, "123"}
63
```
64
65
### Safe Content Conversion
66
67
Converts safe iodata to regular strings, ensuring the content was properly marked as safe before conversion.
68
69
```elixir { .api }
70
def safe_to_string(safe_content) :: String.t()
71
# Parameters:
72
# - safe_content: Phoenix.HTML.safe - Content marked as safe
73
#
74
# Returns:
75
# - String.t() - Regular string representation
76
#
77
# Raises:
78
# - If content is not marked as safe
79
```
80
81
**Usage Examples:**
82
83
```elixir
84
# Convert safe iodata to string
85
safe_content = {:safe, ["<p>", "Hello", "</p>"]}
86
result = safe_to_string(safe_content)
87
# Returns: "<p>Hello</p>"
88
89
# Use with html_escape for complete safety
90
user_data = "<script>alert('XSS')</script>"
91
safe_string = user_data |> html_escape() |> safe_to_string()
92
# Returns: "<script>alert('XSS')</script>"
93
```
94
95
### HTML Attribute Escaping
96
97
Escapes HTML attributes with special handling for common attribute patterns, supporting nested data structures and boolean attributes.
98
99
```elixir { .api }
100
def attributes_escape(attrs) :: Phoenix.HTML.safe
101
# Parameters:
102
# - attrs: list | map - Enumerable of HTML attributes
103
#
104
# Returns:
105
# - Phoenix.HTML.safe - Escaped attributes as iodata
106
```
107
108
**Special Attribute Behaviors:**
109
110
- **`:class`**: Accepts list of classes, filters out `nil` and `false` values
111
- **`:data`, `:aria`, `:phx`**: Accepts keyword lists, converts to dash-separated attributes
112
- **`:id`**: Validates that numeric IDs are not used (raises ArgumentError)
113
- **Boolean attributes**: `true` values render as bare attributes, `false`/`nil` are omitted
114
- **Atom keys**: Automatically converted to dash-case (`:phx_value_id` → `phx-value-id`)
115
116
**Usage Examples:**
117
118
```elixir
119
# Basic attributes
120
attrs = [title: "Click me", id: "my-button", disabled: true]
121
escaped = attributes_escape(attrs)
122
# Returns: {:safe, [" title=\"Click me\" id=\"my-button\" disabled"]}
123
124
# Class list handling
125
attrs = [class: ["btn", "btn-primary", nil, "active"]]
126
escaped = attributes_escape(attrs)
127
# Returns: {:safe, [" class=\"btn btn-primary active\""]}
128
129
# Data attributes
130
attrs = [data: [confirm: "Are you sure?", method: "delete"]]
131
escaped = attributes_escape(attrs)
132
# Returns: {:safe, [" data-confirm=\"Are you sure?\" data-method=\"delete\""]}
133
134
# Phoenix-specific attributes
135
attrs = [phx: [value: [user_id: 123]]]
136
escaped = attributes_escape(attrs)
137
# Returns: {:safe, [" phx-value-user-id=\"123\""]}
138
139
# Combined usage
140
attrs = [
141
class: ["btn", "btn-danger"],
142
data: [confirm: "Delete user?"],
143
phx: [click: "delete_user"]
144
]
145
escaped_str = attributes_escape(attrs) |> safe_to_string()
146
# Returns: " class=\"btn btn-danger\" data-confirm=\"Delete user?\" phx-click=\"delete_user\""
147
```
148
149
### JavaScript Content Escaping
150
151
Escapes HTML content for safe inclusion in JavaScript strings, handling special characters that could break JavaScript syntax or enable XSS attacks.
152
153
```elixir { .api }
154
def javascript_escape(content) :: binary | Phoenix.HTML.safe
155
# Parameters:
156
# - content: binary | Phoenix.HTML.safe - Content to escape for JavaScript
157
#
158
# Returns:
159
# - binary - Escaped string (for binary input)
160
# - Phoenix.HTML.safe - Escaped safe content (for safe input)
161
```
162
163
**Escaped Characters:**
164
- Quotes: `"` → `\"`, `'` → `\'`
165
- Backslashes: `\` → `\\`
166
- Newlines: `\n`, `\r`, `\r\n` → `\n`
167
- Script tags: `</` → `<\/`
168
- Unicode separators: `\u2028` → `\\u2028`, `\u2029` → `\\u2029`
169
- Null bytes: `\u0000` → `\\u0000`
170
- Backticks: `` ` `` → ``\```
171
172
**Usage Examples:**
173
174
```elixir
175
# Escape user content for JavaScript
176
user_content = ~s(<script>alert("XSS")</script>)
177
escaped = javascript_escape(user_content)
178
# Returns: "<\\/script>alert(\\\"XSS\\\")<\\/script>"
179
180
# Use in template JavaScript
181
html_content = render("user_profile.html", user: @user)
182
javascript_code = """
183
$("#container").html("#{javascript_escape(html_content)}");
184
"""
185
186
# Safe content handling
187
safe_content = {:safe, ~s(<div class="user">'John'</div>)}
188
escaped_safe = javascript_escape(safe_content)
189
# Returns: {:safe, "<div class=\\\"user\\\">\\'John\\'</div>"}
190
```
191
192
### CSS Identifier Escaping
193
194
Escapes strings for safe use as CSS identifiers, following CSS specification for character escaping in selectors and property values.
195
196
```elixir { .api }
197
def css_escape(value) :: String.t()
198
# Parameters:
199
# - value: String.t() - String to escape for CSS usage
200
#
201
# Returns:
202
# - String.t() - CSS-safe identifier string
203
```
204
205
**Usage Examples:**
206
207
```elixir
208
# Escape problematic CSS characters
209
css_class = css_escape("user-123 name")
210
# Returns: "user-123\\ name"
211
212
# Handle numeric prefixes
213
css_id = css_escape("123-user")
214
# Returns: "\\31 23-user"
215
216
# Use in dynamic CSS generation
217
user_id = "user@domain.com"
218
safe_selector = "##{css_escape(user_id)}"
219
# Returns: "#user\\@domain\\.com"
220
```
221
222
### Template Engine Functions
223
224
Core functions from the Phoenix.HTML.Engine module that handle template processing and HTML safety in EEx templates.
225
226
#### Content Encoding for Templates
227
228
Converts various content types to HTML-safe iodata for use in template rendering.
229
230
```elixir { .api }
231
def encode_to_iodata!(content) :: iodata
232
# Parameters:
233
# - content: term - Content to encode (safe tuples, binaries, lists, etc.)
234
#
235
# Returns:
236
# - iodata - HTML-safe iodata representation
237
```
238
239
**Content Type Handling:**
240
241
- **`{:safe, body}`**: Returns the body directly without encoding
242
- **`nil` or `""`**: Returns empty string
243
- **Binary strings**: HTML-escapes the content
244
- **Lists**: Processes through `Phoenix.HTML.Safe.List.to_iodata/1`
245
- **Other types**: Processes through `Phoenix.HTML.Safe.to_iodata/1`
246
247
**Usage Examples:**
248
249
```elixir
250
# Safe content passes through
251
Phoenix.HTML.Engine.encode_to_iodata!({:safe, "<p>Safe content</p>"})
252
# Returns: "<p>Safe content</p>"
253
254
# Binaries are HTML-escaped
255
Phoenix.HTML.Engine.encode_to_iodata!("<script>alert('XSS')</script>")
256
# Returns: HTML-escaped iodata
257
258
# Empty values become empty strings
259
Phoenix.HTML.Engine.encode_to_iodata!(nil)
260
# Returns: ""
261
262
Phoenix.HTML.Engine.encode_to_iodata!("")
263
# Returns: ""
264
265
# Lists are processed via Safe protocol
266
Phoenix.HTML.Engine.encode_to_iodata!(["<p>", "content", "</p>"])
267
# Returns: HTML-safe iodata
268
269
# Other types use Safe protocol
270
Phoenix.HTML.Engine.encode_to_iodata!(123)
271
# Returns: "123"
272
273
Phoenix.HTML.Engine.encode_to_iodata!(:hello)
274
# Returns: "hello" (HTML-escaped)
275
```
276
277
#### Direct HTML Escaping
278
279
Performs direct HTML escaping on binary strings with optimized performance.
280
281
```elixir { .api }
282
def html_escape(binary) :: iodata
283
# Parameters:
284
# - binary: binary - String to HTML-escape
285
#
286
# Returns:
287
# - iodata - HTML-escaped iodata structure
288
```
289
290
**Escaped Characters:**
291
- `<` → `<`
292
- `>` → `>`
293
- `&` → `&`
294
- `"` → `"`
295
- `'` → `'`
296
297
**Usage Examples:**
298
299
```elixir
300
# Basic HTML escaping
301
Phoenix.HTML.Engine.html_escape("<script>alert('XSS')</script>")
302
# Returns: HTML-escaped iodata
303
304
# Preserves non-HTML content
305
Phoenix.HTML.Engine.html_escape("Hello World")
306
# Returns: "Hello World"
307
308
# Handles quotes and ampersands
309
Phoenix.HTML.Engine.html_escape(~s(The "quick" & 'brown' fox))
310
# Returns: HTML-escaped iodata with quotes and ampersand escaped
311
```
312
313
#### Template Variable Access
314
315
Fetches template assigns with comprehensive error handling and debugging information.
316
317
```elixir { .api }
318
def fetch_assign!(assigns, key) :: term
319
# Parameters:
320
# - assigns: map - Template assigns map
321
# - key: atom - Assign key to fetch
322
#
323
# Returns:
324
# - term - The assign value
325
#
326
# Raises:
327
# - ArgumentError - With detailed error message if assign not found
328
```
329
330
**Usage Examples:**
331
332
```elixir
333
# Successful assign access
334
assigns = %{user: %{name: "John"}, title: "Welcome"}
335
user = Phoenix.HTML.Engine.fetch_assign!(assigns, :user)
336
# Returns: %{name: "John"}
337
338
# Missing assign raises informative error
339
Phoenix.HTML.Engine.fetch_assign!(assigns, :missing)
340
# Raises: ArgumentError with message:
341
# "assign @missing not available in template.
342
# Available assigns: [:user, :title]"
343
```
344
345
## Protocol: Phoenix.HTML.Safe
346
347
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.
348
349
```elixir { .api }
350
defprotocol Phoenix.HTML.Safe do
351
@spec to_iodata(t) :: iodata
352
def to_iodata(data)
353
end
354
```
355
356
### Built-in Protocol Implementations
357
358
All implementations ensure data is properly escaped for HTML context:
359
360
```elixir { .api }
361
# Atom implementation - converts to escaped string
362
defimpl Phoenix.HTML.Safe, for: Atom do
363
def to_iodata(nil) :: ""
364
def to_iodata(atom) :: iodata # HTML-escaped string conversion
365
end
366
367
# BitString implementation - HTML escapes binary content
368
defimpl Phoenix.HTML.Safe, for: BitString do
369
def to_iodata(binary) :: iodata # HTML-escaped content
370
end
371
372
# Date/Time implementations - ISO8601 conversion
373
defimpl Phoenix.HTML.Safe, for: Date do
374
def to_iodata(date) :: binary # ISO8601 date string
375
end
376
377
defimpl Phoenix.HTML.Safe, for: DateTime do
378
def to_iodata(datetime) :: iodata # HTML-escaped ISO8601 string
379
end
380
381
# Numeric implementations - string conversion
382
defimpl Phoenix.HTML.Safe, for: Integer do
383
def to_iodata(integer) :: binary # String representation
384
end
385
386
# List implementation - recursive HTML escaping
387
defimpl Phoenix.HTML.Safe, for: List do
388
def to_iodata(list) :: iodata # Recursively escaped list content
389
end
390
391
# Tuple implementation - handles {:safe, content} tuples
392
defimpl Phoenix.HTML.Safe, for: Tuple do
393
def to_iodata({:safe, data}) :: iodata # Extracts safe data
394
def to_iodata(other) :: no_return # Raises Protocol.UndefinedError
395
end
396
```
397
398
**Custom Protocol Implementation:**
399
400
```elixir
401
# Example: Custom struct implementation
402
defmodule User do
403
defstruct [:name, :email]
404
end
405
406
defimpl Phoenix.HTML.Safe, for: User do
407
def to_iodata(%User{name: name, email: email}) do
408
Phoenix.HTML.Engine.html_escape("#{name} (#{email})")
409
end
410
end
411
412
# Usage
413
user = %User{name: "John <script>", email: "john@example.com"}
414
safe_output = Phoenix.HTML.Safe.to_iodata(user)
415
# Returns HTML-escaped: "John <script> (john@example.com)"
416
```
417
418
## Error Handling
419
420
### Common Safety Errors
421
422
```elixir
423
# ArgumentError: Numeric ID values
424
attributes_escape([id: 123])
425
# Raises: "attempting to set id attribute to 123, but setting the DOM ID to a number..."
426
427
# ArgumentError: Invalid list content in templates
428
Phoenix.HTML.Safe.to_iodata([1000]) # Integer > 255 in list
429
# Raises: "lists in Phoenix.HTML templates only support iodata..."
430
431
# Protocol.UndefinedError: Unsupported tuple format
432
Phoenix.HTML.Safe.to_iodata({:unsafe, "content"})
433
# Raises: Protocol.UndefinedError for Phoenix.HTML.Safe protocol
434
```
435
436
## Security Best Practices
437
438
1. **Default to Escaping**: Never use `raw/1` with user-provided content
439
2. **Validate IDs**: Use string prefixes for numeric ID values
440
3. **Protocol Implementation**: Always escape content in custom Safe protocol implementations
441
4. **Template Safety**: Let Phoenix.HTML.Engine handle automatic escaping in templates
442
5. **JavaScript Context**: Use `javascript_escape/1` for content inserted into JavaScript strings
443
6. **CSS Context**: Use `css_escape/1` for dynamic CSS identifier generation