Download as pdf or txt
Download as pdf or txt
You are on page 1of 13

Security: Bring CORS And

CSP Into Core


GSoC 2023 Proposal
Hrushikesh Vaidya
hrus.in

Table of Contents
Table of Contents 1

Project Spec 1
CORS 2
CSP 4

Implementation Details 7
CORS 7
CORS Settings 7
CORS Middleware 8
CORS Decorators 8
CSP 9
CSP Middleware 9
CSP Settings 10
CSP Decorators 10

Timeline 11

References 12

About Me 13

Project Spec
Since there already exist two mature and popular libraries providing CORS and CSP
support for Django - django-cors-headers and django-csp - my proposed
implementation is to bring features from the two libraries into Django core, with the design
and API matching Django’s CSRF protection design and API.
The key goals of the proposal are covered in the headings below.
CORS
Similar to Django’s CSRF protection, we will provide the following features for CORS
protection -
1. CorsMiddleware for adding CORS headers to responses. This will do the heavy
lifting of actually adding headers to responses.
2. CORS decorators -
a. cors_protect - A decorator that will allow users to configure all CORS
settings at the same time. Using this decorator will only send the CORS
header passed to it as arguments, and other headers configured in the
settings will be ignored.
b. cors_update - A decorator that will allow users to configure all CORS
settings at the same time. Using the decorator will send the CORS headers
passed to it as arguments, as well as any additional CORS headers defined
in the settings.
c. cors_exempt - A decorator that will exempt a view from CORS protection.
3. CORS settings for configuring a global policy or good defaults -
a. CORS_ALLOWED_ORIGINS - Globally sets the allowed origins to a sequence
of strings and/or regular expressions.
b. CORS_ALLOW_ALL_ORIGINS - Globally sets the allowed origins to *
c. CORS_PATHS - A list/sequence of regexes that will define the urlpatterns
that will be applied CORS protection. Only responses for URLs that match
these regexes will be sent CORS headers.
d. CORS_ALLOWED_METHODS - Globally sets the allowed methods for CORS
requests
e. CORS_ALLOWED_HEADERS - Globally sets the allowed headers for CORS
requests
f. CORS_EXPOSE_HEADERS - Globally sets the headers that will be exposed
to the browser for CORS responses
g. CORS_PREFLIGHT_MAX_AGE - Globally sets the max age for caching a
preflight response
h. CORS_ALLOW_CREDENTIALS - Globally sets if credentials are allowed for
CORS requests

The implementation details for the middleware, decorators, and settings are covered in the
Implementation Details heading.

This means users will be able to configure CORS protection in a few ways. If they add
CorsMiddleware to the list of middlewares and configure the CORS_ALLOWED_ORIGINS
setting or set the CORS_ALLOW_ALL_ORIGINS setting to True, CORS protection will be
enabled project-wide and CORS headers will be sent for all requests.

If users add CorsMiddleware to the list of middlewares and configure the CORS_PATHS
setting in addition to CORS_ALLOWED_ORIGINS or CORS_ALLOW_ALL_ORIGINS, CORS
headers will be sent to all requests to URLs matching the CORS_PATHS setting.

This would be the bare minimum configuration needed to start sending CORS headers,
and CORS protection behavior can be customized by also configuring settings (d) through
(f).

Users can use the cors_protect and cors_update decorators to customize the CORS
headers sent on a per-view basis. Both cors_protect and cors_update will also take a
callable as an optional argument, which will allow users to customize the CORS headers
sent on a per-request basis. The callable will be passed the request, and should return a
dictionary specifying the CORS headers to be sent as keys (in lowercase), and their
corresponding values as values.

We will also add system checks that will make sure CORS settings are configured correctly
(if at all) when Django starts. The requirements for a valid configuration are -

1. Specifying exactly one of CORS_ALLOWED_ORIGINS or


CORS_ALLOW_ALL_ORIGINS, and optionally specifying CORS_PATHS. Specifying
both CORS_ALLOWED_ORIGINS and CORS_ALLOW_ALL_ORIGINS will result in a
warning, and the value in CORS_ALLOWED_ORIGINS will be used and
CORS_ALLOW_ALL_ORIGINS will be ignored.
2. Specifying zero or more of the remaining settings.

We will also add system checks to make sure that the values in the CORS settings are of
the correct format and data type.

CSP
Similar to the proposed CORS protection spec, the CSP protection spec will provide the
following features -

1. CspMiddleware for adding the CSP header to responses. This will do the heavy
lifting of building the CSP policy and adding the CSP header.
2. CSP decorators -
a. csp_protect - A decorator that takes in all supported CSP directives as
arguments and adds the correct CSP header to the response. Adding this
decorator to a view will only add CSP directives for the arguments passed
to the decorator, and any additional global directives configured in the
settings will be ignored.
b. csp_update - A decorator that takes in all supported CSP directives as
arguments and adds the correct CSP header to the response. Adding this
header to a view will add directives for the passed arguments, as well as
any other global directives configured in the settings.
c. csp_exempt - A decorator that will exempt a view from CSP protection.
d. csp_use_policy - A decorator that will define which CSP policy to use for
a particular view.
3. CSP settings - We will provide a single setting for configuring the CSP policies -
CSP_POLICIES, which will map policy names to CSP directives. Moreover, we will
provide a CSP_DEFAULT_POLICY setting, which will define the CSP policy that will
be sent by default. The CSP_POLICIES setting will be a dictionary where the keys
will be the policy name and the values will be dictionaries specifying the policy. A
policy will be specified with directives as keys and strings as their values. There are
many such directives, the full list is given below.
a. DEFAULT_SRC
b. SCRIPT_SRC
c. SCRIPT_SRC_ATTR
d. SCRIPT_SRC_ELEM
e. IMG_SRC
f. OBJECT_SRC
g. PREFETCH_SRC
h. MEDIA_SRC
i. FRAME_SRC
j. FONT_SRC
k. CONNECT_SRC
l. STYLE_SRC
m. STYLE_SRC_ATTR
n. STYLE_SRC_ELEM
o. BASE_URI
p. CHILD_SRC
q. FRAME_ANSCESTORS
r. NAVIGATE_TO
s. FORM_ACTION
t. SANDBOX
u. REPORT_URI
v. REPORT_TO
w. MANIFEST_SRC
x. WORKER_SRC
y. PLUGIN_TYPES
z. REQUIRE_SRI_FOR
aa. UPGRADE_INSECURE_REQUESTS
bb. REQUIRE_TRUSTED_TYPES_FOR
cc. TRUSTED_TYPES
dd. BLOCK_ALL_MIXED_CONTENT
ee. REPORT_ONLY
ff. INCLUDE_NONCE_IN
gg. REPORT_PERCENTAGE

Most sites which need CSP will need just one policy, which will be named “default” at
project start. The default policy will be empty when a new project is created. An example
policy configuration for a project may look like the following -

CSP_DEFAULT_POLICY = “default”
CSP_POLICIES = {
“default”: {
DEFAULT_SRC: [“‘self’”],
SCRIPT_SRC: [“‘self’”, “cdn.example.com”]
},
“report-only”: {
REPORT_ONLY: True,
REPORT_URI: “/report-uri”,
OBJECT_SRC: None,
DEFAULT_SRC: [“https:”],
}
}

The implementation details for the middleware, settings, and decorators are provided
under the Implementation Details heading.

CSP protection will be disabled by default. Configuring CSP protection will involve adding
CspMiddleware to the list of middlewares, and configuring the CSP_POLICY setting.

Once configured, CspMiddleware will construct and send the CSP header as defined by
the CSP_DEFAULT_POLICY setting. Specific views will be able to send a different policy
by using the csp_use_policy decorator, and specific views (if any, although not
recommended) will be exempted by use of the csp_exempt decorator. We will not
provide a setting to only send CSP headers to specific paths, as in case of CORS, and CSP
headers will be sent to all paths. This behavior is to ensure that CSP protection is paranoid
and restrictive by default, since excluding any path in a project will eliminate the benefits
of CSP everywhere in the project [see Reference].

Since inline scripts will not execute once CSP is configured, we will provide nonce support.
CspMiddleware will generate a nonce value and add it as an attribute to the request
object as request.csp_nonce. Users will then need to add the nonce attribute to each
inline script tag in their templates. The INCLUDE_NONCE_IN setting will accept directives
as values, and add the CSP nonce to all the directives passed to it.

To make it more convenient to work with the CSP nonce, we will include a context
processor that will add a variable CSP_NONCE to the global template context, as a
shorthand for request.csp_nonce.

The REPORT_PERCENTAGE setting will allow users to limit the proportion of CSP
violations reported to the REPORT_URI. This may be useful in case users expect a lot of
violations to be reported. The default value of REPORT_PERCENTAGE will be 1 (100%).

We will also add system checks to make sure CSP settings are configured correctly, and
also to warn users if their policies contain any obvious weaknesses (such as the use of
‘unsafe-inline’, etc.)

Implementation Details

CORS
CORS will be implemented in the following files -
1. CorsMiddleware will be implemented in django.middleware.cors, following
the design of CsrfMiddleware
2. CORS decorators will be implemented in django.views.decorators.cors,
following the design of CSRF decorators
3. CORS settings will be added to django.conf.global_settings with their
defaults.
4. CORS system checks will be implemented in
django.core.checks.security.cors

CORS Settings
The default values for CORS settings will be
CORS_ALLOWED_ORIGINS = []
CORS_ALLOW_ALL_ORIGINS will not be defined
CORS_PATHS will not be defined
CORS_ALLOWED_METHODS = [
“DELETE”, “GET”, “OPTIONS”, “PATCH”, “POST”, “PUT”
]
CORS_ALLOWED_HEADERS = [
"accept", "accept-encoding", "authorization",
"content-type", "dnt", "origin", "user-agent",
"x-csrftoken", "x-requested-with",
]
CORS_EXPOSED_HEADERS = []
CORS_PREFLIGHT_MAX_AGE = 86400
CORS_ALLOW_CREDENTIALS = False

CORS Middleware
CorsMiddleware will implement process_request and process_response. During
process_request, CorsMiddleware will check if CORS headers are to be sent for this
request, and add a _cors_enabled attribute to the request. process_request will also
return responses for CORS preflight requests.

During process_response, we will add the actual headers to the response, and return
the response.

CORS Decorators
This section details the signatures of all CORS decorators and gives some usage examples
for decorators for which the usage may not be completely unambiguous from their
description.

1. @cors_protect(
allowed_origins: Sequence[str | Pattern[str]],
allow_all_origins: bool,
allowed_methods: Sequence[str],
allowed_headers: Sequence[str],
exposed_headers: Sequence[str],
preflight_max_age: int,
allow_credentials: bool,
policy_func: Callable[[HttpRequest], dict]
)
@cors_protect(
allow_all_origins=True,
allow_credentials=False
)
def my_view(request):
return render(request, ‘template.html’)

2. @cors_update(
allowed_origins: Sequence[str | Pattern[str]],
allow_all_origins: bool,
allowed_methods: Sequence[str],
allowed_headers: Sequence[str],
exposed_headers: Sequence[str],
preflight_max_age: int,
allow_credentials: bool,
policy_func: Callable[[HttpRequest], dict]
)

@cors_update(preflight_max_age=3600)
def my_view(request):
return render(request, ‘template.html’)

3. @cors_exempt()

Both cors_protect and cors_update will take a callable as an optional argument,


which will allow users to customize the CORS headers sent on a per-request basis. The
callable will be passed the request, and should return a dictionary having keys as CORS
settings names (in lowercase) and values as their corresponding values. An example of
such a callable is given below -

def policy_func(request):
if request.headers[‘Origin’] == ‘api.example.com’:
preflight_max_age = 86400
else:
preflight_max_age = 3600
return {preflight_max_age: preflight_max_age}

For convenience and consistency, we will provide constants/default values for CORS
headers and methods, which will be the same as the ones that django-cors-headers
provides.
CSP
CSP will be implemented in the following files -
1. CspMiddleware will be implemented in django.middleware.csp
2. CSP decorators will be implemented in django.views.decorators.csp
3. CSP settings will be added to django.conf.global_settings with their
default values.
4. The CSP context processor will be implemented in
django.template.context_processors
5. CSP system checks will be implemented in
django.core.checks.security.csp

CSP Middleware
CspMiddleware will implement process_request, where it will generate the CSP nonce
and attach it to the request object. It will also implement process_response, where it
will build the actual policy from configured settings and add the Content-Security-Policy
header to the response.

CspMiddleware will have utility methods to build the CSP policy as well as generate the
CSP nonce. The nonce will be generated similarly to how django-csp does it, by using
os.urandom(16).

CSP Settings
The default values for CSP settings will be

CSP_DEFAULT_POLICY = “default”
CSP_POLICIES = {
“default”: {}
}

The default CSP policy will be empty, since it is not feasible for Django to determine what
kind of default policy a project will need when a new project is created.

CSP Decorators
This section details the signatures of all CSP decorators and gives some usage examples
for decorators for which the usage may not be completely unambiguous from their
description. The csp_protect and csp_update decorator will take in all CSP settings as
arguments (in lowercase). There are 32 such settings, so I won’t list them all again here.
1. @csp_protect(**kwargs)

@csp_protect(script_src=[“‘self’”])
def my_view(request):
return render(request, ‘template.html’)

2. @csp_update(**kwargs)

@csp_update(include_nonce_in=[“script-src”])
def my_view(request):
return render(request, ‘template.html’)

3. @csp_exempt()
4. @csp_use_policy(policy: str)

@csp_use_policy(“report-only”)
def my_view(request):
return render(request, ‘template.html’)

Although we allow users to define multiple CSP policies, csp_use_policy will


only accept one policy as an argument.

Timeline
4th May 2023 to 28th May 2023 (Community Bonding Period)
● Set up test projects
● Set up testing environments for testing CORS and CSP
● Finalize any pending points of the proposal
● Discuss proposal specifics with community
● Create ticket(s) for new features - CORS and CSP

29th May 2023 to 31st May 2023


● Add all CORS settings to django.conf.global_settings
● Document all CORS settings in docs/ref/settings.txt

1st June 2023 to 20th June 2023


● Implement CorsMiddleware in django.middleware.cors
● Write tests for CorsMiddleware
● Write tests for CORS and CSRF together
● Document CorsMiddleware in docs/ref/middleware.txt
● Create docs/ref/cors.txt and document Cross Origin Resource Sharing
● Create docs/howto/cors.txt and write a short guide on best practices for CORS

21st June 2023 to 2nd July 2023


● Implement all CORS decorators
● Write tests for all CORS decorators
● Document all CORS decorators in docs/ref/cors.txt

3rd July 2023 to 13th July 2023


● Add system checks to ensure CORS settings are configured correctly
● Add system checks to ensure CORS settings are assigned the correct data type and
format
● Add tests for all new system checks
● Document new system checks in docs/ref/checks.txt

14th July 2023 to 16th July 2023


● Ask for final reviews on CORS PR
● Merge CORS PR into core

17th July 2023 to 23th July 2023


● Add all CSP settings to django.conf.global_settings
● Document all CSP settings in docs/ref/settings.txt

24th July 2023 to 13th August 2023


● Implement CspMiddleware in django.middleware.csp
● Add CSP_NONCE context processor
● Write tests for CspMiddleware
● Write tests for context processor
● Document CspMiddleware in docs/ref/middleware.txt
● Create docs/ref/csp.txt and document Content Security Policy
● Create docs/howto/csp.txt and write a short guide on best practices for CSP,
including common CSP pitfalls and how to avoid them

13th August 2023 to 20th August 2023


● Implement all CSP decorators
● Write tests for all CSP decorators
● Document all CSP decorators in docs/ref/csp.txt

21st August 2023 to 28th August 2023


● Add system checks to make sure CSP settings are configured correctly
● Add security checks to make sure CSP policies don’t have trivial bypasses and
vulnerabilities
● Add tests for all new system checks
● Document new system checks in docs/ref/checks.txt

29th August 2023 to 31st August 2023


● Ask for final reviews of CSP PR
● Merge CSP PR into core

Stretch Goals
1. Multiple CSP policies - It would be good to support sending multiple CSP policies,
either via the decorator, or via the CSP_DEFAULT_POLICY setting, or both. This
behavior is supported by browsers by simply sending multiple CSP headers for a
response, but it is harder to implement into Django, since response headers are
stored in a dictionary, which means we can’t specify the
Content-Security-Policy header more than once. To support multiple CSP
policies we will have to combine multiple policies into one, by considering the most
restrictive values for each directive, and then set the Content-Security-Policy
header once.

2. Hash based CSP - Support for hash based CSP would improve the security of all
Django projects. This could possibly be done by adding a setting called
SCRIPT_SRC_HASHES which would contain the hash values of the scripts the user
wants to include, but this idea doesn’t seem like the best approach and could be
refined.

3. Removing inline JavaScript from Admin GIS - Currently there is some inline
JavaScript in contrib/gis/templates/gis/openlayers.html and
contrib/gis/templates/gis/openlayers-osm.html, which will not work if
a content security policy is applied. Removing the inline JavaScript from the
template is non-trivial, but there is a closed PR by Claude P which shows roughly
how it can be done.

References
https://github.com/adamchainz/django-cors-headers/issues/578 - CORS headers not sent
for error responses
https://github.com/adamchainz/django-cors-headers/issues/422

https://csper.io/blog/multiple-policies - Multiple CSP policies

See warning here


https://django-csp-test.readthedocs.io/en/latest/configuration.html#other-settings - Why
not provide a way to only send CSP headers to certain paths.

Weaknesses in CSP configurations


https://storage.googleapis.com/pub-tools-public-publication-data/pdf/45542.pdf

About Me
Hi, my name is Hrushikesh Vaidya. I’m currently a second year engineering student at
the College of Engineering, Pune, India, majoring in electronics engineering with a
minor in computer science.

I’ve been programming in Python for 4 years, and working with Django for around 3
years. I’ve worked on a few Trac tickets over the last few months. I submitted a GSoC
proposal to add rate limiting to Django core in 2022. You can find that proposal and the
related discussion on the Django Forum.

My biggest & longest project so far has been a website offering an educational suite to
Indian teachers - including virtual classrooms, online student testing and analytics, and
much more. It is hosted at classberg.com. I sold a customized installation of the source
code for the same website to my old tuition class, and installed it on a subdomain on
their domain.

Working on classberg.com also led me to create a couple of my own open source


libraries - one in Python called docxlatex, a very minimal OOXML parser, and one in
JavaScript called MJXGUI, a GUI mathematical equation editor widget for the web.
However, none of the two projects have mature APIs, since I was a beginner when I
wrote them.

My username almost everywhere is hrushikeshrv (GitHub, LinkedIn, Discord, Django


Forum, and more). You can contact me at my username [at] gmail dot com. My portfolio
website is hosted at hrus.in.

You might also like