skip to content

In a Blade view, {{ }} encodes quotes, so why can an href, an unquoted attribute or an onclick handler built from user data still be an XSS hole?

level: seniorimportance: should knowfreq 40%

answer

  1. e() knows HTML, not URLs or JS
  2. no quotes: a space starts a new attribute
  3. javascript: survives htmlspecialchars
  4. handlers decode ' back to a quote
  5. Js::from or data-* for handlers

basics

~20 s

e() only stops a value breaking out of quoted HTML. Unquoted attributes break on a space, a javascript: URL has nothing to encode, and an onclick attribute is entity-decoded before its JavaScript runs, so each needs quoting, a scheme check or Js::from.

solid answer

~40 s

`{{ }}` calls `e()`, which encodes `<`, `>`, `&`, `"` and `'`. That is exactly right for element text and for **quoted** attribute values, and nothing more. In `<input value={{ $v }}>` the value is unquoted, so a space lets it add `onfocus=...`. In `<a href="{{ $url }}">`, `javascript:alert(1)` contains no character `e()` changes, so the link runs script; validate the scheme, for instance with `url:http,https`. In `onclick="follow('{{ $name }}')"`, the browser decodes `&#039;` back into `'` before the JavaScript runs, so the name escapes the string. Use `{{ Js::from($name) }}` or `@js($name)` there, or better, put the value in a `data-` attribute and read it from a script.

code

html · 10 lines
html
{{-- Unsafe: unquoted, raw scheme, handler string --}}
<input value={{ $member->nick }}>
<a href="{{ $member->website }}">Website</a>
<button onclick="follow('{{ $member->name }}')">Follow</button>

{{-- Safer --}}
<input value="{{ $member->nick }}">
<a href="{{ $member->website }}">Website</a> {{-- validated url:http,https --}}
<button onclick="follow({{ Js::from($member->name) }})">Follow</button>
<button data-name="{{ $member->name }}" class="js-follow">Follow</button>

go deeper

for a junior

Recall to quote every attribute and that {{ }} does not make a javascript: link or an inline handler safe.

for a middle

Explain which characters e() encodes and why each of unquoted attributes, URL schemes and on* handlers slips past them.

for a senior

Review templates by position: scheme validation for URLs, Js::from or data-* for JavaScript contexts, allow-lists for style, with CSP as a second layer.

for a principal

Set conventions that remove whole classes: no inline handlers, links built from routes, validated schemes at the edge, and CSP enforcing it.

## What e() guarantees Blade's `{{ $x }}` compiles to `e($x)`, which runs `htmlspecialchars` with `ENT_QUOTES`. It encodes five characters: `&`, `<`, `>`, `"` and `'`. That guarantees one thing: **the value cannot create new HTML tags or leave a quoted attribute**. It does not know whether the attribute is a URL, a script, a style rule, or unquoted - and those positions have their own dangerous characters. This answer covers where that gap shows up in Blade templates; the general theory of output positions belongs to application-security material. ## Four places it falls short ### 1. Unquoted attribute values ```html <input name="nick" value={{ $nick }}> ``` Without quotes, the attribute value ends at the first space. A nickname of `x autofocus onfocus=alert(1)` contains no character `e()` encodes, so it adds two attributes and runs script when the page loads. **Always quote attribute values** in Blade templates. ### 2. URL attributes (href, src, action, formaction) ```html <a href="{{ $member->website }}">Website</a> ``` A value of `javascript:alert(document.cookie)` passes through `e()` unchanged, and clicking the link runs it. HTML escaping cannot help because nothing in the URL is HTML-special. The fix is a **scheme check** before the value is stored or printed - Laravel's `url` validation rule accepts allowed protocols, as in `url:http,https` - or building links yourself with `route()` and `url()` so users never choose the scheme. ### 3. Event-handler attributes ```html <button onclick="follow('{{ $member->name }}')">Follow</button> ``` `e()` turns `'` into `&#039;`, which looks safe. But the browser **decodes entities in attribute values before handing an `on*` attribute to the JavaScript engine**, so a name like `');alert(1);//` becomes a real quote again inside the JavaScript string. Two fixes: - `onclick="follow({{ Js::from($member->name) }})"` - `Js::from` produces a JavaScript string literal with quotes, `<`, `>` and `&` hex-escaped, so neither the attribute nor the JavaScript string can be closed. `@js($member->name)` is the directive form. - Better: `data-name="{{ $member->name }}"` plus a listener in a script file that reads `element.dataset.name`. The value stays data, `e()` is exactly the right escaping, and no inline handler is needed. ### 4. Style attributes and other interpreters A `style` attribute is CSS. `e()` stops quotes, but it does not stop a user-supplied value from adding CSS properties. Only put allow-listed values (a colour from a fixed list, a number you cast) into `style`. ## Position-by-position summary | Position in a Blade view | Right tool | |---|---| | Element text | `{{ $x }}` | | Quoted ordinary attribute | `"{{ $x }}"` | | Unquoted attribute | add quotes, then `{{ $x }}` | | URL attribute | scheme check on input or on output, then `{{ $x }}` | | `on*` handler, `x-data`, `x-on:` | `Js::from($x)` / `@js($x)`, or move the value to `data-*` | | `style` | allow-listed values only | | `<script>` block | `Js::from($x)` / `@js($x)` | ## Why the handler case surprises people The page source for the handler example looks reassuring: `onclick="follow('O&#039;Brien')"`. Reviewers see the entity and conclude the quote is neutralised. The order of operations says otherwise. The HTML parser reads the attribute and **decodes** its value to `follow('O'Brien')`; only then does the JavaScript engine compile the handler, and the restored quote ends the string. `Js::from` avoids this because its output uses JavaScript escapes (`'`), which the HTML parser leaves alone and the JavaScript engine interprets inside the string literal. ## Habits that prevent these bugs 1. Quote every attribute value in Blade templates. 2. Never let user data choose an attribute **name** or a tag name; `e()` has no answer for either. 3. Validate URL schemes where the URL enters the system, and keep `{{ }}` on output. 4. Keep JavaScript out of attributes; pass values through `data-*` or `Js::from`. 5. When reviewing, read each `{{ }}` together with the characters around it: the position decides whether `e()` is enough. A **Content Security Policy** that forbids inline handlers is a useful second layer, but it complements, not replaces, correct escaping in each position.

  • Why is data-name="{{ $name }}" read through dataset safe, when the onclick version is not?
    Both attributes are entity-decoded by the browser, but `dataset.name` hands the decoded value to JavaScript as a string, never as code. In `onclick` the decoded value becomes part of source code the engine parses, so a restored quote ends the string literal.
  • Does wrapping the URL in {!! !!} or HtmlString change anything for a javascript: link?
    It only makes things worse: the value loses HTML escaping too, so a quote can now break out of the attribute as well. The `javascript:` scheme was never an escaping problem; it needs a scheme check or links built by `route()` and `url()`.
  • Is {{ Js::from($x) }} safe inside a double-quoted x-data attribute?
    Yes. `Js::from` hex-escapes double and single quotes, `<`, `>` and `&`, so its output contains no character that closes the attribute, and `Js` being `Htmlable` means `{{ }}` does not escape it again. `@js($x)` produces the same output.

saying these in an interview costs you the question

  • e() makes a value safe in every attribute, quoted or not
  • htmlspecialchars blocks javascript: URLs in href
  • Entities in an onclick value stay encoded when the JavaScript runs
  • Using {!! !!} for URLs avoids breaking the link and is fine
  • A Content Security Policy makes attribute escaping unnecessary