Security Tip: Don't Use nl2br()!

[Tip#67] As useful as it sounds, nl2br() can potentially leave you open to Cross-Site Scripting (XSS) vulnerabilities... you should reach for CSS instead!

Security Tip: Don't Use nl2br()!

👉 Looking to dive deeper into Laravel security? Check out Practical Laravel Security, my hands-on security course that uses interactive hacking challenges to teach you about how vulnerabilities work, so you can avoid them in your own code. 🕵️

👉 Worried about your app being hacked? Book in a Laravel Security Audit and Penetration Test! I can find the vulnerabilities before a hacker does, and help you fix them. 🕵️


PHP is full of interesting and useful functions for a variety of use cases. One such function which gets used a lot is `nl2br()`! If you’re not familiar, `nl2br()` adds `<br>` characters into strings where there are newline characters (`\n`, `\n\r`, and `\r`).

For example:

> nl2br("One\nTwo\nThree");
= """
  One<br />\n
  Two<br />\n
  Three
  """

The most common use case I see for `nl2br()` is to display user-submitted inputs from `<textarea>` fields. It translates the newline characters the user inputted into actual newlines (via `<br>` tags), which are displayed on the page.

However, the risk is that by using `nl2br()`, you’re needing to output the user input unescaped on the page, which can introduce Cross-Site Scripting (XSS) vulnerabilities.

For example, if we use the following user input in next few examples:

One
Two
<img src=x onerror="alert('Boom!')">
Three

You can’t use use `nl2br()` inside blade escaping tags:

{{ nl2br($input) }}

As it’ll escape the `<br>` tags too:

All of the content on a single line, with no line breaks and the HTML escaped and visible.

But when you unescape the output:

{!! nl2br($input) !!}

You get this:

The text on new lines, but the XSS payload has been triggered… 😔

One way to work around this is to escape inside the `nl2br()`:

{!! nl2br(e($input)) !!}
Escaped output (XSS payload is visible) with newlines.

It works, but it looks pretty ugly and is very easy to forget!

A much better way to solve it is to use the CSS rule `white-space: pre-line;` on the escaped input, without making any modifications to the input1:

<div style="white-space: pre-line;">{{ $input }}</div>

Or if you use Tailwind CSS2:

<div class="whitespace-pre-line">{{ $input }}</div>
Escaped output (XSS payload is visible) with newlines.

This approach provides an incredibly clean solution3 that takes advantage of standard output escaping to prevent XSS from sneaking in, while still preserving the inputted newlines in the way the user intended. Also, you’re unlikely to forget to escape the output as `{{ ... }}` should be your default already.


Looking to learn more?
Security Tip #48: Test for Missing Authorisation
▶️ In Depth #16: What Are Insecure Functions?

  1. Beyond the escaping, obviously…

  2. See https://tailwindcss.com/docs/whitespace for all of the whitespace formatting outputs - there are some other useful ones in there!

  3. Ignore the inline styles, they are only there for the demo. In general inline styles should be avoided, as they can cause Content Security Policy violations!