Opened 4 years ago

Closed 2 weeks ago

#33180 closed Cleanup/optimization (fixed)

Debug 500 HTML broken with strict Content-Security-Policy (CSP)

Reported by: Adam Johnson Owned by: Jordan
Component: Error reporting Version: dev
Severity: Normal Keywords: CSP
Cc: Collin Anderson Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Adam Johnson)

When using a strict CSP header, the debug view is broken as the inline CSS and JS get blocked by the browser. The page is somewhat usable as all the text is all visible, but it isn't particularly friendly or necessarily obvious why it's broken.

For example:


I thought of a couple of potential fixes:

  1. Make the debug http response a special class that deletes any CSP header added to it. This would work for most cases but not if the CSP header is added outside of Django (WSGI middleware, a server wrapping runserver).
  2. Move the CSS and JS to separate URL's so they're compatible with CSP. They could be served by a special route/path in runserver.

I am personally leaning towards #2 - even though it's more complex, it is more general.

There's also some opportunity to modernize the CSS and JS in the debug page a bit, such as rewriting use of window.onload and onclick handlers.

To reproduce, you can save the below as app.py and run python app.py runserver:

import sys

from django.conf import settings
from django.urls import path

settings.configure(
    DEBUG=True,
    ROOT_URLCONF=__name__,
    SECRET_KEY="django-insecure-r42jn$xf4g+=w@=l#m6ghqo0!$icww-h4+$5gojq(1ld$x%!6f",
    MIDDLEWARE=[f"{__name__}.CSPMiddleware"],
)


class CSPMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)
        response[
            "Content-Security-Policy"
        ] = "object-src 'none'; base-uri 'none'; default-src 'self';"
        return response


def index(request):
    1 / 0


urlpatterns = [path("", index)]

if __name__ == "__main__":
    from django.core.management import execute_from_command_line

    execute_from_command_line(sys.argv)

Attachments (1)

error.png (207.8 KB ) - added by Adam Johnson 4 years ago.

Download all attachments as: .zip

Change History (13)

comment:1 by Adam Johnson, 4 years ago

Description: modified (diff)

comment:2 by Adam Johnson, 4 years ago

Description: modified (diff)

by Adam Johnson, 4 years ago

Attachment: error.png added

comment:3 by Mariusz Felisiak, 4 years ago

Triage Stage: UnreviewedAccepted
Type: BugCleanup/optimization

Personally, I prefer the second option.

Follow up to #32624.

comment:4 by Adam Johnson, 4 years ago

One complication is that the HTML email reporter may be used to render emails. Emails can't use separate CSS. I guess an implementation for #2 may need to pursue a separate CSS file that can be inlined when is_email = True. (The JS is only used for browser renders.)

comment:5 by Tom Carrick, 4 years ago

I'm not too sure about a special route for runserver. I'm probably not the only person that sometimes uses gunicorn/etc. locally or in some other locked down environment where I still want to see the error pages. I'm not sure I have a good alternative though.

comment:6 by Adam Johnson, 4 years ago

It needn't be done by modifying the server per se. It can probably be part of the WSGI application, which should work under gunicorn etc.

comment:7 by Teddy Ni, 4 years ago

For convenience, could this be implemented by using a nonce?

comment:8 by Adam Johnson, 4 years ago

We could use a nonce with a modified version of my suggested fix #1, since any CSP header will be added *after* the response has been created. We'd have to detect the header being added and pull the nonce out of it (if it's there) or add it, both paths requiring us to parse the header. Sounds complicated to me.

comment:9 by Collin Anderson, 4 years ago

I feel like ideally we get Content-Security-Policy framework added to core (#15727) so there's a reliable way to edit CSP headers on a per-response basis.

Re moving js/css to separate file/url, Google's CSP Evaluator https://csp-evaluator.withgoogle.com/ (linked from https://infosec.mozilla.org/guidelines/web_security#content-security-policy) says regarding script-src: self:
"Host whitelists can frequently be bypassed. Consider using 'strict-dynamic' in combination with CSP nonces or hashes." "'self' can be problematic if you host JSONP, Angular or user uploaded files."

(There's probably pros and cons either way, as you could still have injection in an inline-script in a template. script-src: self is pretty popular, and contrib.admin relies on it being there.)

comment:10 by Jordan, 6 months ago

Owner: set to Jordan
Status: newassigned

comment:11 by Rob Hudson, 2 weeks ago

With the recent addition of CSP and the CSP view decorators, the debug views are now being decorated to not have a CSP applied. This only prevents the built-in CSP support from adding a header, not removing a header that already exists by other means.

comment:12 by Natalia Bidart, 2 weeks ago

Cc: Collin Anderson added
Keywords: CSP added
Resolution: fixed
Status: assignedclosed

Thank you Rob! I can confirm that in the current main branch, the debug 500 view no longer triggers CSP violations when using strict policies.

Given this, I think we can consider the original report fixed. Moving CSS/JS to dedicated files would introduce its own risks (for example, chained failures if static file handling is the source of the error). Since Django already allows projects to override the 500 view, it seems best to leave the debug view as-is.

If there's a strong desire to explore isolated assets for the debug pages, that feels like a separate feature request rather than part of this bug report with the recent CSP features that Django merged into core.

Note: See TracTickets for help on using tickets.
Back to Top