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()!

PHP is full of interesting and useful functions for a variety of use cases, and 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 input:

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

Or if you use Tailwind CSS:

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

This approach provides an incredibly clean solution 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.