or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

forms.mdhtml-safety.mdindex.mdjavascript.md

html-safety.mddocs/

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, [[[] | "&lt;"], "script", [] | "&gt;", "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: "&lt;script&gt;alert('XSS')&lt;/script&gt;"

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

- `<``&lt;`

292

- `>``&gt;`

293

- `&``&amp;`

294

- `"``&quot;`

295

- `'``&#39;`

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 &lt;script&gt; (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