Django: A security improvement coming to format_html()

This is my security blimp.

Can you spot the problem with this Django snippet?

from django.utils.html import format_html


def user_snippet(user):
    return format_html(f"<em>{user.name}</em>")

Well, the problem is that format_html() is passed an already-templated string! Its escaping ability is not being used.

If the user name contains HTML, it will be injected into the final output:

In [2]: user_snippet(User(name="<script>Bobby Tables</script>"))
Out[2]: '<em><script>Bobby Tables</script></em>'

Oh no!

This is known as an XSS (cross-site-scripting) vulnerability. format_html() exists to protect against XSS, when used correctly.

Here’s the correct way to use it:

from django.utils.html import format_html


def user_snippet(user):
    return format_html("<em>{}</em>", user.name)

format_html() is passed the HTML template and the variables to add into it. It safely escapes any HTML characters in the variables.

I’ve seen plenty of the misuse shown in the first pattern, with f-strings, str.format(), and %-formatting. It seems like an easy mistake to make, even an “attractive nuisance”.

Two weeks ago, whilst at DjangoCon Europe, I proposed that Django deprecate the ability for format_html() calls without any arguments in Ticket #34609. The ticket was quickly accepted and Bhuvnesh Sharma picked it up and wrote the patch. Mariusz Felisiak reviewed and merged it in commit 094b0bea2c. Thanks both!

Starting from Django 5.0, there’ll be a warning if you don’t pass any arguments to format_html() (except the template string). And from Django 6.0, it will raise a ValueError.

That’s another sharp corner rounded off in Django.

Fin

May your HTML generation be XSS-free,

—Adam


Learn how to make your tests run quickly in my book Speed Up Your Django Tests.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Tags: