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?
answer
- e() knows HTML, not URLs or JS
- no quotes: a space starts a new attribute
- javascript: survives htmlspecialchars
- handlers decode ' back to a quote
- Js::from or data-* for handlers
basics
~20 se() 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 `'` 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{{-- 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
Recall to quote every attribute and that {{ }} does not make a javascript: link or an inline handler safe.
Explain which characters e() encodes and why each of unquoted attributes, URL schemes and on* handlers slips past them.
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.
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 `'`, 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'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