#36206: Issues in the Existing SecurityMiddleware Code 1. Incorrect use of
response.setdefault() instead of response.headers.setdefault() 2. In the
process_request() method, HTTPS redirection is done While this works,
%-formatting is less readable and slightly less performant than modern
alternatives like f-strings 3. Preventing Overwriting of Existing Headers
-------------------------------------+-------------------------------------
Reporter: Abhijeet Kumar | Owner: (none)
Type: | Status: closed
Cleanup/optimization |
Component: HTTP handling | Version: 5.1
Severity: Normal | Resolution: invalid
Keywords: | Triage Stage:
| Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Tim Graham):
* component: Uncategorized => HTTP handling
* keywords: security.py =>
* resolution: => invalid
* status: new => closed
* type: Bug => Cleanup/optimization
Old description:
> 1. Incorrect use of response.setdefault() instead of
> response.headers.setdefault()
> Issue:
> In the original code, the Cross-Origin-Opener-Policy (COOP) header is set
> using:
>
> response.setdefault("Cross-Origin-Opener-Policy",
> self.cross_origin_opener_policy)
>
> This is incorrect because:
>
> a. response.setdefault() does not exist in Django’s HttpResponse class.
> b. Headers should be set using response.headers.setdefault() to ensure
> they are only added if they don’t already exist.
>
> Suggested Modification:
> Replace:
>
> response.setdefault("Cross-Origin-Opener-Policy",
> self.cross_origin_opener_policy)
>
> With:
>
> response.headers.setdefault("Cross-Origin-Opener-Policy",
> self.cross_origin_opener_policy)
>
> 2. Improving String Formatting for Readability & Performance
>
> Issue:
> In the process_request() method, HTTPS redirection is done using:
>
> return HttpResponsePermanentRedirect(
> "https://%s%s" % (host, request.get_full_path())
> )
>
> While this works, %-formatting is less readable and slightly less
> performant than modern alternatives like f-strings.
>
> Suggested Modification:
> Change:
>
> return HttpResponsePermanentRedirect(
> "https://%s%s" % (host, request.get_full_path())
> )
>
> To:
> return
> HttpResponsePermanentRedirect(f"https://{host}{request.get_full_path()}")
>
> 3. Preventing Overwriting of Existing Headers
>
> Issue:
>
> The original code unconditionally sets security headers like:
>
> response.headers["Strict-Transport-Security"] = sts_header
> response.headers["X-Content-Type-Options"] = "nosniff"
>
> This could Override existing security policies set by other middleware or
> custom responses &
> Prevent flexibility in modifying security headers dynamically.
>
> Suggested Modification:
>
> Use setdefault() instead of direct assignment:
>
> response.headers.setdefault("Strict-Transport-Security", sts_header)
> response.headers.setdefault("X-Content-Type-Options", "nosniff")
>
> Suggested Code:
>
> import re
>
> from django.conf import settings
> from django.http import HttpResponsePermanentRedirect
> from django.utils.deprecation import MiddlewareMixin
>
> class SecurityMiddleware(MiddlewareMixin):
> def __init__(self, get_response):
> super().__init__(get_response)
> self.sts_seconds = settings.SECURE_HSTS_SECONDS
> self.sts_include_subdomains =
> settings.SECURE_HSTS_INCLUDE_SUBDOMAINS
> self.sts_preload = settings.SECURE_HSTS_PRELOAD
> self.content_type_nosniff = settings.SECURE_CONTENT_TYPE_NOSNIFF
> self.redirect = settings.SECURE_SSL_REDIRECT
> self.redirect_host = settings.SECURE_SSL_HOST
> self.redirect_exempt = [re.compile(r) for r in
> settings.SECURE_REDIRECT_EXEMPT]
> self.referrer_policy = settings.SECURE_REFERRER_POLICY
> self.cross_origin_opener_policy =
> settings.SECURE_CROSS_ORIGIN_OPENER_POLICY
>
> def process_request(self, request):
> path = request.path.lstrip("/")
> if (
> self.redirect
> and not request.is_secure()
> and not any(pattern.search(path) for pattern in
> self.redirect_exempt)
> ):
> host = self.redirect_host or request.get_host()
> return
> HttpResponsePermanentRedirect(f"https://{host}{request.get_full_path()}")
>
> def process_response(self, request, response):
> if (
> self.sts_seconds
> and request.is_secure()
> and "Strict-Transport-Security" not in response.headers
> ):
> sts_header = f"max-age={self.sts_seconds}"
> if self.sts_include_subdomains:
> sts_header += "; includeSubDomains"
> if self.sts_preload:
> sts_header += "; preload"
> response.headers.setdefault("Strict-Transport-Security",
> sts_header)
>
> if self.content_type_nosniff:
> response.headers.setdefault("X-Content-Type-Options",
> "nosniff")
>
> if self.referrer_policy:
> # Support a comma-separated string or iterable of values to
> allow fallback.
> response.headers.setdefault(
> "Referrer-Policy",
> ",".join(
> [v.strip() for v in self.referrer_policy.split(",")]
> if isinstance(self.referrer_policy, str)
> else self.referrer_policy
> ),
> )
>
> if self.cross_origin_opener_policy:
> response.headers.setdefault(
> "Cross-Origin-Opener-Policy",
> self.cross_origin_opener_policy,
> )
>
> return response
New description:
1. Incorrect use of `response.setdefault()` instead of
`response.headers.setdefault()`
Issue:
In the original code, the Cross-Origin-Opener-Policy (COOP) header is set
using:
{{{#!python
response.setdefault("Cross-Origin-Opener-Policy",
self.cross_origin_opener_policy)
}}}
This is incorrect because:
a. `response.setdefault()` does not exist in Django’s `HttpResponse`
class.
b. Headers should be set using `response.headers.setdefault()` to ensure
they are only added if they don’t already exist.
Suggested Modification:
Replace:
{{{#!python
response.setdefault("Cross-Origin-Opener-Policy",
self.cross_origin_opener_policy)
}}}
With:
{{{#!python
response.headers.setdefault("Cross-Origin-Opener-Policy",
self.cross_origin_opener_policy)
}}}
2. Improving String Formatting for Readability & Performance
Issue:
In the process_request() method, HTTPS redirection is done using:
{{{#!python
return HttpResponsePermanentRedirect(
"https://%s%s" % (host, request.get_full_path())
)
}}}
While this works, %-formatting is less readable and slightly less
performant than modern alternatives like f-strings.
Suggested Modification:
Change:
{{{#!python
return HttpResponsePermanentRedirect(
"https://%s%s" % (host, request.get_full_path())
)
}}}
To:
{{{#!python
return
HttpResponsePermanentRedirect(f"https://{host}{request.get_full_path()}")
}}}
3. Preventing Overwriting of Existing Headers
Issue:
The original code unconditionally sets security headers like:
{{{#!python
response.headers["Strict-Transport-Security"] = sts_header
response.headers["X-Content-Type-Options"] = "nosniff"
}}}
is could Override existing security policies set by other middleware or
custom responses &
Prevent flexibility in modifying security headers dynamically.
Suggested Modification:
Use `setdefault()` instead of direct assignment:
{{{#!python
response.headers.setdefault("Strict-Transport-Security", sts_header)
response.headers.setdefault("X-Content-Type-Options", "nosniff")
}}}
Suggested Code:
{{{#!python
import re
from django.conf import settings
from django.http import HttpResponsePermanentRedirect
from django.utils.deprecation import MiddlewareMixin
class SecurityMiddleware(MiddlewareMixin):
def __init__(self, get_response):
super().__init__(get_response)
self.sts_seconds = settings.SECURE_HSTS_SECONDS
self.sts_include_subdomains =
settings.SECURE_HSTS_INCLUDE_SUBDOMAINS
self.sts_preload = settings.SECURE_HSTS_PRELOAD
self.content_type_nosniff = settings.SECURE_CONTENT_TYPE_NOSNIFF
self.redirect = settings.SECURE_SSL_REDIRECT
self.redirect_host = settings.SECURE_SSL_HOST
self.redirect_exempt = [re.compile(r) for r in
settings.SECURE_REDIRECT_EXEMPT]
self.referrer_policy = settings.SECURE_REFERRER_POLICY
self.cross_origin_opener_policy =
settings.SECURE_CROSS_ORIGIN_OPENER_POLICY
def process_request(self, request):
path = request.path.lstrip("/")
if (
self.redirect
and not request.is_secure()
and not any(pattern.search(path) for pattern in
self.redirect_exempt)
):
host = self.redirect_host or request.get_host()
return
HttpResponsePermanentRedirect(f"https://{host}{request.get_full_path()}")
def process_response(self, request, response):
if (
self.sts_seconds
and request.is_secure()
and "Strict-Transport-Security" not in response.headers
):
sts_header = f"max-age={self.sts_seconds}"
if self.sts_include_subdomains:
sts_header += "; includeSubDomains"
if self.sts_preload:
sts_header += "; preload"
response.headers.setdefault("Strict-Transport-Security",
sts_header)
if self.content_type_nosniff:
response.headers.setdefault("X-Content-Type-Options",
"nosniff")
if self.referrer_policy:
# Support a comma-separated string or iterable of values to
allow fallback.
response.headers.setdefault(
"Referrer-Policy",
",".join(
[v.strip() for v in self.referrer_policy.split(",")]
if isinstance(self.referrer_policy, str)
else self.referrer_policy
),
)
if self.cross_origin_opener_policy:
response.headers.setdefault(
"Cross-Origin-Opener-Policy",
self.cross_origin_opener_policy,
)
return response
}}}
--
Comment:
1. `HttpResponse.set_default()`
[https://github.com/django/django/blob/87c5de3b7f2316aa17353d74f54e6ff19013d049/django/http/response.py#L278-L280
exists].
2. Per [https://docs.djangoproject.com/en/dev/internals/contributing
/writing-code/coding-style/#python-style Python style guide], we won't
accept PRs that merely change formatting style: "Don’t waste time doing
unrelated refactoring of existing code to adjust the formatting method."
3. It could be useful, but at this point it would be backward-
incompatible, so I think we should leave it alone unless we have a very
compelling use case. Changing anything security related can't be done
lightly.
Thanks for the ideas. As Jake said, in the future, please don't combine
separate issues in one ticket.
--
Ticket URL: <https://code.djangoproject.com/ticket/36206#comment:2>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.
--
You received this message because you are subscribed to the Google Groups
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion visit
https://groups.google.com/d/msgid/django-updates/010701952b0377f2-5ff117c7-1f38-48cb-b362-2a4e14f510e4-000000%40eu-central-1.amazonses.com.