skip to content

In Laravel, why is rendering a customer's order note with Str::markdown() risky by default, and which options make it safe?

level: seniorimportance: should knowfreq 28%

answer

  1. GitHub-flavoured CommonMark underneath
  2. raw HTML allowed by default
  3. html_input: strip or escape
  4. allow_unsafe_links: false
  5. raw Blade output is required

basics

~20 s

Str::markdown() passes raw HTML and javascript: links through by default, and its output must be printed unescaped, so user Markdown becomes an XSS hole. Pass html_input => 'strip' and allow_unsafe_links => false, or purify the HTML.

solid answer

~40 s

`Str::markdown($string, array $options = [], array $extensions = [])` builds a CommonMark `GithubFlavoredMarkdownConverter` with your options. The Laravel docs warn that **by default Markdown supports raw HTML**, which exposes XSS with raw user input. Because the result is HTML, you print it with `{!! !!}` (or wrap it with `toHtmlString()`), so nothing escapes it for you: a note containing `<script>` or `[click](javascript:…)` runs in the admin's browser. The fix is `Str::markdown($note, ['html_input' => 'strip', 'allow_unsafe_links' => false])` — or `'escape'` to show tags as text — and, if some HTML must survive, running the output through an HTML purifier. `Str::inlineMarkdown()` takes the same options.

code

php · 14 lines
php
<?php

use Illuminate\Support\Str;

$note = 'Ring twice <img src=x onerror="alert(1)"> [track](javascript:alert(2))';

Str::markdown($note);
// raw <img ... onerror=...> and a javascript: link survive

Str::markdown($note, [
    'html_input' => 'strip',
    'allow_unsafe_links' => false,
]);
// tags stripped, unsafe link target removed

go deeper

for a junior

Know that Str::markdown() turns Markdown into HTML and that user input needs the html_input and allow_unsafe_links options.

for a middle

Explain why Markdown output must be echoed raw, what 'strip' versus 'escape' do, and how unsafe links get through.

for a senior

Centralise safe Markdown rendering in one helper, add tests with script and javascript: payloads, and audit raw Blade echoes.

for a principal

Decide which content paths may carry HTML, who can author them, and where sanitisation is enforced across the product.

## The feature An order page lets customers add delivery notes, and support staff want basic formatting — bold, lists, links. `Str::markdown()` converts GitHub-flavoured Markdown to HTML in one call: ```php <?php use Illuminate\Support\Str; Str::markdown('**Leave at the side door**'); // <p><strong>Leave at the side door</strong></p> ``` Internally it creates a `League\CommonMark\GithubFlavoredMarkdownConverter` with the `$options` array you pass, adds any `$extensions`, and returns the converted string. `Str::inlineMarkdown()` does the same without wrapping the output in block elements such as `<p>`, and the `Stringable` chain has matching `->markdown()` and `->inlineMarkdown()` methods. ## Why the default is dangerous The Laravel docs state it directly: **by default, Markdown supports raw HTML**, which exposes cross-site scripting vulnerabilities when used with raw user input. Two paths: 1. **Raw HTML** in the note — `<script>…</script>`, `<img src=x onerror=…>` — is passed through into the output. 2. **Unsafe links** — `[track parcel](javascript:…)` — are kept as clickable links unless you forbid them. Markdown output is HTML by definition, so a Blade view has to print it **unescaped** — `{!! Str::markdown($order->note) !!}` or a value converted with `->toHtmlString()`. Blade's normal `{{ }}` protection does not apply to it. Whoever opens the order — often a staff member with admin rights — runs the attacker's script. ## The safe configuration The docs point to two CommonMark options: | Option | Values | Effect | |---|---|---| | `html_input` | `'strip'` | removes raw HTML tags from the output | | | `'escape'` | shows raw HTML as visible text | | | `'allow'` | passes raw HTML through (the default behaviour) | | `allow_unsafe_links` | `false` | drops `javascript:`, `vbscript:`, `file:` and similar link targets | ```php <?php $html = Str::markdown($order->note, [ 'html_input' => 'strip', 'allow_unsafe_links' => false, ]); ``` With both set, the docs' example input `Inject: <script>alert("Hello XSS!");</script>` becomes `<p>Inject: alert(&quot;Hello XSS!&quot;);</p>` — the tags are gone and the text is harmless. ## If some HTML must survive Sometimes product copy written by staff needs a few HTML elements. Then: - keep `allow_unsafe_links` off; - allow HTML but pass the output through an **HTML purifier** with an allow-list of tags and attributes, as the docs recommend; - treat staff-authored content as a separate, trusted path, and never reuse that configuration for customer input. ## Where to put the rule 1. **One helper or macro** — for example a `Str::macro('safeMarkdown', …)` registered in a service provider — so nobody calls `Str::markdown()` on user input with default options. 2. **Render at output time**, not at save time, so a later fix to the options applies to old notes too. 3. **Review for `{!! !!}`** in Blade: every unescaped echo should trace back to a trusted or sanitised source. 4. **Tests** that feed a script tag and a `javascript:` link through the helper and assert neither survives. ## A reusable safe helper ```php <?php use Illuminate\Support\HtmlString; use Illuminate\Support\Str; Str::macro('safeMarkdown', function (?string $text): HtmlString { return new HtmlString(Str::markdown($text ?? '', [ 'html_input' => 'strip', 'allow_unsafe_links' => false, ])); }); // In a Blade view: {{ Str::safeMarkdown($order->note) }} ``` Returning an `HtmlString` makes the intent explicit: Blade's `{{ }}` prints `Htmlable` values without escaping, so the view needs no `{!! !!}`, and every raw-output site now goes through the safe options. Registering it once in a service provider's `boot()` means reviewers only have to check one place. ## What this is not This is not a general XSS lesson: the Blade escaping rules and the theory of injection live elsewhere. The Laravel-specific point is that `Str::markdown()` is a **converter, not a sanitizer**, its defaults allow HTML, and its options are the switch you must set.

  • What is the difference between html_input 'strip' and 'escape' in Laravel's Str::markdown()?
    `'strip'` removes raw HTML from the output entirely, so readers never see it. `'escape'` converts it to entities, so `<b>` appears as the literal text `<b>`. Both are safe; `'escape'` is useful when users legitimately type angle brackets and you want them visible.
  • Why not just print Str::markdown() output with {{ }} in Blade to stay safe?
    Because `{{ }}` escapes the generated HTML too, so the page shows literal tags such as `<p>` and `<strong>` instead of formatted text. Markdown output has to be printed unescaped, which is exactly why the converter options must remove dangerous input first.

saying these in an interview costs you the question

  • Believes Str::markdown() sanitizes HTML by default.
  • Relies on Blade's {{ }} escaping for Markdown output.
  • Sets html_input to strip but leaves javascript: links allowed.
  • Stores converted HTML at save time and never re-renders with safer options.
  • Uses the staff-content Markdown configuration for customer input.