Repository: mozilla/django-csp (language: python)

Issue:
Allow enforced CSP and Report-Only at the same time
The CSP 1.0 specification allows this explicitly in the sixth paragraph of Section 3.3: http://www.w3.org/TR/CSP/#processing-model
An example is given, too: "For example, if a server operator is using one policy but wishes to experiment with a stricter policy, the server operator can monitor the stricter policy while enforcing the original policy."

This would require bigger changes to the way the middleware works (maybe a flag as a parameter to the decorators? ... something like only_report=True). If this is in scope for the middleware, I am willing to propose an implementation soon.

Allow enforced CSP and Report-Only at the same time
The CSP 1.0 specification allows this explicitly in the sixth paragraph of Section 3.3: http://www.w3.org/TR/CSP/#processing-model
An example is given, too: "For example, if a server operator is using one policy but wishes to experiment with a stricter policy, the server operator can monitor the stricter policy while enforcing the original policy."

This would require bigger changes to the way the middleware works (maybe a flag as a parameter to the decorators? ... something like only_report=True). If this is in scope for the middleware, I am willing to propose an implementation soon.

Possible to override CSP_INCLUDE_NONCE_IN per-view with a decorator?
It would be nice to be able to use `@csp_replace` and `@csp_update` on `CSP_INCLUDE_NONCE_IN`.
Also, even `@csp` which is supposed to override everything, does not override the nonce generation.

Why?
Because some browsers like Chromium do not respect the `style-src: 'unsafe-inline'` if a nonce is given.
So if you generate a nonce with `CSP_INCLUDE_NONCE_IN`, for your entire project, but you want for some precise view to use `'unsafe-inline'`, you need to also have a mean to disable the nonce. Or else the browser will just ignore the `'unsafe-inline'`.


Discussion / hints:
Definitely in scope, would be happy to support this, but it is going to require a big change, at least in terms of API, we should figure out how we want it to look first. Tagging in @graingert and @mozfreddyb.

One option would be to add a second, parallel set of settings, e.g.:

``` python
CSP_DEFAULT_SRC = '...'
CSP_RO_DEFAULT_SRC = '...'
CSP_IMG_SRC = '...'
CSP_RO_IMG_SRC = '...'
```

Then `utils.from_settings` would either need a companion (and probably a re-name) or a lot more complexity. The `CSP_REPORT_ONLY` boolean could still be used to flip the normal settings (i.e. without `_RO`) to report-only mode, or users could just define the `_RO` settings instead.

It's verbose, I don't love it. The decorator would still need a keyword to decide which policy is being updated (or both?). But it is a way to do site-wide alternate policies. Doing that via decorator would be painful.

Maybe instead of x number of settings, it makes sense to change it to one setting in a dict, in the style of other complex Django settings? e.g.

``` python
CSP_POLICY = {
    'default': {
        'DEFAULT_SRC': "'self'",
        'IMG_SRC': ["'self'", 'img-host.example.com'],
        'REPORT_ONLY': False,
    },
    'test_new_policy': {
        ...,
        'REPORT_ONLY': True,
    }
}

# Optional setting, defaults to 'default'
CSP_POLICIES = ['default', 'test_new_policy']
```

It's a bigger API change, but maybe lets things be cleaner when building policies? And it condenses, rather than expands, the namespace.

Alternate proposals?

I think you want two policy dicts so as to reflect how the underlying headers operate:

``` python
CSP_REPORT_ONLY_POLICY = {
    'DEFAULT_SRC': "'*'",
}
CSP_POLICY = {
     'DEFAULT_SRC': "'self'",
}
```

CSP technically does allow [multiple policies](https://dvcs.w3.org/hg/content-security-policy/raw-file/tip/csp-specification.dev.html#content-security-policy-header-field). Not sure exactly why you'd want to, but allowing `n>=1` policy definitions is more in line with that part of the spec.

In that case something like:

``` python
CSP_POLICIES = [
        {
            'DEFAULT_SRC': "'*'",
        },
        {
             'DEFAULT_SRC': "'self'",
             'REPORT_ONLY': True
        }
]
```

Rather than using a dict and defining order in another setting 

Also the CSP specification is invalid according to http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2

> Multiple message-header fields with the same field-name MAY be present in a message **if and only if the entire field-value for that header field is defined as a comma-separated list** [i.e., #(values)]. It MUST be possible to combine the multiple header fields into one "field-name: field-value" pair, without changing the semantics of the message, by appending each subsequent field-value to the first, each separated by a comma. The order in which header fields with the same field-name are received is therefore significant to the interpretation of the combined field value, and thus a proxy MUST NOT change the order of these field values when a message is forwarded. 

There is no precedent for an ordered list of dicts in the django settings. A dict allows these to be named, and a separate setting makes it easy to flip them on and off or reorder.

It's not our job here to resolve issues between RFC2616 and CSP.

I like @jsocol's original proposal, even though it is a bit verbose, but I prefer explicitly here.
It would need some work for current users of django-csp, but there's little we can do?!

Yep, I like the nested dict idea, too. However, why use "DEFAULT_SRC" rather than "default-src"? The latter would copy the original header more closely.

@mozfreddyb do you mean a nested dict or a parallel set of settings?

@qll copying the style of other settings (e.g. `DATABASES` and `CACHES`) but you're right, in this case copying the names directly from the header is a more compelling way to do it.

Nested dict as in your very first comment, @jsocol.

Love the nested dict idea.  You should add support to the decorators as well to replace or append policies based on name.

Knowing that there is support in the spec for multiple policies makes me question whether the CSP decorator (@csp) is adequately clear and explicit? It could be append CSP or replace CSP.  I'd suggest clearing the existing CSP should be done through one and only one very clear path (`@csp_exempt`) to avoid user error in a security-critical component of the app (I would rename this: @csp_clear or @csp_purge for clarity).  

Does it also make sense to think about plugging into the `sites` contrib app to allow per site CSP configurations in the DB? (for now, this could be name-based and hardcoded in settings, but eventually dynamic db-based CSPs could be supported...)

And finally a thought on backwards compatibility for users:

You can leave the current settings, just load them into the policy dict under the 'default' key.   If the 'default' key is also explicitly defined, throw an error. Set `CSP_POLICIES` to `['default']` by default ;)
  
Oh and this issue is **super** old. Would you like me to take this on?
@DylanYoung if you have the time for it, please go for it.

Regarding what you said...

> (I would rename this: @csp_clear or @csp_purge for clarity).

Personally, I don't think that's necessary. `@...except` follows various Django APIs and should be explicit enough imho.

I also don't think we need support for `django.contrib.sites` - I'd leave that to specific projects to handle dynamically. We shouldn't solve all problems in this library, that'd be out of scope.

Yeah, I like your idea of backwards compatibility. We could first introduce the new policy dictionary but also allow loading of it, if it's specified. That way we can safely deprecate old settings and transition to the new settings format.

Looking forward to reviewing your pull request!
any update on this? Would be nice to have this in django core, which waits on resolve of this ticket. thanks.
@hiren1771 Thanks for the ping.  My time got swallowed by life recently, but I'll see if I can get to this soon.  
Got a little start on it.  Not quite functional yet. 

Couple notes:

NEW settings:

CSP_POLICY_DEFINITIONS (dict)
CSP_UPDATE_TEMPLATE (str)
CSP_POLICIES (list, tuple)

The template is used for appending policies.  It will be used as the "base" of the new policy.  Can be set to `None` to use the provided policy "as-is".

I opted to make `csp_update` also allow appending.  Check out the new definition (9fe25d9) and let me know if it makes sense to y'all. 

Legacy settings are still supported and there's a toggle in the code to change their precedence after an initial deprecation period.
thanks, is this ticket still the right approach to use, considering this comment from the django ticket:
https://code.djangoproject.com/ticket/15727#comment:22
@hiren1771 Should be fine I think.  Once it's coded, they can fold it into core. I'll keep that in mind while I'm refactoring though. 
Ah I see, this module is also a middleware, and so is SecurityMiddleware. If there is anything I can help with, let me know. I have never worked on django code base but can do general python. Thanks again for picking this ticket up.
@hiren1771 Sweet.  I think I'll have a working version tomorrow (maybe you could give it a test and see if it all works and makes sense to you?), but there will be few final decisions to be made.

Pinging @jsocol @graingert @mozfreddyb to try to get some of those discussions rolling.  In particular,  there are a couple of "per policy" settings that aren't directives, report-only being one.  Do we want to differentiate these somehow from the actual directives?

As far as back compatibility goes,  I think the easiest thing is to continue supporting mixed case,  underscores,  and hyphens, then just normalise in the backend.   Does that make sense to everyone? (This way we can keep using kwargs too).
Also, I think I need a bit of clarification on the spec. My reading is that multiple comma delineated values are permitted, but only one of each of the two headers.  Is that correct?

If not, then I think we have to impose that restriction anyways as I don't think Django's response supports multiple headers with the same key. 
All tests are passing now.  Haven't written tests for the multi-policy case yet so there might still be some bugs lurking in there.  Still have some cleanup to do, but we're close :) 
A few notes before I forget and for reference when I go to do the docs:

`csp_update` can currently append, but `csp_replace` can not (it's trivial to make it so though if desired)
They both take an optional list of names which defaults to `['default']` and any single-policy `kwargs` will be applied to policies with that key (this preserves back-compat and allows bulk updates).
They also take a `csp_definitions` object (which currently does not support the "single-policy" mode) 

The `csp` decorator works a little differently.  Its first argument is an iterable of policies, which can be either a two-tuple `(name, csp)`,  a csp dictionary (random name will be applied), or a name which will be looked up both in the currently applied policy definitions and in the base config.  `kwargs` can be passed in for back-compat as well; they will also be assigned a random name.

There is also a new `csp_order` (open to renaming) decorator that reorders existing policies based on index or name and can also trim off extra policies (though always has at least one: use `csp_exempt` to remove a policy).  Maybe `csp_select` would be a better name?  It works with an iterable of mixed names and indexes.
FYI: I opted to make the "pseudo directives" identical to the old settings but lowercase.  I think it's useful to be able to visually disambiguate them via the '-' versus '_' distinction.

(In fact I could even use it in the code instead the lookup dictionary... might be cleaner.    Yeah; I'll clean that up.  I think there's even some old mechanics in there for when I was going to change the names that will be unnecessary now.
Identify pseudo settings: `bool(kwarg.lower() in DIRECTIVES)`
Get all pseudo settings: `d for d in DIRECTIVES if '_' in d`

Yes yes, that will be much nicer :D
)
> Also, I think I need a bit of clarification on the spec. My reading is that multiple comma delineated values are permitted, but only one of each of the two headers. Is that correct?

I don't think I understand your question. Can you rephrase, give an example or a link to the spec text you're talking about? :)
The headers are concatenated like any other multi value header

On Mon, 9 Mar 2020, 08:04 Frederik Braun, <notifications@github.com> wrote:

> Also, I think I need a bit of clarification on the spec. My reading is
> that multiple comma delineated values are permitted, but only one of each
> of the two headers. Is that correct?
>
> I don't think I understand your question. Can you rephrase, give an
> example or a link to the spec text you're talking about? :)
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/mozilla/django-csp/issues/36?email_source=notifications&email_token=AADFATBKG4IKFYHVDTFJV6TRGSPKRA5CNFSM4AJ6MI52YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEOGBVOI#issuecomment-596384441>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AADFATF7VWKO2Y6PRT4WKFDRGSPKRANCNFSM4AJ6MI5Q>
> .
>

@graingert That's what I thought.  Thanks for the confirmation.  Note that there are headers that are permitted multiple times: https://stackoverflow.com/questions/4371328/are-duplicate-http-response-headers-acceptable

Mostly I just wanted to confirm that preserving the order *between* multiple report-only and regular policies wasn't desired / allowed by the spec.  I don't think Django supports it anyways (after a quick look at the response implementation),  so the point is kind of moot anyways :D
Any thoughts on the optional space between commas? We can remove it to save a few bytes on the wire. Or keep it in for legibility. Or toggle it based on the DEBUG setting. 
> Any thoughts on the optional space between commas? We can remove it to save a few bytes on the wire. Or keep it in for legibility. Or toggle it based on the DEBUG setting.

Does it matter, are the headers not compressed anyway?
@hiren1771 Good point.  I'm not 100% on that,  but they probably are.  I'm just going to leave that as is for now.
Hi, I'm just curious what the status of this work is? Is there stuff that needs more hands? 
> Hi, I'm just curious what the status of this work is? Is there stuff that needs more hands?

Good question. It looks like Casey is pretty far along.

@DylanYoung thanks for working on this! Is #179 your most recent work or do you have any unpushed changes? Was there anything else you were planning to add to the branch?
Sorry all!  I almost got this done and then had some life stuff that took
me away from it.  I'll review my current work tomorrow and let you know.  I
believe there were just a few last questions to be answered about how we
want the API to function and then it should be pretty well there!

Best,

Casey

On Fri, 30 Jul 2021 at 11:30, Greg Guthe ***@***.***> wrote:

> Hi, I'm just curious what the status of this work is? Is there stuff that
> needs more hands?
>
> Good question. It looks like Dylan is pretty far along.
>
> @DylanYoung <https://github.com/DylanYoung> thanks for working on this!
> Is #179 <https://github.com/mozilla/django-csp/pull/179> your most recent
> work or do you have any unpushed changes? Was there anything else you were
> planning to add to the branch?
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/mozilla/django-csp/issues/36#issuecomment-889932297>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABMG3FDP45565UKLLNBVBXLT2KZRZANCNFSM4AJ6MI5Q>
> .
>

 @DylanYoung did you get a chance to review your work? do you have any unpushed changes? Thanks!
I took a look at my work and added some notes to the PR for when I get back to it.  If anyone has any thoughts on my comments so far, feel free to reply.  I think the next step is for me to add some more detailed unit tests and that should clarify how things are working now so we/you can decide if any major design changes are needed (in particular, I got a little bogged down in the decorators and maybe made them a bit overly flexible).  

> CSP technically does allow [multiple policies](https://dvcs.w3.org/hg/content-security-policy/raw-file/tip/csp-specification.dev.html#content-security-policy-header-field). Not sure exactly why you'd want to, but allowing `n>=1` policy definitions is more in line with that part of the spec.

As for this comment, I'm not sure Django supports this?  It's still nice to have multiple policies defined in a central spot though, as then the decorators can just reference them by name. 

EDIT: I'm silly.  This is already supported in a backhanded way.  It's handled by merging the policies together. 
I haven't yet checked whether I have any further work on my local (it's on my other computer).  At most I think I started on some doc updates.  I'll check shortly. 
I've rebased this on the latest master, worked out a bunch of kinks, and written a handful of tests.  Still needs doc updates, branch cleanup/squash down, and few last design decisions.  I've simplified things significantly and instead of complicating the behaviour of the existing decorators, I've added two new ones: `csp_select` and `csp_append` which selects policies from those that are available (essentially changing CSP_POLICIES on a per-view basis) and appends a new policy to the currently selected policies, respectively.

Only major functional change remaining is to fold in `CSP_EXCLUDE_URL_PREFIXES` and then doc updates and I think it should be ready to go.




`CSP_EXCLUDE_URL_PREFIXES` is folded in now. 

Needs:
- [x] Finish up docs updates

Nice-to-have:
- [ ] More utils tests and possibly other tests

Decisions:

- [ ] test dependency updates: do we want them done here?
- [ ] Deprecate block-all-mixed-content directive: do we want that done here?
- [ ] removal of some outdated compatibility code: again, do we want that done here?
- [ ] FIXME: should `csp_update` allow updating report_only?  It's an easy fix I think (`csp_replace` does).  
- [x] `        allow_legacy=True  # not hasattr(settings, 'CSP_POLICY_DEFINITIONS'),`
   - We'll need to update a bunch of tests if we want to toggle this.  It disallows mixing the legacy settings with the new settings. 

@DylanYoung  - I did not test any of this code and it may contain inaccuracies/errors but this hopefully this helps you get ideas and fulfills your checklist.

**Background on report-only:**

Content Security Policy (CSP) report-only mode is a feature of content security policies that allows you to test a new policy without actually enforcing it. When you enable CSP report-only mode, the browser will not block any content that violates the policy. Rather, it will send reports summarizing policy violations to a specified report-uri.

In other words, CSP report-only mode provides a way to monitor and debug your content security policy before putting it into full enforcement mode. It enables the web administrators to evaluate the potential impact and effectiveness of their CSP configuration by identifying which resources would be blocked if the policy were in enforcement mode.

The report-uri is the URL to which violation reports should be sent; it is specified as a directive within the CSP header. Developers can use these reports to fine-tune their policy to avoid breaking existing code while still maintaining a secure content delivery. Reporting helps web admins to discover and address any CSP issues with application functionality or 3rd party domains used on the website.

**Possible implementation of the updated csp_update view to allow updating report_only:**

```
from django.shortcuts import render, redirect
from django.views.decorators.http import require_POST
from csp.models import CSPReport
from django.utils import timezone
from datetime import timedelta


@require_POST
def csp_update(request):
    """View for updating the CSP policy and report-only mode"""

    # Check if the request contains report-only field and its value is True
    report_only = request.POST.get('report_only') == 'True'

    # Check if the request contains a CSP policy and its value is valid JSON
    csp_policy = request.POST.get('policy')
    if csp_policy:
        try:
            json.loads(csp_policy)
        except ValueError:
            return render(request, 'csp/update.html', {
                'error': 'Invalid CSP policy JSON'
            })

        # save the new policy with the report-only settings
        csp_report = CSPReport.objects.create(
            policy=csp_policy,
            report_only=report_only,
            created_at=timezone.now()
        )

        # delete old reports if they are too old
        too_old = timezone.now() - timedelta(days=30)
        CSPReport.objects.filter(created_at__lt=too_old).delete()

    # redirect back to update page with success message
    return redirect('csp_update_view', success=True)

```

This implementation checks whether the request contains a report_only field and if it is set to True. If it is, then the middleware is enabled in report-only mode. It also validates the provided CSP policy JSON and saves it along with the report-only settings. Additionally, it deletes old reports that are older than 30 days to prevent the database from growing too large over time.

Finally, the view redirects back to the update page with a success message indicating that the policy has been successfully updated.


***

**Random Unit tests**

```
from django.test import TestCase
from csp.utils import build_policy


class UtilsTestCase(TestCase):
    def test_build_policy_empty_dict(self):
        """Test build_policy with an empty dict"""
        policy = build_policy({})
        self.assertEqual(policy, "")

    def test_build_policy_default_src(self):
        """Test build_policy with a default-src directive"""
        policy = build_policy({"default-src": ["https://example.com/%22]%7D)
        self.assertEqual(policy, "default-src https://example.com/;%22)

    def test_build_policy_multiple_directives(self):
        """Test build_policy with multiple directives"""
        policy = build_policy({
            "default-src": ["https://example.com/"],
            "script-src": ["https://scripts.example.com/"],
            "frame-src": ["https://frames.example.com/"],
        })
        expected_policy = "default-src https://example.com/; " \
            "script-src https://scripts.example.com/; " \
            "frame-src https://frames.example.com/;"
        self.assertEqual(policy, expected_policy)

    def test_build_policy_unsupported_directive_key(self):
       """Test build_policy with an unsupported directive key"""
        with self.assertRaises(ValueError):
            build_policy({"unsupported-src": ["https://example.com/%22]%7D)

    def test_build_policy_unsupported_directive_value(self):
        """Test build_policy with an unsupported directive value"""
        with self.assertRaises(ValueError):
            build_policy({"default-src": ["example.com/"]})

    def test_build_policy_invalid_directive_value(self):
        """Test build_policy with an invalid directive value type"""
        with self.assertRaises(TypeError):
            build_policy({"default-src": "https://example.com/%22%7D)
```



***


**Util tests for each function in utils.py**

```
def test_normalize_config(self):
    """ 
    Test _normalize_config function
    """
    # Test with empty config
    config = {}
    expected_output = {}
    output = _normalize_config(config)
    self.assertEqual(output, expected_output)

    # Test with incomplete config
    config = {"default-src": ["'self'"], "frame-src": ["'none'"]}
    expected_output = {
        "default-src": ["'self'"],
        "frame-src": ["'none'"],
        "script-src": ["'self'"],
        "style-src": ["'self'"],
        "img-src": ["'self'"],
        "media-src": ["'self'"],
        "font-src": ["'self'"],
        "connect-src": ["'self'"],
        "object-src": ["'none'"],
        "sandbox": [],
        "report-uri": [],
    }
    output = _normalize_config(config)
    self.assertEqual(output, expected_output)

    # Test with complete config
    config = {
        "default-src": ["'self'"],
        "frame-src": ["'none'"],
        "script-src": ["'self'", "https://example.com"],
        "style-src": ["'self'"],
        "img-src": ["'self'"],
        "media-src": ["'self'"],
        "font-src": ["'self'"],
        "connect-src": ["'self'"],
        "object-src": ["'none'"],
        "sandbox": [],
        "report-uri": ["https://example.com/report"],
    }
    expected_output = config
    output = _normalize_config(config)
    self.assertEqual(output, expected_output)
```





```
from django.test import TestCase
from csp.utils import build_policy

class BuildPolicyTestCase(TestCase):
    def test_build_policy(self):
        policy = {
            'default-src': ["'self'"],
            'script-src': ["'self'", 'ajax.googleapis.com'],
            'style-src': ["'self'"],
            'font-src': ["'self'", 'ajax.googleapis.com', 'fonts.gstatic.com'],
            'img-src': ["'self'", 'ajax.googleapis.com'],
            'connect-src': ["'self'", 'ajax.googleapis.com'],
            'media-src': ["'self'"],
            'object-src': ["'none'"],
            'frame-src': ["'self'"],
            'sandbox': ['allow-same-origin'],
            'report-uri': ['/csp-violation-report/'],
        }

        result = build_policy(policy)

        expected = (
            "default-src 'self'; "
            "script-src 'self' ajax.googleapis.com; "
            "style-src 'self'; "
            "font-src 'self' ajax.googleapis.com fonts.gstatic.com; "
            "img -src 'self' ajax.googleapis.com; "
            "connect-src 'self' ajax.googleapis.com; "
            "media-src 'self'; "
            "object-src 'none'; "
            "frame-src 'self'; "
            "sandbox allow-same-origin; "
            "report-uri /csp-violation-report/;"
        )

        self.assertEqual(result, expected)

```




```
from django.test import TestCase
from csp.utils import _update_policy

class UpdatePolicyTestCase(TestCase):

    def setUp(self):
        self.policy = {
            'default-src': ["'self'"],
            'script-src': ["'none'"],
            'object-src': ["'self'"],
        }

    def test_empty_update(self):
        expected = self.policy.copy()
        result = _update_policy(self.policy, {})
        self.assertDictEqual(result, expected)

    def test_one_source_update(self):
        update = {'script-src': ["'self'"]}
        expected = {
            'default-src': ["'self'"],
            'script-src': ["'self'"],
            'object-src': ["'self'"],
        }
        result = _update_policy(self.policy, update)
        self.assertDictEqual(result, expected)

    def test_multiple_sources_update(self):
        update = {
            'default-src': ["'none'"],
            'frame-src': ["'self'", 'https://example.com/'],
        }
        expected = {
            'default-src': ["'none'"],
            'script-src': ["'none'"],
            'object-src': ["'self'"],
            'frame-src': ["'self'", 'https://example.com/'],
        }
        result = _update_policy(self.policy, update)
        self.assertDictEqual(result, expected)
```




```
from django.test import TestCase
from csp.utils import _replace_policy

class ReplacePolicyTestCase(TestCase):
    def test_replace_policy(self):
        old_policy = "default-src 'none';"
        new_policy = "default-src 'self'; script-src 'self';"
        result = _replace_policy(old_policy, new_policy)
        expected = "default-src 'self'; script-src 'self';"
        self.assertEqual(result, expected)

        old_policy = "default-src 'self'; script-src 'self';"
        new_policy = "connect-src 'self'; img-src 'self';"
        result = _replace_policy(old_policy, new_policy)
        expected = "connect-src 'self'; img-src 'self';"
        self.assertEqual(result, expected)
```




```
from django.test import TestCase
from csp.utils import _compile_policy

class CompilePolicyTestCase(TestCase):

    def test_compile_policy(self):
        
        policy = {
            'default-src': [],
            'script-src': ['self', 'unsafe-hashes'],
            'style-src': ['self'],
        }
        
        expected_result = "default-src 'none'; script-src 'self' 'unsafe-hashes'; style-src 'self'"
        
        result = _compile_policy(policy)
        
        self.assertEqual(expected_result, result)

```




```
from django.test import TestCase
from csp.utils import kwarg_to_directive

class KwargsToDirectiveTestCase(TestCase):
    def test_kwarg_to_directive(self):
        kwarg_dict = {'default_src': ("'self'", 'example.com', '*.example.com'),
                      'script_src': ("'self'", 'example.org', '*.example.org')}

        expected_directive_str = "default-src 'self' example.com *.example.com; script-src 'self' example.org *.example.org"
        actual_directive_str = kwarg_to_directive(kwarg_dict)

        self.assertEqual(actual_directive_str, expected_directive_str)
```





```
from django.test import TestCase
from csp.utils import PolicyNames


class PolicyNamesTest(TestCase):
    def test_policynames_default(self):
        pn = PolicyNames()

        self.assertEqual(pn.script_src, "'self'")
        self.assertEqual(pn.style_src, "'self'")
        self.assertEqual(pn.img_src, "'self'")
        self.assertEqual(pn.connect_src, "'self'")
        self.assertEqual(pn.font_src, "'self'")
        self.assertEqual(pn.base_uri, "'self'")
        self.assertEqual(pn.frame_src, "'self'")
        self.assertEqual(pn.media_src, "'self'")
        self.assertEqual(pn.object_src, "'self'")
        self.assertEqual(pn.plugin_types, "")
        self.assertEqual(pn.report_uri, "/csp-report/")
        self.assertEqual(pn.visible_annotations, "")
        self.assertEqual(pn.use_script_nonce, False)
        self.assertEqual(pn.use_style_nonce, False)
        self.assertEqual(pn.nonce_random, False)

    def test_policynames_custom(self):
        pn = PolicyNames({
            "img_src": "https://example.com/",
            "style_src": "'self' https://cdn.example.com/'",
            "connect_src": "'self' ws://example.com'",
            "font_src": "'self' https://fonts.example.com/'",
            "frame_src": "'self' https://iframe.example.com/'",
            "media_src": "'self' https://media.example.com/'",
            "object-src": "'none'",
            "report_uri": "/my-csp-report"
        })

        self.assertEqual(pn.script_src, "'self'")
        self.assertEqual(pn.style_src, "'self' https://cdn.example.com/%27%22)
        self.assertEqual(pn.img_src, "https://example.com/%22)
        self.assertEqual(pn.connect_src, "'self' ws://example.com'")
        self.assertEqual(pn.font_src, "'self' https://fonts.example.com/%27%22)
        self.assertEqual(pn.base_uri, "'self'")
        self.assertEqual(pn.frame_src, "'self' https://iframe.example.com/%27%22)
        self.assertEqual(pn.media_src, "'self' https://media.example.com/%27%22)
        self.assertEqual(pn.object_src, "'none'")
        self.assertEqual(pn.plugin_types, "")
        self.assertEqual(pn.report_uri, "/my-csp-report")
        self.assertEqual(pn.visible_annotations, "")
```




```
from csp.utils import _clean_input_policy
import unittest

class TestCleanInputPolicy(unittest.TestCase):
    
    def test_clean_input_policy_1(self):
        """Test empty string input"""
        output = _clean_input_policy("")
        self.assertEqual(output, "")
        
    def test_clean_input_policy_2(self):
        """Test input with only spaces"""
        output = _clean_input_policy("     ")
        self.assertEqual(output, "")
        
    def test_clean_input_policy_3(self):
        """Test input with valid policy"""
        input_policy = "default-src 'self'; script-src 'self' https://example.com"
        output = _clean_input_policy(input_policy)
        self.assertEqual(output, input_policy)
        
    def test_clean_input_policy_4(self):
        """Test input with invalid policy"""
        input_policy = "default-src 'self'; invalid-policy; script-src 'self' https://example.com"
        output = _clean_input_policy(input_policy)
        expected_output = "default-src 'self'; script-src 'self' https://example.com"
        self.assertEqual(output, expected_output)
```






```
import unittest
from csp.utils import _kwargs_to_directives

class TestKwargsToDirectives(TestCase):
    def test_kwargs_to_directives_success(self):
        input_kwargs = {'height': 100, 'width': 200, 'color': 'red'}
        expected_output = ['height=100', 'width=200', 'color=red']
        self.assertEqual(_kwargs_to_directives(input_kwargs), expected_output)

    def test_kwargs_to_directives_empty_kwargs(self):
        input_kwargs = {}
        expected_output = []
        self.assertEqual(_kwargs_to_directives(input_kwargs), expected_output)

    def test_kwargs_to_directives_non_string_value(self):
        input_kwargs = {'height': 100, 'width': 200, 'color': True}
        expected_output = ['height=100', 'width=200', 'color=True']
        self.assertEqual(_kwargs_to_directives(input_kwargs), expected_output)

    def test_kwargs_to_directives_null_value(self):
        input_kwargs = {'height': 100, 'width': None, 'color': 'red'}
        expected_output = ['height=100', 'width=None', 'color=red']
        self.assertEqual(_kwargs_to_directives(input_kwargs), expected_output)

    def test_kwargs_to_directives_invalid_key(self):
        input_kwargs = {'height': 100, 'width': 200, 123: 'red'}
        with self.assertRaises(TypeError):
            _kwargs_to_directives(input_kwargs)

    def test_kwargs_to_directives_invalid_value(self):
        input_kwargs = {'height': 100, 'width': '200', 'color': None}
        expected_output = ['height=100', 'width=200', 'color=None']
        self.assertEqual(_kwargs_to_directives(input_kwargs), expected_output)

```


```
from django.test import TestCase
from csp.utils import _policies_from_names_and_kwargs

class TestUtils(TestCase):
    def test_policies_from_names_and_kwargs(self):
        names = ['default-src', 'script-src']
        kwargs = {'default-src': ["'self'"], 'script-src': ["'self'", 'example.com']}
        policies = _policies_from_names_and_kwargs(names, kwargs)
        expected_policies = {
            'default-src': ["'self'"],
            'script-src': ["'self'", 'example.com']
        }
        self.assertDictEqual(policies, expected_policies)
```





```
from django.test import TestCase
from csp.utils import _policies_from_args_and_kwargs

class TestUtils(TestCase):
    def test_policies_from_args_and_kwargs(self):
        args = ('default-src', "'self'")
        kwargs = {'script-src': ["'self'", 'example.com']}
        policies = _policies_from_args_and_kwargs(args, kwargs)
        expected_policies = {
            'default-src': ["'self'"],
            'script-src': ["'self'", 'example.com']
        }
        self.assertDictEqual(policies, expected_policies)

```




```
from django.test import TestCase
from csp.utils import _default_attr_mapper

class TestUtils(TestCase):
    def test_default_attr_mapper(self):
        obj = {'foo': 'bar', 'baz': 'qux'}
        attrs = ['foo', 'baz']
        result = _default_attr_mapper(obj, attrs)
        expected_result = {'foo': 'bar', 'baz': 'qux'}
        self.assertDictEqual(result, expected_result)
```




```
from django.test import TestCase
from csp.utils import _bool_attr_mapper

class TestUtils(TestCase):
    def test_bool_attr_mapper(self):
        obj = {'foo': True, 'bar': False}
        attrs = ['foo', 'bar']
        result = _bool_attr_mapper(obj, attrs)
        expected_result = {'foo': 'true', 'bar': 'false'}
        self.assertDictEqual(result, expected_result)
```



```
import asyncio
from django.test import TestCase
from csp.utils import _async_attr_mapper

class TestUtils(TestCase):
    async def test_async_attr_mapper(self):
        async def async_func():
            await asyncio.sleep(1)
            return 'async_value'

        obj = {'foo': async_func()}
        attrs = ['foo']
        result = await _async_attr_mapper(obj, attrs)
        expected_result = {'foo': 'async_value'}
        self.assertDictEqual(result, expected_result)

```


```
from django.test import TestCase
from csp.utils import _unwrap_script

class TestUtils(TestCase):
    def test_unwrap_script(self):
        input_html = '<script>console.log("Hello, world!")</script>'
        expected_output = 'console.log("Hello, world!")'
        result = _unwrap_script(input_html)
        self.assertEqual(result, expected_output)
```



```
from django.test import TestCase
from csp.utils import build_script_tag

class TestUtils(TestCase):
    def test_build_script_tag(self):
        input_src = 'https://example.com/script.js'
        expected_output = '<script src="https://example.com/script.js" nonce="abc123"></script>'
        result = build_script_tag(input_src, 'abc123')
        self.assertEqual(result, expected_output)
```






```
import os
import re
import unittest
import requests

class TestDependencyUpdates(unittest.TestCase):

    def test_dependency_updates(self):
        repo_path = "https://api.github.com/repos/DylanYoung/django-csp/branches/GH-36"
        response = requests.get(repo_path).json()
        dependencies = response.get("dependencies", {})

        for dep in dependencies:
            current_version = dependencies.get(dep).get("version")
            latest_version = self.get_latest_version(dep)

            message = f"{dep} dependency not updated to latest version"
            self.assertEqual(current_version, latest_version, message)

    def get_latest_version(self, dependency):
        package_name = re.sub(r'[^\w\s]','', dependency.lower())
        package_path = f"https://pypi.org/pypi/{package_name}/json"
        response = requests.get(package_path).json()

        version_numbers = []
        for version_number in response.get("releases", {}).keys():
            version_numbers.append(version_number)
        version_numbers.sort(reverse=True)

        return version_numbers[0]
```


***

**block-all-mixed-content**

The block-all-mixed-content directive code in the django-csp library can be found in the csp.middleware.CSPMiddleware class in the middleware.py file. 

Here's an example of how to set the block-all-mixed-content directive in the CSP header using the CSPMiddleware class:

```
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import cached_property
from csp.utils import build_policy_string

class CSPMiddleware(MiddlewareMixin):
    @cached_property
    def get_response(self, request):
        csp_header = build_policy_string({
            'default-src': ["'self'"],
            'block-all-mixed-content': True,
        })
        response['Content-Security-Policy'] = csp_header
```



In this example, the block-all-mixed-content directive is included in the CSP header policy string as a boolean value. This directive instructs the browser to block all HTTP content that is loaded over HTTPS.







```
class CSPMiddleware(MiddlewareMixin):
    """
    Middleware to add Content-Security-Policy headers to responses.

    See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
    """

    def init(self, get_response=None, kwargs):
        self.get_response = get_response
        self.config = kwargs.get('config', {})
        self.config['report_uri'] = kwargs.get('report_uri', self.config.get('report_uri', None))
        self.config['browser_sniff'] = kwargs.get('browser_sniff', False)
        self.config.update(DEFAULT_SRC=None, SCRIPT_SRC=None, OBJECT_SRC=None, STYLE_SRC=None, IMG_SRC=None, MEDIA_SRC=None,
                            FRAME_SRC=None, FONT_SRC=None, CONNECT_SRC=None, BASE_URI=None, FORM_ACTION=None,
                            REPORT_ONLY=None, BLOCK_ALL_MIXED_CONTENT=None, UPGRADE_INSECURE_REQUESTS=None, SANDBOX=None)


 if 'block_all_mixed_content' in self.config:
            warnings.warn(
                'The "block_all_mixed_content" directive has been deprecated by the latest Content Security Policy '
                'standard. Please remove this directive from your Content Security Policy. To block all mixed content in modern browsers, use the "block-all" directive instead',
                DeprecationWarning)
            self.config.pop('block_all_mixed_content', None)

    def call(self, request):
        response = self.get_response(request)
        if not response.has_header('Content-Security-Policy'):
            csp_header = self.build_header(request)
            response['Content-Security-Policy'] = csp_header
        return response

    def build_header(self, request):
        """
        Build a Content Security Policy header value.
        """
        builder = CSPBuilder(self.config, request)
        return builder.build()

```


It is generally a good idea to follow current best practices when it comes to web security. The block-all-mixed-content directive is now deprecated because it can sometimes lead to compatibility issues with certain websites that incorporate both http and https resources. 

In its place, the block-all directive should be used to block all mixed content by default. This directive will ensure that browsers block insecure content automatically while allowing secure content to load normally. 

Therefore, deprecating the block-all-mixed-content directive and replacing it with the block-all directive is a good security measure and aligns with the latest security standards.







***

**Outdated Code**

To locate outdated compatibility code in the django-csp library, you can search for code that references deprecated or removed features, such as django.conf.urls.defaults, django.utils.importlib, or django.core.urlresolvers. You can also look for code that uses older syntax or patterns that have been replaced with newer, more efficient alternatives. 


It is generally a good idea to remove outdated code in any software project, including the django-csp package. Outdated code often refers to code that is no longer maintained, has been replaced with better alternatives or is no longer compatible with newer versions of the software platform or libraries. This code can cause problems like security vulnerabilities, performance issues, and functionality problems. Additionally, removing outdated code can improve the maintainability of the codebase, make it easier to introduce new features, and make it more understandable for developers who are new to the project. However, careful consideration and testing should be taken before removing any code to ensure that it will not break existing functionality or cause other issues.

***



**Doing something like the original comments of this thread**


```
from csp.middleware import CSPMiddleware
from csp.utils import build_policy

class MyCSPMiddleware(CSPMiddleware):

    def __init__(self, get_response=None):
        self.policies = []
        # CSP_POLICIES is a list of dictionaries
        for policy in settings.CSP_POLICIES:
            self.add_policy(policy)
        super().__init__(get_response)

    def add_policy(self, policy):
        """Add a policy to be enforced"""
        report_only = policy.pop('REPORT_ONLY', False)
        csp_policy = build_policy(policy)
        self.policies.append((report_only, csp_policy))

    def process_response(self, request, response):
        """Add CSP headers to the response"""
        for report_only, policy in self.policies:
            response._csp_policy = policy
            response.report_only = report_only
        return super().process_response(request, response)

```

With this implementation, the MyCSPMiddleware class extends the CSPMiddleware class provided by django-csp. In the constructor, the CSP_POLICIES list defined in settings.py is processed and each policy is added to a list of policies to be enforced. The add_policy() method appends policies to the policy list by creating a new policy object using the csp.utils.build_policy() method.

In the process_response() method, the policies are added to the response object by setting the _csp_policy attribute to the policy object, and the report_only attribute to the value of the REPORT_ONLY parameter in the policy dictionary. Finally, the process_response() method of the parent class is called to complete the response processing.

This implementation allows for multiple CSP policies to be defined in the CSP_POLICIES settings in settings.py, and these policies will be added to the response headers for every request processed by the middleware.


```
CSP_POLICIES = [
    {
        'default-src': ['self'],
        'img-src': ['*', 'data:'],
        'script-src': ['self', 'ajax.googleapis.com'],
        'style-src': ['self']
    },
    {
        'default-src': ['none'],
        'connect-src': ['self'],
        'img-src': ['self', 'data:'],
        'script-src': ['self'],
        'style-src': ['self']
    }
]

MIDDLEWARE = [
    # ...
    'path.to.MyCSPMiddleware',
    # ...
]
```


In this example, CSP_POLICIES is a list of two policy dictionaries, each defining a different policy. Each policy dictionary contains directives and sources that specify the allowed sources for various types of content. In this example, the first policy allows content from the local domain, Google's AJAX API, and data URIs for images. The second policy allows content only from the local domain and does not allow any content by default.

The `MyCSPMiddleware middleware is added to the MIDDLEWARE setting to process the policies defined in CSP_POLICIES and add them to the response headers.

With this configuration, any responses generated by Django will contain the CSP header specifying the policies defined in CSP_POLICIES. Any violations against these policies will be logged as specified in the policy. This helps to reduce the risk of cross-site scripting (XSS) attacks and safeguarding the application from other potential security exploits.
Definitely in scope, would be happy to support this, but it is going to require a big change, at least in terms of API, we should figure out how we want it to look first. Tagging in @graingert and @mozfreddyb.

One option would be to add a second, parallel set of settings, e.g.:

``` python
CSP_DEFAULT_SRC = '...'
CSP_RO_DEFAULT_SRC = '...'
CSP_IMG_SRC = '...'
CSP_RO_IMG_SRC = '...'
```

Then `utils.from_settings` would either need a companion (and probably a re-name) or a lot more complexity. The `CSP_REPORT_ONLY` boolean could still be used to flip the normal settings (i.e. without `_RO`) to report-only mode, or users could just define the `_RO` settings instead.

It's verbose, I don't love it. The decorator would still need a keyword to decide which policy is being updated (or both?). But it is a way to do site-wide alternate policies. Doing that via decorator would be painful.

Maybe instead of x number of settings, it makes sense to change it to one setting in a dict, in the style of other complex Django settings? e.g.

``` python
CSP_POLICY = {
    'default': {
        'DEFAULT_SRC': "'self'",
        'IMG_SRC': ["'self'", 'img-host.example.com'],
        'REPORT_ONLY': False,
    },
    'test_new_policy': {
        ...,
        'REPORT_ONLY': True,
    }
}

# Optional setting, defaults to 'default'
CSP_POLICIES = ['default', 'test_new_policy']
```

It's a bigger API change, but maybe lets things be cleaner when building policies? And it condenses, rather than expands, the namespace.

Alternate proposals?

I think you want two policy dicts so as to reflect how the underlying headers operate:

``` python
CSP_REPORT_ONLY_POLICY = {
    'DEFAULT_SRC': "'*'",
}
CSP_POLICY = {
     'DEFAULT_SRC': "'self'",
}
```

CSP technically does allow [multiple policies](https://dvcs.w3.org/hg/content-security-policy/raw-file/tip/csp-specification.dev.html#content-security-policy-header-field). Not sure exactly why you'd want to, but allowing `n>=1` policy definitions is more in line with that part of the spec.

In that case something like:

``` python
CSP_POLICIES = [
        {
            'DEFAULT_SRC': "'*'",
        },
        {
             'DEFAULT_SRC': "'self'",
             'REPORT_ONLY': True
        }
]
```

Rather than using a dict and defining order in another setting 

Also the CSP specification is invalid according to http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2

> Multiple message-header fields with the same field-name MAY be present in a message **if and only if the entire field-value for that header field is defined as a comma-separated list** [i.e., #(values)]. It MUST be possible to combine the multiple header fields into one "field-name: field-value" pair, without changing the semantics of the message, by appending each subsequent field-value to the first, each separated by a comma. The order in which header fields with the same field-name are received is therefore significant to the interpretation of the combined field value, and thus a proxy MUST NOT change the order of these field values when a message is forwarded. 

There is no precedent for an ordered list of dicts in the django settings. A dict allows these to be named, and a separate setting makes it easy to flip them on and off or reorder.

It's not our job here to resolve issues between RFC2616 and CSP.

I like @jsocol's original proposal, even though it is a bit verbose, but I prefer explicitly here.
It would need some work for current users of django-csp, but there's little we can do?!

Yep, I like the nested dict idea, too. However, why use "DEFAULT_SRC" rather than "default-src"? The latter would copy the original header more closely.

@mozfreddyb do you mean a nested dict or a parallel set of settings?

@qll copying the style of other settings (e.g. `DATABASES` and `CACHES`) but you're right, in this case copying the names directly from the header is a more compelling way to do it.

Nested dict as in your very first comment, @jsocol.

Love the nested dict idea.  You should add support to the decorators as well to replace or append policies based on name.

Knowing that there is support in the spec for multiple policies makes me question whether the CSP decorator (@csp) is adequately clear and explicit? It could be append CSP or replace CSP.  I'd suggest clearing the existing CSP should be done through one and only one very clear path (`@csp_exempt`) to avoid user error in a security-critical component of the app (I would rename this: @csp_clear or @csp_purge for clarity).  

Does it also make sense to think about plugging into the `sites` contrib app to allow per site CSP configurations in the DB? (for now, this could be name-based and hardcoded in settings, but eventually dynamic db-based CSPs could be supported...)

And finally a thought on backwards compatibility for users:

You can leave the current settings, just load them into the policy dict under the 'default' key.   If the 'default' key is also explicitly defined, throw an error. Set `CSP_POLICIES` to `['default']` by default ;)
  
Oh and this issue is **super** old. Would you like me to take this on?
@DylanYoung if you have the time for it, please go for it.

Regarding what you said...

> (I would rename this: @csp_clear or @csp_purge for clarity).

Personally, I don't think that's necessary. `@...except` follows various Django APIs and should be explicit enough imho.

I also don't think we need support for `django.contrib.sites` - I'd leave that to specific projects to handle dynamically. We shouldn't solve all problems in this library, that'd be out of scope.

Yeah, I like your idea of backwards compatibility. We could first introduce the new policy dictionary but also allow loading of it, if it's specified. That way we can safely deprecate old settings and transition to the new settings format.

Looking forward to reviewing your pull request!
any update on this? Would be nice to have this in django core, which waits on resolve of this ticket. thanks.
@hiren1771 Thanks for the ping.  My time got swallowed by life recently, but I'll see if I can get to this soon.  
Got a little start on it.  Not quite functional yet. 

Couple notes:

NEW settings:

CSP_POLICY_DEFINITIONS (dict)
CSP_UPDATE_TEMPLATE (str)
CSP_POLICIES (list, tuple)

The template is used for appending policies.  It will be used as the "base" of the new policy.  Can be set to `None` to use the provided policy "as-is".

I opted to make `csp_update` also allow appending.  Check out the new definition (9fe25d9) and let me know if it makes sense to y'all. 

Legacy settings are still supported and there's a toggle in the code to change their precedence after an initial deprecation period.
thanks, is this ticket still the right approach to use, considering this comment from the django ticket:
https://code.djangoproject.com/ticket/15727#comment:22
@hiren1771 Should be fine I think.  Once it's coded, they can fold it into core. I'll keep that in mind while I'm refactoring though. 
Ah I see, this module is also a middleware, and so is SecurityMiddleware. If there is anything I can help with, let me know. I have never worked on django code base but can do general python. Thanks again for picking this ticket up.
@hiren1771 Sweet.  I think I'll have a working version tomorrow (maybe you could give it a test and see if it all works and makes sense to you?), but there will be few final decisions to be made.

Pinging @jsocol @graingert @mozfreddyb to try to get some of those discussions rolling.  In particular,  there are a couple of "per policy" settings that aren't directives, report-only being one.  Do we want to differentiate these somehow from the actual directives?

As far as back compatibility goes,  I think the easiest thing is to continue supporting mixed case,  underscores,  and hyphens, then just normalise in the backend.   Does that make sense to everyone? (This way we can keep using kwargs too).
Also, I think I need a bit of clarification on the spec. My reading is that multiple comma delineated values are permitted, but only one of each of the two headers.  Is that correct?

If not, then I think we have to impose that restriction anyways as I don't think Django's response supports multiple headers with the same key. 
All tests are passing now.  Haven't written tests for the multi-policy case yet so there might still be some bugs lurking in there.  Still have some cleanup to do, but we're close :) 
A few notes before I forget and for reference when I go to do the docs:

`csp_update` can currently append, but `csp_replace` can not (it's trivial to make it so though if desired)
They both take an optional list of names which defaults to `['default']` and any single-policy `kwargs` will be applied to policies with that key (this preserves back-compat and allows bulk updates).
They also take a `csp_definitions` object (which currently does not support the "single-policy" mode) 

The `csp` decorator works a little differently.  Its first argument is an iterable of policies, which can be either a two-tuple `(name, csp)`,  a csp dictionary (random name will be applied), or a name which will be looked up both in the currently applied policy definitions and in the base config.  `kwargs` can be passed in for back-compat as well; they will also be assigned a random name.

There is also a new `csp_order` (open to renaming) decorator that reorders existing policies based on index or name and can also trim off extra policies (though always has at least one: use `csp_exempt` to remove a policy).  Maybe `csp_select` would be a better name?  It works with an iterable of mixed names and indexes.
FYI: I opted to make the "pseudo directives" identical to the old settings but lowercase.  I think it's useful to be able to visually disambiguate them via the '-' versus '_' distinction.

(In fact I could even use it in the code instead the lookup dictionary... might be cleaner.    Yeah; I'll clean that up.  I think there's even some old mechanics in there for when I was going to change the names that will be unnecessary now.
Identify pseudo settings: `bool(kwarg.lower() in DIRECTIVES)`
Get all pseudo settings: `d for d in DIRECTIVES if '_' in d`

Yes yes, that will be much nicer :D
)
> Also, I think I need a bit of clarification on the spec. My reading is that multiple comma delineated values are permitted, but only one of each of the two headers. Is that correct?

I don't think I understand your question. Can you rephrase, give an example or a link to the spec text you're talking about? :)
The headers are concatenated like any other multi value header

On Mon, 9 Mar 2020, 08:04 Frederik Braun, <notifications@github.com> wrote:

> Also, I think I need a bit of clarification on the spec. My reading is
> that multiple comma delineated values are permitted, but only one of each
> of the two headers. Is that correct?
>
> I don't think I understand your question. Can you rephrase, give an
> example or a link to the spec text you're talking about? :)
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/mozilla/django-csp/issues/36?email_source=notifications&email_token=AADFATBKG4IKFYHVDTFJV6TRGSPKRA5CNFSM4AJ6MI52YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEOGBVOI#issuecomment-596384441>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AADFATF7VWKO2Y6PRT4WKFDRGSPKRANCNFSM4AJ6MI5Q>
> .
>

@graingert That's what I thought.  Thanks for the confirmation.  Note that there are headers that are permitted multiple times: https://stackoverflow.com/questions/4371328/are-duplicate-http-response-headers-acceptable

Mostly I just wanted to confirm that preserving the order *between* multiple report-only and regular policies wasn't desired / allowed by the spec.  I don't think Django supports it anyways (after a quick look at the response implementation),  so the point is kind of moot anyways :D
Any thoughts on the optional space between commas? We can remove it to save a few bytes on the wire. Or keep it in for legibility. Or toggle it based on the DEBUG setting. 
> Any thoughts on the optional space between commas? We can remove it to save a few bytes on the wire. Or keep it in for legibility. Or toggle it based on the DEBUG setting.

Does it matter, are the headers not compressed anyway?
@hiren1771 Good point.  I'm not 100% on that,  but they probably are.  I'm just going to leave that as is for now.
Hi, I'm just curious what the status of this work is? Is there stuff that needs more hands? 
> Hi, I'm just curious what the status of this work is? Is there stuff that needs more hands?

Good question. It looks like Casey is pretty far along.

@DylanYoung thanks for working on this! Is #179 your most recent work or do you have any unpushed changes? Was there anything else you were planning to add to the branch?
Sorry all!  I almost got this done and then had some life stuff that took
me away from it.  I'll review my current work tomorrow and let you know.  I
believe there were just a few last questions to be answered about how we
want the API to function and then it should be pretty well there!

Best,

Casey

On Fri, 30 Jul 2021 at 11:30, Greg Guthe ***@***.***> wrote:

> Hi, I'm just curious what the status of this work is? Is there stuff that
> needs more hands?
>
> Good question. It looks like Dylan is pretty far along.
>
> @DylanYoung <https://github.com/DylanYoung> thanks for working on this!
> Is #179 <https://github.com/mozilla/django-csp/pull/179> your most recent
> work or do you have any unpushed changes? Was there anything else you were
> planning to add to the branch?
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/mozilla/django-csp/issues/36#issuecomment-889932297>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABMG3FDP45565UKLLNBVBXLT2KZRZANCNFSM4AJ6MI5Q>
> .
>

 @DylanYoung did you get a chance to review your work? do you have any unpushed changes? Thanks!
I took a look at my work and added some notes to the PR for when I get back to it.  If anyone has any thoughts on my comments so far, feel free to reply.  I think the next step is for me to add some more detailed unit tests and that should clarify how things are working now so we/you can decide if any major design changes are needed (in particular, I got a little bogged down in the decorators and maybe made them a bit overly flexible).  

> CSP technically does allow [multiple policies](https://dvcs.w3.org/hg/content-security-policy/raw-file/tip/csp-specification.dev.html#content-security-policy-header-field). Not sure exactly why you'd want to, but allowing `n>=1` policy definitions is more in line with that part of the spec.

As for this comment, I'm not sure Django supports this?  It's still nice to have multiple policies defined in a central spot though, as then the decorators can just reference them by name. 

EDIT: I'm silly.  This is already supported in a backhanded way.  It's handled by merging the policies together. 
I haven't yet checked whether I have any further work on my local (it's on my other computer).  At most I think I started on some doc updates.  I'll check shortly. 
I've rebased this on the latest master, worked out a bunch of kinks, and written a handful of tests.  Still needs doc updates, branch cleanup/squash down, and few last design decisions.  I've simplified things significantly and instead of complicating the behaviour of the existing decorators, I've added two new ones: `csp_select` and `csp_append` which selects policies from those that are available (essentially changing CSP_POLICIES on a per-view basis) and appends a new policy to the currently selected policies, respectively.

Only major functional change remaining is to fold in `CSP_EXCLUDE_URL_PREFIXES` and then doc updates and I think it should be ready to go.




`CSP_EXCLUDE_URL_PREFIXES` is folded in now. 

Needs:
- [x] Finish up docs updates

Nice-to-have:
- [ ] More utils tests and possibly other tests

Decisions:

- [ ] test dependency updates: do we want them done here?
- [ ] Deprecate block-all-mixed-content directive: do we want that done here?
- [ ] removal of some outdated compatibility code: again, do we want that done here?
- [ ] FIXME: should `csp_update` allow updating report_only?  It's an easy fix I think (`csp_replace` does).  
- [x] `        allow_legacy=True  # not hasattr(settings, 'CSP_POLICY_DEFINITIONS'),`
   - We'll need to update a bunch of tests if we want to toggle this.  It disallows mixing the legacy settings with the new settings. 

@DylanYoung  - I did not test any of this code and it may contain inaccuracies/errors but this hopefully this helps you get ideas and fulfills your checklist.

**Background on report-only:**

Content Security Policy (CSP) report-only mode is a feature of content security policies that allows you to test a new policy without actually enforcing it. When you enable CSP report-only mode, the browser will not block any content that violates the policy. Rather, it will send reports summarizing policy violations to a specified report-uri.

In other words, CSP report-only mode provides a way to monitor and debug your content security policy before putting it into full enforcement mode. It enables the web administrators to evaluate the potential impact and effectiveness of their CSP configuration by identifying which resources would be blocked if the policy were in enforcement mode.

The report-uri is the URL to which violation reports should be sent; it is specified as a directive within the CSP header. Developers can use these reports to fine-tune their policy to avoid breaking existing code while still maintaining a secure content delivery. Reporting helps web admins to discover and address any CSP issues with application functionality or 3rd party domains used on the website.

**Possible implementation of the updated csp_update view to allow updating report_only:**

```
from django.shortcuts import render, redirect
from django.views.decorators.http import require_POST
from csp.models import CSPReport
from django.utils import timezone
from datetime import timedelta


@require_POST
def csp_update(request):
    """View for updating the CSP policy and report-only mode"""

    # Check if the request contains report-only field and its value is True
    report_only = request.POST.get('report_only') == 'True'

    # Check if the request contains a CSP policy and its value is valid JSON
    csp_policy = request.POST.get('policy')
    if csp_policy:
        try:
            json.loads(csp_policy)
        except ValueError:
            return render(request, 'csp/update.html', {
                'error': 'Invalid CSP policy JSON'
            })

        # save the new policy with the report-only settings
        csp_report = CSPReport.objects.create(
            policy=csp_policy,
            report_only=report_only,
            created_at=timezone.now()
        )

        # delete old reports if they are too old
        too_old = timezone.now() - timedelta(days=30)
        CSPReport.objects.filter(created_at__lt=too_old).delete()

    # redirect back to update page with success message
    return redirect('csp_update_view', success=True)

```

This implementation checks whether the request contains a report_only field and if it is set to True. If it is, then the middleware is enabled in report-only mode. It also validates the provided CSP policy JSON and saves it along with the report-only settings. Additionally, it deletes old reports that are older than 30 days to prevent the database from growing too large over time.

Finally, the view redirects back to the update page with a success message indicating that the policy has been successfully updated.


***

**Random Unit tests**

```
from django.test import TestCase
from csp.utils import build_policy


class UtilsTestCase(TestCase):
    def test_build_policy_empty_dict(self):
        """Test build_policy with an empty dict"""
        policy = build_policy({})
        self.assertEqual(policy, "")

    def test_build_policy_default_src(self):
        """Test build_policy with a default-src directive"""
        policy = build_policy({"default-src": ["https://example.com/%22]%7D)
        self.assertEqual(policy, "default-src https://example.com/;%22)

    def test_build_policy_multiple_directives(self):
        """Test build_policy with multiple directives"""
        policy = build_policy({
            "default-src": ["https://example.com/"],
            "script-src": ["https://scripts.example.com/"],
            "frame-src": ["https://frames.example.com/"],
        })
        expected_policy = "default-src https://example.com/; " \
            "script-src https://scripts.example.com/; " \
            "frame-src https://frames.example.com/;"
        self.assertEqual(policy, expected_policy)

    def test_build_policy_unsupported_directive_key(self):
       """Test build_policy with an unsupported directive key"""
        with self.assertRaises(ValueError):
            build_policy({"unsupported-src": ["https://example.com/%22]%7D)

    def test_build_policy_unsupported_directive_value(self):
        """Test build_policy with an unsupported directive value"""
        with self.assertRaises(ValueError):
            build_policy({"default-src": ["example.com/"]})

    def test_build_policy_invalid_directive_value(self):
        """Test build_policy with an invalid directive value type"""
        with self.assertRaises(TypeError):
            build_policy({"default-src": "https://example.com/%22%7D)
```



***


**Util tests for each function in utils.py**

```
def test_normalize_config(self):
    """ 
    Test _normalize_config function
    """
    # Test with empty config
    config = {}
    expected_output = {}
    output = _normalize_config(config)
    self.assertEqual(output, expected_output)

    # Test with incomplete config
    config = {"default-src": ["'self'"], "frame-src": ["'none'"]}
    expected_output = {
        "default-src": ["'self'"],
        "frame-src": ["'none'"],
        "script-src": ["'self'"],
        "style-src": ["'self'"],
        "img-src": ["'self'"],
        "media-src": ["'self'"],
        "font-src": ["'self'"],
        "connect-src": ["'self'"],
        "object-src": ["'none'"],
        "sandbox": [],
        "report-uri": [],
    }
    output = _normalize_config(config)
    self.assertEqual(output, expected_output)

    # Test with complete config
    config = {
        "default-src": ["'self'"],
        "frame-src": ["'none'"],
        "script-src": ["'self'", "https://example.com"],
        "style-src": ["'self'"],
        "img-src": ["'self'"],
        "media-src": ["'self'"],
        "font-src": ["'self'"],
        "connect-src": ["'self'"],
        "object-src": ["'none'"],
        "sandbox": [],
        "report-uri": ["https://example.com/report"],
    }
    expected_output = config
    output = _normalize_config(config)
    self.assertEqual(output, expected_output)
```





```
from django.test import TestCase
from csp.utils import build_policy

class BuildPolicyTestCase(TestCase):
    def test_build_policy(self):
        policy = {
            'default-src': ["'self'"],
            'script-src': ["'self'", 'ajax.googleapis.com'],
            'style-src': ["'self'"],
            'font-src': ["'self'", 'ajax.googleapis.com', 'fonts.gstatic.com'],
            'img-src': ["'self'", 'ajax.googleapis.com'],
            'connect-src': ["'self'", 'ajax.googleapis.com'],
            'media-src': ["'self'"],
            'object-src': ["'none'"],
            'frame-src': ["'self'"],
            'sandbox': ['allow-same-origin'],
            'report-uri': ['/csp-violation-report/'],
        }

        result = build_policy(policy)

        expected = (
            "default-src 'self'; "
            "script-src 'self' ajax.googleapis.com; "
            "style-src 'self'; "
            "font-src 'self' ajax.googleapis.com fonts.gstatic.com; "
            "img -src 'self' ajax.googleapis.com; "
            "connect-src 'self' ajax.googleapis.com; "
            "media-src 'self'; "
            "object-src 'none'; "
            "frame-src 'self'; "
            "sandbox allow-same-origin; "
            "report-uri /csp-violation-report/;"
        )

        self.assertEqual(result, expected)

```




```
from django.test import TestCase
from csp.utils import _update_policy

class UpdatePolicyTestCase(TestCase):

    def setUp(self):
        self.policy = {
            'default-src': ["'self'"],
            'script-src': ["'none'"],
            'object-src': ["'self'"],
        }

    def test_empty_update(self):
        expected = self.policy.copy()
        result = _update_policy(self.policy, {})
        self.assertDictEqual(result, expected)

    def test_one_source_update(self):
        update = {'script-src': ["'self'"]}
        expected = {
            'default-src': ["'self'"],
            'script-src': ["'self'"],
            'object-src': ["'self'"],
        }
        result = _update_policy(self.policy, update)
        self.assertDictEqual(result, expected)

    def test_multiple_sources_update(self):
        update = {
            'default-src': ["'none'"],
            'frame-src': ["'self'", 'https://example.com/'],
        }
        expected = {
            'default-src': ["'none'"],
            'script-src': ["'none'"],
            'object-src': ["'self'"],
            'frame-src': ["'self'", 'https://example.com/'],
        }
        result = _update_policy(self.policy, update)
        self.assertDictEqual(result, expected)
```




```
from django.test import TestCase
from csp.utils import _replace_policy

class ReplacePolicyTestCase(TestCase):
    def test_replace_policy(self):
        old_policy = "default-src 'none';"
        new_policy = "default-src 'self'; script-src 'self';"
        result = _replace_policy(old_policy, new_policy)
        expected = "default-src 'self'; script-src 'self';"
        self.assertEqual(result, expected)

        old_policy = "default-src 'self'; script-src 'self';"
        new_policy = "connect-src 'self'; img-src 'self';"
        result = _replace_policy(old_policy, new_policy)
        expected = "connect-src 'self'; img-src 'self';"
        self.assertEqual(result, expected)
```




```
from django.test import TestCase
from csp.utils import _compile_policy

class CompilePolicyTestCase(TestCase):

    def test_compile_policy(self):
        
        policy = {
            'default-src': [],
            'script-src': ['self', 'unsafe-hashes'],
            'style-src': ['self'],
        }
        
        expected_result = "default-src 'none'; script-src 'self' 'unsafe-hashes'; style-src 'self'"
        
        result = _compile_policy(policy)
        
        self.assertEqual(expected_result, result)

```




```
from django.test import TestCase
from csp.utils import kwarg_to_directive

class KwargsToDirectiveTestCase(TestCase):
    def test_kwarg_to_directive(self):
        kwarg_dict = {'default_src': ("'self'", 'example.com', '*.example.com'),
                      'script_src': ("'self'", 'example.org', '*.example.org')}

        expected_directive_str = "default-src 'self' example.com *.example.com; script-src 'self' example.org *.example.org"
        actual_directive_str = kwarg_to_directive(kwarg_dict)

        self.assertEqual(actual_directive_str, expected_directive_str)
```





```
from django.test import TestCase
from csp.utils import PolicyNames


class PolicyNamesTest(TestCase):
    def test_policynames_default(self):
        pn = PolicyNames()

        self.assertEqual(pn.script_src, "'self'")
        self.assertEqual(pn.style_src, "'self'")
        self.assertEqual(pn.img_src, "'self'")
        self.assertEqual(pn.connect_src, "'self'")
        self.assertEqual(pn.font_src, "'self'")
        self.assertEqual(pn.base_uri, "'self'")
        self.assertEqual(pn.frame_src, "'self'")
        self.assertEqual(pn.media_src, "'self'")
        self.assertEqual(pn.object_src, "'self'")
        self.assertEqual(pn.plugin_types, "")
        self.assertEqual(pn.report_uri, "/csp-report/")
        self.assertEqual(pn.visible_annotations, "")
        self.assertEqual(pn.use_script_nonce, False)
        self.assertEqual(pn.use_style_nonce, False)
        self.assertEqual(pn.nonce_random, False)

    def test_policynames_custom(self):
        pn = PolicyNames({
            "img_src": "https://example.com/",
            "style_src": "'self' https://cdn.example.com/'",
            "connect_src": "'self' ws://example.com'",
            "font_src": "'self' https://fonts.example.com/'",
            "frame_src": "'self' https://iframe.example.com/'",
            "media_src": "'self' https://media.example.com/'",
            "object-src": "'none'",
            "report_uri": "/my-csp-report"
        })

        self.assertEqual(pn.script_src, "'self'")
        self.assertEqual(pn.style_src, "'self' https://cdn.example.com/%27%22)
        self.assertEqual(pn.img_src, "https://example.com/%22)
        self.assertEqual(pn.connect_src, "'self' ws://example.com'")
        self.assertEqual(pn.font_src, "'self' https://fonts.example.com/%27%22)
        self.assertEqual(pn.base_uri, "'self'")
        self.assertEqual(pn.frame_src, "'self' https://iframe.example.com/%27%22)
        self.assertEqual(pn.media_src, "'self' https://media.example.com/%27%22)
        self.assertEqual(pn.object_src, "'none'")
        self.assertEqual(pn.plugin_types, "")
        self.assertEqual(pn.report_uri, "/my-csp-report")
        self.assertEqual(pn.visible_annotations, "")
```




```
from csp.utils import _clean_input_policy
import unittest

class TestCleanInputPolicy(unittest.TestCase):
    
    def test_clean_input_policy_1(self):
        """Test empty string input"""
        output = _clean_input_policy("")
        self.assertEqual(output, "")
        
    def test_clean_input_policy_2(self):
        """Test input with only spaces"""
        output = _clean_input_policy("     ")
        self.assertEqual(output, "")
        
    def test_clean_input_policy_3(self):
        """Test input with valid policy"""
        input_policy = "default-src 'self'; script-src 'self' https://example.com"
        output = _clean_input_policy(input_policy)
        self.assertEqual(output, input_policy)
        
    def test_clean_input_policy_4(self):
        """Test input with invalid policy"""
        input_policy = "default-src 'self'; invalid-policy; script-src 'self' https://example.com"
        output = _clean_input_policy(input_policy)
        expected_output = "default-src 'self'; script-src 'self' https://example.com"
        self.assertEqual(output, expected_output)
```






```
import unittest
from csp.utils import _kwargs_to_directives

class TestKwargsToDirectives(TestCase):
    def test_kwargs_to_directives_success(self):
        input_kwargs = {'height': 100, 'width': 200, 'color': 'red'}
        expected_output = ['height=100', 'width=200', 'color=red']
        self.assertEqual(_kwargs_to_directives(input_kwargs), expected_output)

    def test_kwargs_to_directives_empty_kwargs(self):
        input_kwargs = {}
        expected_output = []
        self.assertEqual(_kwargs_to_directives(input_kwargs), expected_output)

    def test_kwargs_to_directives_non_string_value(self):
        input_kwargs = {'height': 100, 'width': 200, 'color': True}
        expected_output = ['height=100', 'width=200', 'color=True']
        self.assertEqual(_kwargs_to_directives(input_kwargs), expected_output)

    def test_kwargs_to_directives_null_value(self):
        input_kwargs = {'height': 100, 'width': None, 'color': 'red'}
        expected_output = ['height=100', 'width=None', 'color=red']
        self.assertEqual(_kwargs_to_directives(input_kwargs), expected_output)

    def test_kwargs_to_directives_invalid_key(self):
        input_kwargs = {'height': 100, 'width': 200, 123: 'red'}
        with self.assertRaises(TypeError):
            _kwargs_to_directives(input_kwargs)

    def test_kwargs_to_directives_invalid_value(self):
        input_kwargs = {'height': 100, 'width': '200', 'color': None}
        expected_output = ['height=100', 'width=200', 'color=None']
        self.assertEqual(_kwargs_to_directives(input_kwargs), expected_output)

```


```
from django.test import TestCase
from csp.utils import _policies_from_names_and_kwargs

class TestUtils(TestCase):
    def test_policies_from_names_and_kwargs(self):
        names = ['default-src', 'script-src']
        kwargs = {'default-src': ["'self'"], 'script-src': ["'self'", 'example.com']}
        policies = _policies_from_names_and_kwargs(names, kwargs)
        expected_policies = {
            'default-src': ["'self'"],
            'script-src': ["'self'", 'example.com']
        }
        self.assertDictEqual(policies, expected_policies)
```





```
from django.test import TestCase
from csp.utils import _policies_from_args_and_kwargs

class TestUtils(TestCase):
    def test_policies_from_args_and_kwargs(self):
        args = ('default-src', "'self'")
        kwargs = {'script-src': ["'self'", 'example.com']}
        policies = _policies_from_args_and_kwargs(args, kwargs)
        expected_policies = {
            'default-src': ["'self'"],
            'script-src': ["'self'", 'example.com']
        }
        self.assertDictEqual(policies, expected_policies)

```




```
from django.test import TestCase
from csp.utils import _default_attr_mapper

class TestUtils(TestCase):
    def test_default_attr_mapper(self):
        obj = {'foo': 'bar', 'baz': 'qux'}
        attrs = ['foo', 'baz']
        result = _default_attr_mapper(obj, attrs)
        expected_result = {'foo': 'bar', 'baz': 'qux'}
        self.assertDictEqual(result, expected_result)
```




```
from django.test import TestCase
from csp.utils import _bool_attr_mapper

class TestUtils(TestCase):
    def test_bool_attr_mapper(self):
        obj = {'foo': True, 'bar': False}
        attrs = ['foo', 'bar']
        result = _bool_attr_mapper(obj, attrs)
        expected_result = {'foo': 'true', 'bar': 'false'}
        self.assertDictEqual(result, expected_result)
```



```
import asyncio
from django.test import TestCase
from csp.utils import _async_attr_mapper

class TestUtils(TestCase):
    async def test_async_attr_mapper(self):
        async def async_func():
            await asyncio.sleep(1)
            return 'async_value'

        obj = {'foo': async_func()}
        attrs = ['foo']
        result = await _async_attr_mapper(obj, attrs)
        expected_result = {'foo': 'async_value'}
        self.assertDictEqual(result, expected_result)

```


```
from django.test import TestCase
from csp.utils import _unwrap_script

class TestUtils(TestCase):
    def test_unwrap_script(self):
        input_html = '<script>console.log("Hello, world!")</script>'
        expected_output = 'console.log("Hello, world!")'
        result = _unwrap_script(input_html)
        self.assertEqual(result, expected_output)
```



```
from django.test import TestCase
from csp.utils import build_script_tag

class TestUtils(TestCase):
    def test_build_script_tag(self):
        input_src = 'https://example.com/script.js'
        expected_output = '<script src="https://example.com/script.js" nonce="abc123"></script>'
        result = build_script_tag(input_src, 'abc123')
        self.assertEqual(result, expected_output)
```






```
import os
import re
import unittest
import requests

class TestDependencyUpdates(unittest.TestCase):

    def test_dependency_updates(self):
        repo_path = "https://api.github.com/repos/DylanYoung/django-csp/branches/GH-36"
        response = requests.get(repo_path).json()
        dependencies = response.get("dependencies", {})

        for dep in dependencies:
            current_version = dependencies.get(dep).get("version")
            latest_version = self.get_latest_version(dep)

            message = f"{dep} dependency not updated to latest version"
            self.assertEqual(current_version, latest_version, message)

    def get_latest_version(self, dependency):
        package_name = re.sub(r'[^\w\s]','', dependency.lower())
        package_path = f"https://pypi.org/pypi/{package_name}/json"
        response = requests.get(package_path).json()

        version_numbers = []
        for version_number in response.get("releases", {}).keys():
            version_numbers.append(version_number)
        version_numbers.sort(reverse=True)

        return version_numbers[0]
```


***

**block-all-mixed-content**

The block-all-mixed-content directive code in the django-csp library can be found in the csp.middleware.CSPMiddleware class in the middleware.py file. 

Here's an example of how to set the block-all-mixed-content directive in the CSP header using the CSPMiddleware class:

```
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import cached_property
from csp.utils import build_policy_string

class CSPMiddleware(MiddlewareMixin):
    @cached_property
    def get_response(self, request):
        csp_header = build_policy_string({
            'default-src': ["'self'"],
            'block-all-mixed-content': True,
        })
        response['Content-Security-Policy'] = csp_header
```



In this example, the block-all-mixed-content directive is included in the CSP header policy string as a boolean value. This directive instructs the browser to block all HTTP content that is loaded over HTTPS.







```
class CSPMiddleware(MiddlewareMixin):
    """
    Middleware to add Content-Security-Policy headers to responses.

    See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
    """

    def init(self, get_response=None, kwargs):
        self.get_response = get_response
        self.config = kwargs.get('config', {})
        self.config['report_uri'] = kwargs.get('report_uri', self.config.get('report_uri', None))
        self.config['browser_sniff'] = kwargs.get('browser_sniff', False)
        self.config.update(DEFAULT_SRC=None, SCRIPT_SRC=None, OBJECT_SRC=None, STYLE_SRC=None, IMG_SRC=None, MEDIA_SRC=None,
                            FRAME_SRC=None, FONT_SRC=None, CONNECT_SRC=None, BASE_URI=None, FORM_ACTION=None,
                            REPORT_ONLY=None, BLOCK_ALL_MIXED_CONTENT=None, UPGRADE_INSECURE_REQUESTS=None, SANDBOX=None)


 if 'block_all_mixed_content' in self.config:
            warnings.warn(
                'The "block_all_mixed_content" directive has been deprecated by the latest Content Security Policy '
                'standard. Please remove this directive from your Content Security Policy. To block all mixed content in modern browsers, use the "block-all" directive instead',
                DeprecationWarning)
            self.config.pop('block_all_mixed_content', None)

    def call(self, request):
        response = self.get_response(request)
        if not response.has_header('Content-Security-Policy'):
            csp_header = self.build_header(request)
            response['Content-Security-Policy'] = csp_header
        return response

    def build_header(self, request):
        """
        Build a Content Security Policy header value.
        """
        builder = CSPBuilder(self.config, request)
        return builder.build()

```


It is generally a good idea to follow current best practices when it comes to web security. The block-all-mixed-content directive is now deprecated because it can sometimes lead to compatibility issues with certain websites that incorporate both http and https resources. 

In its place, the block-all directive should be used to block all mixed content by default. This directive will ensure that browsers block insecure content automatically while allowing secure content to load normally. 

Therefore, deprecating the block-all-mixed-content directive and replacing it with the block-all directive is a good security measure and aligns with the latest security standards.







***

**Outdated Code**

To locate outdated compatibility code in the django-csp library, you can search for code that references deprecated or removed features, such as django.conf.urls.defaults, django.utils.importlib, or django.core.urlresolvers. You can also look for code that uses older syntax or patterns that have been replaced with newer, more efficient alternatives. 


It is generally a good idea to remove outdated code in any software project, including the django-csp package. Outdated code often refers to code that is no longer maintained, has been replaced with better alternatives or is no longer compatible with newer versions of the software platform or libraries. This code can cause problems like security vulnerabilities, performance issues, and functionality problems. Additionally, removing outdated code can improve the maintainability of the codebase, make it easier to introduce new features, and make it more understandable for developers who are new to the project. However, careful consideration and testing should be taken before removing any code to ensure that it will not break existing functionality or cause other issues.

***



**Doing something like the original comments of this thread**


```
from csp.middleware import CSPMiddleware
from csp.utils import build_policy

class MyCSPMiddleware(CSPMiddleware):

    def __init__(self, get_response=None):
        self.policies = []
        # CSP_POLICIES is a list of dictionaries
        for policy in settings.CSP_POLICIES:
            self.add_policy(policy)
        super().__init__(get_response)

    def add_policy(self, policy):
        """Add a policy to be enforced"""
        report_only = policy.pop('REPORT_ONLY', False)
        csp_policy = build_policy(policy)
        self.policies.append((report_only, csp_policy))

    def process_response(self, request, response):
        """Add CSP headers to the response"""
        for report_only, policy in self.policies:
            response._csp_policy = policy
            response.report_only = report_only
        return super().process_response(request, response)

```

With this implementation, the MyCSPMiddleware class extends the CSPMiddleware class provided by django-csp. In the constructor, the CSP_POLICIES list defined in settings.py is processed and each policy is added to a list of policies to be enforced. The add_policy() method appends policies to the policy list by creating a new policy object using the csp.utils.build_policy() method.

In the process_response() method, the policies are added to the response object by setting the _csp_policy attribute to the policy object, and the report_only attribute to the value of the REPORT_ONLY parameter in the policy dictionary. Finally, the process_response() method of the parent class is called to complete the response processing.

This implementation allows for multiple CSP policies to be defined in the CSP_POLICIES settings in settings.py, and these policies will be added to the response headers for every request processed by the middleware.


```
CSP_POLICIES = [
    {
        'default-src': ['self'],
        'img-src': ['*', 'data:'],
        'script-src': ['self', 'ajax.googleapis.com'],
        'style-src': ['self']
    },
    {
        'default-src': ['none'],
        'connect-src': ['self'],
        'img-src': ['self', 'data:'],
        'script-src': ['self'],
        'style-src': ['self']
    }
]

MIDDLEWARE = [
    # ...
    'path.to.MyCSPMiddleware',
    # ...
]
```


In this example, CSP_POLICIES is a list of two policy dictionaries, each defining a different policy. Each policy dictionary contains directives and sources that specify the allowed sources for various types of content. In this example, the first policy allows content from the local domain, Google's AJAX API, and data URIs for images. The second policy allows content only from the local domain and does not allow any content by default.

The `MyCSPMiddleware middleware is added to the MIDDLEWARE setting to process the policies defined in CSP_POLICIES and add them to the response headers.

With this configuration, any responses generated by Django will contain the CSP header specifying the policies defined in CSP_POLICIES. Any violations against these policies will be logged as specified in the policy. This helps to reduce the risk of cross-site scripting (XSS) attacks and safeguarding the application from other potential security exploits.
Hey @fallen,

>  some browsers like Chromium do not respect the style-src: 'unsafe-inline' if a nonce is given

This is interesting.

> Possible to override CSP_INCLUDE_NONCE_IN per-view with a decorator?

No this isn't possible, since `CSP_INCLUDE_NONCE_IN` is pulled from settings and not the header config that the other decorators operate on.

If it's OK to remove the nonce from all the directives, you could use a decorator like:

```python
def csp_remove_nonces(f):
    @wraps(f)
    def _wrapped(*a, **kw):
        r = f(*a, **kw)
        r._csp_nonce = None
        return r
    return _wrapped
```

then since build_policy in utils strips the nonce sources when nonce is `None`. The full patch might be:

```diff
commit 4e29019150c71a2d9938b610afcae5ec88a9046e
Author: Greg Guthe <gguthe@mozilla.com>
Date:   Mon Jan 6 01:40:05 2020 -0500

    add csp_remove_nonces decorator (needs docs)

diff --git a/csp/decorators.py b/csp/decorators.py
index bce3352..69e1fab 100644
--- a/csp/decorators.py
+++ b/csp/decorators.py
@@ -10,6 +10,15 @@ def csp_exempt(f):
     return _wrapped
 
 
+def csp_remove_nonces(f):
+    @wraps(f)
+    def _wrapped(*a, **kw):
+        r = f(*a, **kw)
+        r._csp_nonce = None
+        return r
+    return _wrapped
+
+
 def csp_update(**kwargs):
     update = dict((k.lower().replace('_', '-'), v) for k, v in kwargs.items())
 
diff --git a/csp/tests/test_decorators.py b/csp/tests/test_decorators.py
index 6f2716e..0d377d8 100644
--- a/csp/tests/test_decorators.py
+++ b/csp/tests/test_decorators.py
@@ -2,7 +2,7 @@ from django.http import HttpResponse
 from django.test import RequestFactory
 from django.test.utils import override_settings
 
-from csp.decorators import csp, csp_replace, csp_update, csp_exempt
+from csp.decorators import csp, csp_replace, csp_update, csp_exempt, csp_remove_nonces
 from csp.middleware import CSPMiddleware
 
 
@@ -98,6 +98,29 @@ def test_csp():
     assert policy_list == ["default-src 'self'"]
 
 
+@override_settings(CSP_INCLUDE_NONCE_IN=["default-src"])
+def test_csp_remove_nonces():
+    # get a new REQUEST so we can modify ._csp_nonce without affecting other
+    # tests
+    request = RequestFactory().get('/')
+    request._csp_nonce = 'test-nonce'
+
+    def view_without_decorator(request):
+        return HttpResponse()
+    response = view_without_decorator(request)
+    mw.process_response(request, response)
+    policy_list = sorted(response["Content-Security-Policy"].split("; "))
+    assert policy_list == ["default-src 'self' 'nonce-test-nonce'"]
+
+    @csp_remove_nonces
+    def view_with_decorator(request):
+        return HttpResponse()
+    response = view_with_decorator(request)
+    mw.process_response(request, response)
+    policy_list = sorted(response["Content-Security-Policy"].split("; "))
+    assert policy_list == ["default-src 'self' 'nonce-test-nonce'"]
+
+
 def test_csp_string_values():
     # Test backwards compatibility where values were strings
     @csp(IMG_SRC='foo.com', FONT_SRC='bar.com')
```
FYI: this will be fixed in https://github.com/mozilla/django-csp/issues/36
> FYI: this will be fixed in #36

Awesome, that's very good news!
Will this be documented?
How will it work then? Thanks :)
Haven't done up the docs yet,  but will hopefully get them done along with some more extensive unit tests this week.   In the single policy case,  it will work just as you requested in the OP.  In the multi-policy case, it can be overridden per policy, by passing 'include_nonce_in' in the policy dictionary ('report_only' works similarly).

:)

If all goes smoothly, I'm hoping we can get this merged and a release cut within a couple weeks.

Fix patch:
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..90da0c0
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,7 @@
+[run]
+source = csp
+omit =
+  csp/tests/*
+
+[report]
+show_missing = True
diff --git a/.gitignore b/.gitignore
index 90f56c0..63d548a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@
 .tox
 dist
 build
+docs/_build
diff --git a/CHANGES b/CHANGES
index 4a45f50..01eeb62 100644
--- a/CHANGES
+++ b/CHANGES
@@ -2,10 +2,17 @@
 CHANGES
 =======
 
-Unreleased
-==========
+4.x - Unreleased
+================
 
-- Add pyproject-fmt to pre-commit, and update pre-commit versions.
+BACKWARDS INCOMPATIBLE changes:
+- Move to dict-based configuration which allows for setting policies for both enforced and
+  report-only. See the migration guide in the docs for migrating your settings.
+
+Other changes:
+- Add pyproject-fmt to pre-commit, and update pre-commit versions
+- Fixes #36: Add support for enforced and report-only policies simultaneously
+- Drop support for Django <=3.2, end of extended support
 
 3.8
 ===
diff --git a/csp/apps.py b/csp/apps.py
new file mode 100644
index 0000000..caf9f45
--- /dev/null
+++ b/csp/apps.py
@@ -0,0 +1,11 @@
+from django.apps import AppConfig
+from django.core import checks
+
+from csp.checks import check_django_csp_lt_4_0
+
+
+class CspConfig(AppConfig):
+    name = "csp"
+
+    def ready(self):
+        checks.register(check_django_csp_lt_4_0, checks.Tags.security)
diff --git a/csp/checks.py b/csp/checks.py
new file mode 100644
index 0000000..23c76f0
--- /dev/null
+++ b/csp/checks.py
@@ -0,0 +1,81 @@
+import pprint
+
+from django.conf import settings
+from django.core.checks import Error
+
+
+OUTDATED_SETTINGS = [
+    "CSP_CHILD_SRC",
+    "CSP_CONNECT_SRC",
+    "CSP_DEFAULT_SRC",
+    "CSP_SCRIPT_SRC",
+    "CSP_SCRIPT_SRC_ATTR",
+    "CSP_SCRIPT_SRC_ELEM",
+    "CSP_OBJECT_SRC",
+    "CSP_STYLE_SRC",
+    "CSP_STYLE_SRC_ATTR",
+    "CSP_STYLE_SRC_ELEM",
+    "CSP_FONT_SRC",
+    "CSP_FRAME_SRC",
+    "CSP_IMG_SRC",
+    "CSP_MANIFEST_SRC",
+    "CSP_MEDIA_SRC",
+    "CSP_PREFETCH_SRC",
+    "CSP_WORKER_SRC",
+    "CSP_BASE_URI",
+    "CSP_PLUGIN_TYPES",
+    "CSP_SANDBOX",
+    "CSP_FORM_ACTION",
+    "CSP_FRAME_ANCESTORS",
+    "CSP_NAVIGATE_TO",
+    "CSP_REQUIRE_SRI_FOR",
+    "CSP_REQUIRE_TRUSTED_TYPES_FOR",
+    "CSP_TRUSTED_TYPES",
+    "CSP_UPGRADE_INSECURE_REQUESTS",
+    "CSP_BLOCK_ALL_MIXED_CONTENT",
+    "CSP_REPORT_URI",
+    "CSP_REPORT_TO",
+    "CSP_INCLUDE_NONCE_IN",
+]
+
+
+def migrate_settings():
+    # This function is used to migrate settings from the old format to the new format.
+    config = {
+        "DIRECTIVES": {},
+    }
+    REPORT_ONLY = False
+
+    if hasattr(settings, "CSP_REPORT_ONLY"):
+        REPORT_ONLY = settings.CSP_REPORT_ONLY
+
+    if hasattr(settings, "CSP_EXCLUDE_URL_PREFIXES"):
+        config["EXCLUDE_URL_PREFIXES"] = settings.CSP_EXCLUDE_URL_PREFIXES
+
+    if hasattr(settings, "CSP_REPORT_PERCENTAGE"):
+        config["REPORT_PERCENTAGE"] = round(settings.CSP_REPORT_PERCENTAGE * 100)
+
+    for setting in OUTDATED_SETTINGS:
+        if hasattr(settings, setting):
+            directive = setting[4:].replace("_", "-").lower()
+            value = getattr(settings, setting)
+            if value:
+                config["DIRECTIVES"][directive] = value
+
+    return config, REPORT_ONLY
+
+
+def check_django_csp_lt_4_0(app_configs, **kwargs):
+    check_settings = OUTDATED_SETTINGS + ["CSP_REPORT_ONLY", "CSP_EXCLUDE_URL_PREFIXES", "CSP_REPORT_PERCENTAGE"]
+    if any(hasattr(settings, setting) for setting in check_settings):
+        # Try to build the new config.
+        config, REPORT_ONLY = migrate_settings()
+        warning = (
+            "You are using django-csp < 4.0 settings. Please update your settings to use the new format.\n"
+            "See https://django-csp.readthedocs.io/en/latest/migration-guide.html for more information.\n\n"
+            "We have attempted to build the new CSP config for you based on your current settings:\n\n"
+            f"CONTENT_SECURITY_POLICY{'_REPORT_ONLY' if REPORT_ONLY else ''} = " + pprint.pformat(config, sort_dicts=True)
+        )
+        return [Error(warning, id="csp.E001")]
+
+    return []
diff --git a/csp/constants.py b/csp/constants.py
new file mode 100644
index 0000000..8cf10bc
--- /dev/null
+++ b/csp/constants.py
@@ -0,0 +1,2 @@
+HEADER = "Content-Security-Policy"
+HEADER_REPORT_ONLY = "Content-Security-Policy-Report-Only"
diff --git a/csp/contrib/rate_limiting.py b/csp/contrib/rate_limiting.py
index 2419c67..4cc4f90 100644
--- a/csp/contrib/rate_limiting.py
+++ b/csp/contrib/rate_limiting.py
@@ -16,9 +16,32 @@ def build_policy(self, request, response):
         replace = getattr(response, "_csp_replace", {})
         nonce = getattr(request, "_csp_nonce", None)
 
-        report_percentage = getattr(settings, "CSP_REPORT_PERCENTAGE")
-        include_report_uri = random.random() < report_percentage
+        policy = getattr(settings, "CONTENT_SECURITY_POLICY", None)
+
+        if policy is None:
+            return ""
+
+        report_percentage = policy.get("REPORT_PERCENTAGE", 100)
+        include_report_uri = random.randint(0, 100) < report_percentage
         if not include_report_uri:
             replace["report-uri"] = None
 
         return build_policy(config=config, update=update, replace=replace, nonce=nonce)
+
+    def build_policy_ro(self, request, response):
+        config = getattr(response, "_csp_config_ro", None)
+        update = getattr(response, "_csp_update_ro", None)
+        replace = getattr(response, "_csp_replace_ro", {})
+        nonce = getattr(request, "_csp_nonce", None)
+
+        policy = getattr(settings, "CONTENT_SECURITY_POLICY_REPORT_ONLY", None)
+
+        if policy is None:
+            return ""
+
+        report_percentage = policy.get("REPORT_PERCENTAGE", 100)
+        include_report_uri = random.randint(0, 100) < report_percentage
+        if not include_report_uri:
+            replace["report-uri"] = None
+
+        return build_policy(config=config, update=update, replace=replace, nonce=nonce, report_only=True)
diff --git a/csp/decorators.py b/csp/decorators.py
index 69d0f85..3d71b6c 100644
--- a/csp/decorators.py
+++ b/csp/decorators.py
@@ -1,55 +1,90 @@
 from functools import wraps
 
 
-def csp_exempt(f):
-    @wraps(f)
-    def _wrapped(*a, **kw):
-        r = f(*a, **kw)
-        r._csp_exempt = True
-        return r
+def csp_exempt(REPORT_ONLY=None):
+    if callable(REPORT_ONLY):
+        raise RuntimeError(
+            "Incompatible `csp_exempt` decorator usage. This decorator now requires arguments, "
+            "even if none are passed. Change bare decorator usage (@csp_exempt) to parameterized "
+            "decorator usage (@csp_exempt()). See the django-csp 4.0 migration guide for more "
+            "information."
+        )
 
-    return _wrapped
+    def decorator(f):
+        @wraps(f)
+        def _wrapped(*a, **kw):
+            resp = f(*a, **kw)
+            if REPORT_ONLY:
+                resp._csp_exempt_ro = True
+            else:
+                resp._csp_exempt = True
+            return resp
+
+        return _wrapped
 
+    return decorator
+
+
+# Error message for deprecated decorator arguments.
+DECORATOR_DEPRECATION_ERROR = (
+    "Incompatible `{fname}` decorator arguments. This decorator now takes a single dict argument. "
+    "See the django-csp 4.0 migration guide for more information."
+)
 
-def csp_update(**kwargs):
-    update = {k.lower().replace("_", "-"): v for k, v in kwargs.items()}
+
+def csp_update(config=None, REPORT_ONLY=False, **kwargs):
+    if config is None and kwargs:
+        raise RuntimeError(DECORATOR_DEPRECATION_ERROR.format(fname="csp_update"))
 
     def decorator(f):
         @wraps(f)
         def _wrapped(*a, **kw):
-            r = f(*a, **kw)
-            r._csp_update = update
-            return r
+            resp = f(*a, **kw)
+            if REPORT_ONLY:
+                resp._csp_update_ro = config
+            else:
+                resp._csp_update = config
+            return resp
 
         return _wrapped
 
     return decorator
 
 
-def csp_replace(**kwargs):
-    replace = {k.lower().replace("_", "-"): v for k, v in kwargs.items()}
+def csp_replace(config=None, REPORT_ONLY=False, **kwargs):
+    if config is None and kwargs:
+        raise RuntimeError(DECORATOR_DEPRECATION_ERROR.format(fname="csp_replace"))
 
     def decorator(f):
         @wraps(f)
         def _wrapped(*a, **kw):
-            r = f(*a, **kw)
-            r._csp_replace = replace
-            return r
+            resp = f(*a, **kw)
+            if REPORT_ONLY:
+                resp._csp_replace_ro = config
+            else:
+                resp._csp_replace = config
+            return resp
 
         return _wrapped
 
     return decorator
 
 
-def csp(**kwargs):
-    config = {k.lower().replace("_", "-"): [v] if isinstance(v, str) else v for k, v in kwargs.items()}
+def csp(config=None, REPORT_ONLY=False, **kwargs):
+    if config is None and kwargs:
+        raise RuntimeError(DECORATOR_DEPRECATION_ERROR.format(fname="csp"))
+
+    config = {k: [v] if isinstance(v, str) else v for k, v in config.items()}
 
     def decorator(f):
         @wraps(f)
         def _wrapped(*a, **kw):
-            r = f(*a, **kw)
-            r._csp_config = config
-            return r
+            resp = f(*a, **kw)
+            if REPORT_ONLY:
+                resp._csp_config_ro = config
+            else:
+                resp._csp_config = config
+            return resp
 
         return _wrapped
 
diff --git a/csp/middleware.py b/csp/middleware.py
index 35465d9..11bf799 100644
--- a/csp/middleware.py
+++ b/csp/middleware.py
@@ -7,6 +7,7 @@
 from django.utils.deprecation import MiddlewareMixin
 from django.utils.functional import SimpleLazyObject
 
+from csp.constants import HEADER, HEADER_REPORT_ONLY
 from csp.utils import build_policy
 
 
@@ -21,8 +22,7 @@ class CSPMiddleware(MiddlewareMixin):
     """
 
     def _make_nonce(self, request):
-        # Ensure that any subsequent calls to request.csp_nonce return the
-        # same value
+        # Ensure that any subsequent calls to request.csp_nonce return the same value
         if not getattr(request, "_csp_nonce", None):
             request._csp_nonce = base64.b64encode(os.urandom(16)).decode("ascii")
         return request._csp_nonce
@@ -32,32 +32,33 @@ def process_request(self, request):
         request.csp_nonce = SimpleLazyObject(nonce)
 
     def process_response(self, request, response):
-        if getattr(response, "_csp_exempt", False):
-            return response
-
-        # Check for ignored path prefix.
-        prefixes = getattr(settings, "CSP_EXCLUDE_URL_PREFIXES", ())
-        if request.path_info.startswith(prefixes):
-            return response
-
         # Check for debug view
-        status_code = response.status_code
         exempted_debug_codes = (
             http_client.INTERNAL_SERVER_ERROR,
             http_client.NOT_FOUND,
         )
-        if status_code in exempted_debug_codes and settings.DEBUG:
+        if response.status_code in exempted_debug_codes and settings.DEBUG:
             return response
 
-        header = "Content-Security-Policy"
-        if getattr(settings, "CSP_REPORT_ONLY", False):
-            header += "-Report-Only"
-
-        if header in response:
-            # Don't overwrite existing headers.
-            return response
-
-        response[header] = self.build_policy(request, response)
+        csp = self.build_policy(request, response)
+        if csp:
+            # Only set header if not already set and not an excluded prefix and not exempted.
+            is_not_exempt = getattr(response, "_csp_exempt", False) is False
+            no_header = HEADER not in response
+            prefixes = getattr(settings, "CONTENT_SECURITY_POLICY", {}).get("EXCLUDE_URL_PREFIXES", ())
+            is_not_excluded = not request.path_info.startswith(prefixes)
+            if all((no_header, is_not_exempt, is_not_excluded)):
+                response[HEADER] = csp
+
+        csp_ro = self.build_policy_ro(request, response)
+        if csp_ro:
+            # Only set header if not already set and not an excluded prefix and not exempted.
+            is_not_exempt = getattr(response, "_csp_exempt_ro", False) is False
+            no_header = HEADER_REPORT_ONLY not in response
+            prefixes = getattr(settings, "CONTENT_SECURITY_POLICY_REPORT_ONLY", {}).get("EXCLUDE_URL_PREFIXES", ())
+            is_not_excluded = not request.path_info.startswith(prefixes)
+            if all((no_header, is_not_exempt, is_not_excluded)):
+                response[HEADER_REPORT_ONLY] = csp_ro
 
         return response
 
@@ -67,3 +68,10 @@ def build_policy(self, request, response):
         replace = getattr(response, "_csp_replace", None)
         nonce = getattr(request, "_csp_nonce", None)
         return build_policy(config=config, update=update, replace=replace, nonce=nonce)
+
+    def build_policy_ro(self, request, response):
+        config = getattr(response, "_csp_config_ro", None)
+        update = getattr(response, "_csp_update_ro", None)
+        replace = getattr(response, "_csp_replace_ro", None)
+        nonce = getattr(request, "_csp_nonce", None)
+        return build_policy(config=config, update=update, replace=replace, nonce=nonce, report_only=True)
diff --git a/csp/utils.py b/csp/utils.py
index 821912d..cf62ee1 100644
--- a/csp/utils.py
+++ b/csp/utils.py
@@ -7,56 +7,78 @@
 from django.utils.encoding import force_str
 
 
-def from_settings():
-    return {
-        # Fetch Directives
-        "child-src": getattr(settings, "CSP_CHILD_SRC", None),
-        "connect-src": getattr(settings, "CSP_CONNECT_SRC", None),
-        "default-src": getattr(settings, "CSP_DEFAULT_SRC", ["'self'"]),
-        "script-src": getattr(settings, "CSP_SCRIPT_SRC", None),
-        "script-src-attr": getattr(settings, "CSP_SCRIPT_SRC_ATTR", None),
-        "script-src-elem": getattr(settings, "CSP_SCRIPT_SRC_ELEM", None),
-        "object-src": getattr(settings, "CSP_OBJECT_SRC", None),
-        "style-src": getattr(settings, "CSP_STYLE_SRC", None),
-        "style-src-attr": getattr(settings, "CSP_STYLE_SRC_ATTR", None),
-        "style-src-elem": getattr(settings, "CSP_STYLE_SRC_ELEM", None),
-        "font-src": getattr(settings, "CSP_FONT_SRC", None),
-        "frame-src": getattr(settings, "CSP_FRAME_SRC", None),
-        "img-src": getattr(settings, "CSP_IMG_SRC", None),
-        "manifest-src": getattr(settings, "CSP_MANIFEST_SRC", None),
-        "media-src": getattr(settings, "CSP_MEDIA_SRC", None),
-        "prefetch-src": getattr(settings, "CSP_PREFETCH_SRC", None),
-        "worker-src": getattr(settings, "CSP_WORKER_SRC", None),
-        # Document Directives
-        "base-uri": getattr(settings, "CSP_BASE_URI", None),
-        "plugin-types": getattr(settings, "CSP_PLUGIN_TYPES", None),
-        "sandbox": getattr(settings, "CSP_SANDBOX", None),
-        # Navigation Directives
-        "form-action": getattr(settings, "CSP_FORM_ACTION", None),
-        "frame-ancestors": getattr(settings, "CSP_FRAME_ANCESTORS", None),
-        "navigate-to": getattr(settings, "CSP_NAVIGATE_TO", None),
-        # Reporting Directives
-        "report-uri": getattr(settings, "CSP_REPORT_URI", None),
-        "report-to": getattr(settings, "CSP_REPORT_TO", None),
-        "require-sri-for": getattr(settings, "CSP_REQUIRE_SRI_FOR", None),
-        # trusted Types Directives
-        "require-trusted-types-for": getattr(settings, "CSP_REQUIRE_TRUSTED_TYPES_FOR", None),
-        "trusted-types": getattr(settings, "CSP_TRUSTED_TYPES", None),
-        # Other Directives
-        "upgrade-insecure-requests": getattr(settings, "CSP_UPGRADE_INSECURE_REQUESTS", False),
-        "block-all-mixed-content": getattr(settings, "CSP_BLOCK_ALL_MIXED_CONTENT", False),
-    }
-
-
-def build_policy(config=None, update=None, replace=None, nonce=None):
+DEFAULT_DIRECTIVES = {
+    # Fetch Directives
+    "child-src": None,
+    "connect-src": None,
+    "default-src": ["'self'"],
+    "script-src": None,
+    "script-src-attr": None,
+    "script-src-elem": None,
+    "object-src": None,
+    "style-src": None,
+    "style-src-attr": None,
+    "style-src-elem": None,
+    "font-src": None,
+    "frame-src": None,
+    "img-src": None,
+    "manifest-src": None,
+    "media-src": None,
+    "prefetch-src": None,  # Deprecated.
+    # Document Directives
+    "base-uri": None,
+    "plugin-types": None,  # Deprecated.
+    "sandbox": None,
+    # Navigation Directives
+    "form-action": None,
+    "frame-ancestors": None,
+    "navigate-to": None,
+    # Reporting Directives
+    "report-uri": None,
+    "report-to": None,
+    "require-sri-for": None,
+    # Trusted Types Directives
+    "require-trusted-types-for": None,
+    "trusted-types": None,
+    # Other Directives
+    "webrtc": None,
+    "worker-src": None,
+    # Directives Defined in Other Documents
+    "upgrade-insecure-requests": None,
+    "block-all-mixed-content": None,  # Deprecated.
+    # Pseudo-directive that affects other directives.
+    "include-nonce-in": None,
+}
+
+
+def default_config(csp):
+    if csp is None:
+        return None
+    # Make a copy of the passed in config to avoid mutating it, and also to drop any unknown keys.
+    config = {}
+    for key, value in DEFAULT_DIRECTIVES.items():
+        config[key] = csp.get(key, value)
+    return config
+
+
+def build_policy(config=None, update=None, replace=None, nonce=None, report_only=False):
     """Builds the policy as a string from the settings."""
 
     if config is None:
-        config = from_settings()
-        # Be careful, don't mutate config as it could be from settings
+        if report_only:
+            config = getattr(settings, "CONTENT_SECURITY_POLICY_REPORT_ONLY", {})
+            config = default_config(config.get("DIRECTIVES", {})) if config else None
+        else:
+            config = getattr(settings, "CONTENT_SECURITY_POLICY", {})
+            config = default_config(config.get("DIRECTIVES", {})) if config else None
+
+    # If config is still `None`, return empty policy.
+    if config is None:
+        return ""
 
     update = update if update is not None else {}
     replace = replace if replace is not None else {}
+
     csp = {}
 
     for k in set(chain(config, replace)):
@@ -80,8 +102,10 @@ def build_policy(config=None, update=None, replace=None, nonce=None):
                 csp[k] += tuple(v)
 
     report_uri = csp.pop("report-uri", None)
+    include_nonce_in = csp.pop("include-nonce-in", [])
 
     policy_parts = {}
+
     for key, value in csp.items():
         # flag directives with an empty directive value
         if len(value) and value[0] is True:
@@ -96,10 +120,9 @@ def build_policy(config=None, update=None, replace=None, nonce=None):
         policy_parts["report-uri"] = " ".join(report_uri)
 
     if nonce:
-        include_nonce_in = getattr(settings, "CSP_INCLUDE_NONCE_IN", ["default-src"])
         for section in include_nonce_in:
             policy = policy_parts.get(section, "")
-            policy_parts[section] = ("{} {}".format(policy, "'nonce-%s'" % nonce)).strip()
+            policy_parts[section] = f"{policy} 'nonce-{nonce}'".strip()
 
     return "; ".join([f"{k} {val}".strip() for k, val in policy_parts.items()])
 
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 7126391..834b417 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -15,149 +15,234 @@ before configuring django-csp.
    policies and even errors when mistakenly configuring them as a ``string``.
 
 
+Migrating from django-csp <= 3.8
+================================
+
+Version 4.0 of django-csp introduces a new configuration format that breaks compatibility with
+previous versions.  If you are migrating from django-csp 3.8 or lower, you will need to update your
+settings to the new format. See the :ref:`migration guide <migration-guide-chapter>` for more
+information.
+
+Configuration
+=============
+
+All configuration of django-csp is done in your Django settings file with the
+``CONTENT_SECURITY_POLICY`` setting or the ``CONTENT_SECURITY_POLICY_REPORT_ONLY`` setting. Each of these
+settings expects a dictionary representing a policy.
+
+The ``CONTENT_SECURITY_POLICY`` setting is your enforcable policy.
+
+The ``CONTENT_SECURITY_POLICY_REPORT_ONLY`` setting is your report-only policy. This policy is
+used to test the policy without breaking the site. It is useful when setting this policy to be
+slightly more strict than the default policy to see what would be blocked if the policy was enforced.
+
+The following is an example of a policy configuration with a default policy and a report-only
+policy. The default policy is considered a "relaxed" policy that allows for the most flexibility
+while still providing a good level of security. The report-only policy is considered a step towards
+a more slightly strict policy and is used to test the policy without breaking the site.
+
+.. code-block:: python
+
+    CONTENT_SECURITY_POLICY = {
+        "EXCLUDE_URL_PREFIXES": ["/excluded-path/"],
+        "DIRECTIVES": {
+            "default-src": ["'self'", "cdn.example.net"],
+            "frame-ancestors": ["'self'"],
+            "form-action": ["'self'"],
+            "report-uri": "/csp-report/",
+        },
+    }
+
+    CONTENT_SECURITY_POLICY_REPORT_ONLY = {
+        "EXCLUDE_URL_PREFIXES": ["/excluded-path/"],
+        "DIRECTIVES": {
+            "default-src": ["'none'"],
+            "connect-src": ["'self'"],
+            "img-src": ["'self'"],
+            "form-action": ["'self'"],
+            "frame-ancestors": ["'self'"],
+            "script-src": ["'self'"],
+            "style-src": ["'self'"],
+            "upgrade-insecure-requests": True,
+            "report-uri": "/csp-report/",
+        },
+    }
+
+
 Policy Settings
 ===============
 
-These settings affect the policy in the header. The defaults are in *italics*.
+At the top level of the policy dictionary, these are the keys that can be used to configure the
+policy.
 
-.. note::
-    Deprecated features of CSP in general have been moved to the bottom of this list.
+``EXCLUDE_URL_PREFIXES``
+    A ``tuple`` of URL prefixes. URLs beginning with any of these will not get the CSP headers.
+    *()*
 
-.. warning::
-   The "special" source values of ``'self'``, ``'unsafe-inline'``,
-   ``'unsafe-eval'``, ``'none'`` and hash-source (``'sha256-...'``) must be
-   quoted! e.g.: ``CSP_DEFAULT_SRC = ("'self'",)``. Without quotes they will
-   not work as intended.
+    .. warning::
 
-``CSP_DEFAULT_SRC``
-    Set the ``default-src`` directive. A ``tuple`` or ``list`` of values,
-    e.g.: ``("'self'", 'cdn.example.net')``. *["'self'"]*
+       Excluding any path on your site will eliminate the benefits of CSP everywhere on your site.
+       The typical browser security model for JavaScript considers all paths alike. A Cross-Site
+       Scripting flaw on, e.g., ``excluded-page/`` can therefore be leveraged to access everything
+       on the same origin.
 
-``CSP_SCRIPT_SRC``
-    Set the ``script-src`` directive. A ``tuple`` or ``list``. *None*
+       # TODO: I can't find any documentation on the above warning.
 
-``CSP_SCRIPT_SRC_ATTR``
-    Set the ``script-src-attr`` directive. A ``tuple`` or ``list``. *None*
+``REPORT_PERCENTAGE``
+    Percentage of requests that should see the ``report-uri`` directive.
+    Use this to throttle the number of CSP violation reports made to your
+    ``report-uri``. An **integer** between 0 and 100 (0 = no reports at all).
+    Ignored if ``report-uri`` isn't set.
 
-``CSP_SCRIPT_SRC_ELEM``
-    Set the ``script-src-elem`` directive. A ``tuple`` or ``list``. *None*
+``DIRECTIVES``
+    A dictionary of policy directives. Each key in the dictionary is a directive and the value is a
+    list of sources for that directive. The following is a list of all the directives that can be
+    configured.
 
-``CSP_IMG_SRC``
-    Set the ``img-src`` directive. A ``tuple`` or ``list``. *None*
+    .. note::
+       The "special" source values of ``'self'``, ``'unsafe-inline'``, ``'unsafe-eval'``,
+       ``'none'`` and hash-source (``'sha256-...'``) must be quoted!
+       e.g.: ``"default-src": ["'self'"]``. Without quotes they will not work as intended.
 
-``CSP_OBJECT_SRC``
-    Set the ``object-src`` directive. A ``tuple`` or ``list``. *None*
+    .. note::
+       Deprecated features of CSP in general have been moved to the bottom of this list.
 
-``CSP_MEDIA_SRC``
-    Set the ``media-src`` directive. A ``tuple`` or ``list``. *None*
+    .. warning::
+       The ``'unsafe-inline'`` and ``'unsafe-eval'`` sources are considered harmful and should be
+       avoided. They are included here for completeness, but should not be used in production.
 
-``CSP_FRAME_SRC``
-    Set the ``frame-src`` directive. A ``tuple`` or ``list``. *None*
+    ``default-src``
+        Set the ``default-src`` directive. A ``tuple`` or ``list`` of values,
+        e.g.: ``("'self'", 'cdn.example.net')``. *["'self'"]*
 
-``CSP_FONT_SRC``
-    Set the ``font-src`` directive. A ``tuple`` or ``list``. *None*
+    ``script-src``
+        Set the ``script-src`` directive. A ``tuple`` or ``list``. *None*
 
-``CSP_CONNECT_SRC``
-    Set the ``connect-src`` directive. A ``tuple`` or ``list``. *None*
+    ``script-src-attr``
+        Set the ``script-src-attr`` directive. A ``tuple`` or ``list``. *None*
 
-``CSP_STYLE_SRC``
-    Set the ``style-src`` directive. A ``tuple`` or ``list``. *None*
+    ``script-src-elem``
+        Set the ``script-src-elem`` directive. A ``tuple`` or ``list``. *None*
 
-``CSP_STYLE_SRC_ATTR``
-    Set the ``style-src-attr`` directive. A ``tuple`` or ``list``. *None*
+    ``img-src``
+        Set the ``img-src`` directive. A ``tuple`` or ``list``. *None*
 
-``CSP_STYLE_SRC_ELEM``
-    Set the ``style-src-elem`` directive. A ``tuple`` or ``list``. *None*
+    ``object-src``
+        Set the ``object-src`` directive. A ``tuple`` or ``list``. *None*
 
-``CSP_BASE_URI``
-    Set the ``base-uri`` directive. A ``tuple`` or ``list``. *None*
+    ``media-src``
+        Set the ``media-src`` directive. A ``tuple`` or ``list``. *None*
 
-    Note: This doesn't use ``default-src`` as a fall-back.
+    ``frame-src``
+        Set the ``frame-src`` directive. A ``tuple`` or ``list``. *None*
 
-``CSP_CHILD_SRC``
-    Set the ``child-src`` directive. A ``tuple`` or ``list``. *None*
+    ``font-src``
+        Set the ``font-src`` directive. A ``tuple`` or ``list``. *None*
 
-``CSP_FRAME_ANCESTORS``
-    Set the ``frame-ancestors`` directive. A ``tuple`` or ``list``. *None*
+    ``connect-src``
+        Set the ``connect-src`` directive. A ``tuple`` or ``list``. *None*
 
-    Note: This doesn't use ``default-src`` as a fall-back.
+    ``style-src``
+        Set the ``style-src`` directive. A ``tuple`` or ``list``. *None*
 
-``CSP_NAVIGATE_TO``
-    Set the ``navigate-to`` directive. A ``tuple`` or ``list``. *None*
+    ``style-src-attr``
+        Set the ``style-src-attr`` directive. A ``tuple`` or ``list``. *None*
 
-    Note: This doesn't use ``default-src`` as a fall-back.
+    ``style-src-elem``
+        Set the ``style-src-elem`` directive. A ``tuple`` or ``list``. *None*
 
-``CSP_FORM_ACTION``
-    Set the ``FORM_ACTION`` directive. A ``tuple`` or ``list``. *None*
+    ``base-uri``
+        Set the ``base-uri`` directive. A ``tuple`` or ``list``. *None*
 
-    Note: This doesn't use ``default-src`` as a fall-back.
+        Note: This doesn't use ``default-src`` as a fall-back.
 
-``CSP_SANDBOX``
-    Set the ``sandbox`` directive. A ``tuple`` or ``list``. *None*
+    ``child-src``
+        Set the ``child-src`` directive. A ``tuple`` or ``list``. *None*
 
-    Note: This doesn't use ``default-src`` as a fall-back.
+    ``frame-ancestors``
+        Set the ``frame-ancestors`` directive. A ``tuple`` or ``list``. *None*
 
-``CSP_REPORT_URI``
-    Set the ``report-uri`` directive. A ``tuple`` or ``list`` of URIs.
-    Each URI can be a full or relative URI. *None*
+        Note: This doesn't use ``default-src`` as a fall-back.
 
-    Note: This doesn't use ``default-src`` as a fall-back.
+    ``navigate-to``
+        Set the ``navigate-to`` directive. A ``tuple`` or ``list``. *None*
+
+        Note: This doesn't use ``default-src`` as a fall-back.
+
+    ``form-action``
+        Set the ``FORM_ACTION`` directive. A ``tuple`` or ``list``. *None*
+
+        Note: This doesn't use ``default-src`` as a fall-back.
+
+    ``sandbox``
+        Set the ``sandbox`` directive. A ``tuple`` or ``list``. *None*
 
-``CSP_REPORT_TO``
-    Set the ``report-to`` directive. A ``string`` describing a reporting
-    group. *None*
+        Note: This doesn't use ``default-src`` as a fall-back.
 
-    See Section 1.2: https://w3c.github.io/reporting/#group
+    ``report-uri``
+        Set the ``report-uri`` directive. A ``tuple`` or ``list`` of URIs.
+        Each URI can be a full or relative URI. *None*
 
-    Also `see this MDN note on <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri>`_ ``report-uri`` and ``report-to``.
+        Note: This doesn't use ``default-src`` as a fall-back.
 
-``CSP_MANIFEST_SRC``
-    Set the ``manifest-src`` directive. A ``tuple`` or ``list``. *None*
+    ``report-to``
+        Set the ``report-to`` directive. A ``string`` describing a reporting
+        group. *None*
 
-``CSP_WORKER_SRC``
-    Set the ``worker-src`` directive. A ``tuple`` or ``list``. *None*
+        See Section 1.2: https://w3c.github.io/reporting/#group
 
-``CSP_REQUIRE_SRI_FOR``
-    Set the ``require-sri-for`` directive. A ``tuple`` or ``list``. *None*
+        Also `see this MDN note on <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri>`_ ``report-uri`` and ``report-to``.
 
-    Valid values: a ``list`` containing ``'script'``, ``'style'``, or both.
+    ``manifest-src``
+        Set the ``manifest-src`` directive. A ``tuple`` or ``list``. *None*
 
-    Spec: require-sri-for-known-tokens_
+    ``worker-src``
+        Set the ``worker-src`` directive. A ``tuple`` or ``list``. *None*
 
-``CSP_UPGRADE_INSECURE_REQUESTS``
-    Include ``upgrade-insecure-requests`` directive. A ``boolean``. *False*
+    ``require-sri-for``
+        Set the ``require-sri-for`` directive. A ``tuple`` or ``list``. *None*
 
-    Spec: upgrade-insecure-requests_
+        Valid values: a ``list`` containing ``'script'``, ``'style'``, or both.
 
-``CSP_REQUIRE_TRUSTED_TYPES_FOR``
-    Include ``require-trusted-types-for`` directive.
-    A ``tuple`` or ``list``. *None*
+        Spec: require-sri-for-known-tokens_
 
-    Valid values: ``["'script'"]``
+    ``upgrade-insecure-requests``
+        Include ``upgrade-insecure-requests`` directive. A ``boolean``. *False*
 
-``CSP_TRUSTED_TYPES``
-    Include ``trusted-types`` directive.
-    A ``tuple`` or ``list``. *None*
+        Spec: upgrade-insecure-requests_
 
-    Valid values: a ``list`` of allowed policy names that may include
-    ``default`` and/or ``'allow-duplicates'``
+    ``require-trusted-types-for``
+        Include ``require-trusted-types-for`` directive.
+        A ``tuple`` or ``list``. *None*
 
-``CSP_INCLUDE_NONCE_IN``
-    Include dynamically generated nonce in all listed directives.
-    A ``tuple`` or ``list``, e.g.: ``CSP_INCLUDE_NONCE_IN = ['script-src']``
-    will add ``'nonce-<b64-value>'`` to the ``script-src`` directive.
-    *['default-src']*
+        Valid values: ``["'script'"]``
+
+    ``trusted-types``
+        Include ``trusted-types`` directive.
+        A ``tuple`` or ``list``. *None*
+
+        Valid values: a ``list`` of allowed policy names that may include
+        ``default`` and/or ``'allow-duplicates'``
+
+    ``include-nonce-in``
+        A ``tuple`` of directives to include a nonce in. *['default-src']*  Any directive that is 
+        included in this list will have a nonce value added to it of the form ``'nonce-{nonce-value}'``.
+
+        Note: This is a bit of a "pseudo"-directive. It's not a real CSP directive as defined by the
+        spec, but it's used to determine which directives should include a nonce value. This is
+        useful for adding nonces to scripts and styles.
+
+        Note: The nonce value will only be generated if ``request.csp_nonce`` is accessed during the
+        request/response cycle.
 
-    Note: The nonce value will only be generated if ``request.csp_nonce``
-    is accessed during the request/response cycle.
 
 Deprecated CSP settings
 -----------------------
-The following settings are still configurable, but are considered deprecated
+The following ``DIRECTIVES`` settings are still configurable, but are considered deprecated
 in terms of the latest implementation of the relevant spec.
 
 
-``CSP_BLOCK_ALL_MIXED_CONTENT``
+``block-all-mixed-content``
     Include ``block-all-mixed-content`` directive. A ``boolean``. *False*
 
     Related `note on MDN <block-all-mixed-content_mdn_>`_.
@@ -165,8 +250,7 @@ in terms of the latest implementation of the relevant spec.
     Spec: block-all-mixed-content_
 
 
-
-``CSP_PLUGIN_TYPES``
+``plugin-types``
     Set the ``plugin-types`` directive. A ``tuple`` or ``list``. *None*
 
     Note: This doesn't use ``default-src`` as a fall-back.
@@ -174,7 +258,7 @@ in terms of the latest implementation of the relevant spec.
     Related `note on MDN <plugin_types_mdn_>`_.
 
 
-``CSP_PREFETCH_SRC``
+``prefetch-src``
     Set the ``prefetch-src`` directive. A ``tuple`` or ``list``. *None*
 
     Related `note on MDN <prefetch_src_mdn_>`_.
@@ -187,31 +271,6 @@ The policy can be changed on a per-view (or even per-request) basis. See
 the :ref:`decorator documentation <decorator-chapter>` for more details.
 
 
-Other Settings
-==============
-
-These settings control the behavior of django-csp. Defaults are in
-*italics*.
-
-``CSP_REPORT_ONLY``
-    Send "report-only" headers instead of real headers.
-    A ``boolean``. *False*
-
-    See the spec_ and the chapter on :ref:`reports <reports-chapter>` for
-    more info.
-
-``CSP_EXCLUDE_URL_PREFIXES``
-    A ``tuple`` (*not* a ``list``) of URL prefixes. URLs beginning with any
-    of these will not get the CSP headers. *()*
-
-.. warning::
-
-   Excluding any path on your site will eliminate the benefits of CSP
-   everywhere on your site. The typical browser security model for
-   JavaScript considers all paths alike. A Cross-Site Scripting flaw
-   on, e.g., ``excluded-page/`` can therefore be leveraged to access
-   everything on the same origin.
-
 .. _Content-Security-Policy: https://www.w3.org/TR/CSP/
 .. _Content-Security-Policy-L3: https://w3c.github.io/webappsec-csp/
 .. _spec: Content-Security-Policy_
@@ -221,3 +280,4 @@ These settings control the behavior of django-csp. Defaults are in
 .. _block-all-mixed-content_mdn: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/block-all-mixed-content
 .. _plugin_types_mdn: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/plugin-types
 .. _prefetch_src_mdn: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/prefetch-src
+.. _strict-csp: https://csp.withgoogle.com/docs/strict-csp.html
\ No newline at end of file
diff --git a/docs/contributing.rst b/docs/contributing.rst
index 4fc170e..3c98b24 100644
--- a/docs/contributing.rst
+++ b/docs/contributing.rst
@@ -34,6 +34,10 @@ Then just `pytest`_ to run the tests::
 
     pytest
 
+To run the tests with coverage and get a report, use the following command::
+
+    pytest --cov=csp --cov-config=.coveragerc
+
 
 .. _PEP8: http://www.python.org/dev/peps/pep-0008/
 .. _ruff: https://pypi.org/project/ruff/
diff --git a/docs/decorators.rst b/docs/decorators.rst
index 3ba6183..6c36378 100644
--- a/docs/decorators.rst
+++ b/docs/decorators.rst
@@ -4,9 +4,8 @@
 Modifying the Policy with Decorators
 ====================================
 
-Content Security Policies should be restricted and paranoid by default.
-You may, on some views, need to expand or change the policy. django-csp
-includes four decorators to help.
+Content Security Policies should be restricted and paranoid by default.  You may, on some views,
+need to expand or change the policy. django-csp includes four decorators to help.
 
 
 ``@csp_exempt``
@@ -20,12 +19,17 @@ view.
     from csp.decorators import csp_exempt
 
     # Will not have a CSP header.
-    @csp_exempt
+    @csp_exempt()
     def myview(request):
         return render(...)
 
-You can manually set this on a per-response basis by setting the
-``_csp_exempt`` attribute on the response to ``True``::
+    # Will not have a CSP report-only header.
+    @csp_exempt(REPORT_ONLY=True)
+    def myview(request):
+        return render(...)
+
+You can manually set this on a per-response basis by setting the ``_csp_exempt``
+or ``_csp_exempt_ro`` attribute on the response to ``True``::
 
     # Also will not have a CSP header.
     def myview(request):
@@ -37,28 +41,31 @@ You can manually set this on a per-response basis by setting the
 ``@csp_update``
 ===============
 
-The ``@csp_update`` header allows you to **append** values to the source
-lists specified in the settings. If there is no setting, the value
-passed to the decorator will be used verbatim.
+The ``@csp_update`` header allows you to **append** values to the source lists specified in the
+settings. If there is no setting, the value passed to the decorator will be used verbatim.
 
 .. note::
-   To quote the CSP spec: "There's no inheritance; ... the default list
-   is not used for that resource type" if it is set. E.g., the following
-   will not allow images from 'self'::
+   To quote the CSP spec: "There's no inheritance; ... the default list is not used for that
+   resource type" if it is set. E.g., the following will not allow images from 'self'::
 
     default-src 'self'; img-src imgsrv.com
 
-The arguments to the decorator the same as the :ref:`settings
-<configuration-chapter>` without the ``CSP_`` prefix, e.g. ``IMG_SRC``.
-(They are also case-insensitive.) The values are either strings, lists
-or tuples.
+The arguments to the decorator are the same as the :ref:`settings <configuration-chapter>`. The
+decorator excpects a single dictionary argument, where the keys are the directives and the values
+are either strings, lists or tuples. An optional argument, ``REPORT_ONLY``, can be set to ``True``
+to update the report-only policy instead of the enforced policy.
 
 ::
 
     from csp.decorators import csp_update
 
-    # Will allow images from imgsrv.com.
-    @csp_update(IMG_SRC='imgsrv.com')
+    # Will append imgsrv.com to the list of values for `img-src` in the enforced policy.
+    @csp_update({"img-src": "imgsrv.com"})
+    def myview(request):
+        return render(...)
+
+    # Will append cdn-img.com to the list of values for `img-src` in the report-only policy.
+    @csp_update({"img-src": "cdn-img.com"}, REPORT_ONLY=True)
     def myview(request):
         return render(...)
 
@@ -66,18 +73,21 @@ or tuples.
 ``@csp_replace``
 ================
 
-The ``@csp_replace`` decorator allows you to **replace** a source list
-specified in settings. If there is no setting, the value passed to the
-decorator will be used verbatim. (See the note under ``@csp_update``.)
-If the specified value is None, the corresponding key will not be included.
+The ``@csp_replace`` decorator allows you to **replace** a source list specified in settings. If
+there is no setting, the value passed to the decorator will be used verbatim. (See the note under
+``@csp_update``.) If the specified value is None, the corresponding key will not be included.
 
 The arguments and values are the same as ``@csp_update``::
 
     from csp.decorators import csp_replace
 
-    # settings.CSP_IMG_SRC = ['imgsrv.com']
-    # Will allow images from imgsrv2.com, but not imgsrv.com.
-    @csp_replace(IMG_SRC='imgsrv2.com')
+    # Will allow images only from imgsrv2.com in the enforced policy.
+    @csp_replace({"img-src": "imgsrv2.com"})
+    def myview(request):
+        return render(...)
+
+    # Will allow images only from cdn-img2.com in the report-only policy.
+    @csp_replace({"img-src": "imgsrv2.com"})
     def myview(request):
         return render(...)
 
@@ -85,13 +95,23 @@ The arguments and values are the same as ``@csp_update``::
 ``@csp``
 ========
 
-If you need to set the entire policy on a view, ignoring all the
-settings, you can use the ``@csp`` decorator. The arguments and values
-are as above::
+If you need to set the entire policy on a view, ignoring all the settings, you can use the ``@csp``
+decorator. This, and the other decorators, can be stacked to update both policies if both are in
+use, as shown below. The arguments and values are as above::
 
     from csp.decorators import csp
 
-    @csp(DEFAULT_SRC=["'self'"], IMG_SRC=['imgsrv.com'],
-         SCRIPT_SRC=['scriptsrv.com', 'googleanalytics.com'])
+    @csp({
+        "default_src": ["'self'"],
+        "img-src": ["imgsrv.com"],
+        "script-src": ["scriptsrv.com", "googleanalytics.com", "'unsafe-inline'"]}
+    })
+    @csp({
+        "default_src": ["'self'"],
+        "img-src": ["imgsrv.com"],
+        "script-src": ["scriptsrv.com", "googleanalytics.com"]},
+        "frame-src": ["'self'"],
+        REPORT_ONLY=True
+    })
     def myview(request):
         return render(...)
diff --git a/docs/index.rst b/docs/index.rst
index 5597cfb..419ab76 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -21,6 +21,7 @@ Contents:
 
    installation
    configuration
+   migration-guide
    decorators
    nonce
    trusted_types
diff --git a/docs/installation.rst b/docs/installation.rst
index 7745419..f593de9 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -4,26 +4,33 @@
 Installing django-csp
 =====================
 
-First, install django-csp via pip or from source::
+First, install django-csp via pip or from source:
 
-    # pip
-    $ pip install django-csp
+.. code-block:: bash
 
-::
+    pip install django-csp
 
-    # source
-    $ git clone https://github.com/mozilla/django-csp.git
-    $ cd django-csp
-    $ python setup.py install
+Add the csp app to your ``INSTALLED_APPS`` in your project's ``settings`` module:
+
+.. code-block:: python
+
+    INSTALLED_APPS = (
+        # ...
+        "csp",
+        # ...
+    )
 
 Now edit your project's ``settings`` module, to add the django-csp middleware
-to ``MIDDLEWARE``, like so::
+to ``MIDDLEWARE``, like so:
+
+.. code-block:: python
 
     MIDDLEWARE = (
         # ...
-        'csp.middleware.CSPMiddleware',
+        "csp.middleware.CSPMiddleware",
         # ...
     )
+
 Note: Middleware order does not matter unless you have other middleware modifying the CSP header.
 
 That should do it! Go on to :ref:`configuring CSP <configuration-chapter>`.
diff --git a/docs/migration-guide.rst b/docs/migration-guide.rst
new file mode 100644
index 0000000..a0772e6
--- /dev/null
+++ b/docs/migration-guide.rst
@@ -0,0 +1,172 @@
+.. _migration-guide-chapter:
+
+==============================
+django-csp 4.0 Migration Guide
+==============================
+
+Overview
+========
+
+In the latest version of `django-csp`, the format for configuring Content Security Policy (CSP)
+settings has been updated are are backwards-incompatible with prior versions. The previous approach
+of using individual settings prefixed with ``CSP_`` for each directive is no longer supported.
+Instead, all CSP settings are now consolidated into one of two dict-based settings:
+``CONTENT_SECURITY_POLICY`` or ``CONTENT_SECURITY_POLICY_REPORT_ONLY``.
+
+Migrating from the Old Settings Format
+======================================
+
+Update `django-csp`
+-------------------
+
+First, update the `django-csp` package to the latest version that supports the new settings format.
+You can do this by running:
+
+.. code-block:: bash
+
+    pip install -U django-csp
+
+Add the `csp` app to `INSTALLED_APPS`
+-------------------------------------
+
+In your Django project's `settings.py` file, add the `csp` app to the ``INSTALLED_APPS`` setting:
+
+.. code-block:: python
+
+    INSTALLED_APPS = [
+        # ...
+        "csp",
+        # ...
+    ]
+
+Run the Django check command
+----------------------------
+
+This is optional but can help kick start the new settings configuration for you. Run the Django
+check command which will look for old settings and output a configuration in the new format:
+
+.. code-block:: bash
+
+    python manage.py check
+
+This can help you identify the existing CSP settings in your project and provide a starting point
+for migrating to the new format.
+
+Identify Existing CSP Settings
+------------------------------
+
+Locate all the existing CSP settings in your Django project. These settings start with the
+``CSP_`` prefix, such as ``CSP_DEFAULT_SRC``, ``CSP_SCRIPT_SRC``, ``CSP_IMG_SRC``, etc.
+
+Create the New Settings Dictionary
+----------------------------------
+
+In your Django project's `settings.py` file, create a new dictionary called
+``CONTENT_SECURITY_POLICY`` or ``CONTENT_SECURITY_POLICY_REPORT_ONLY``, depending on whether you want to
+enforce the policy or only report violations, or both. Use the output from the Django check command
+as a starting point to populate this dictionary.
+
+Migrate Existing Settings
+-------------------------
+
+Migrate your existing CSP settings to the new format by populating the ``DIRECTIVES`` dictionary
+inside the ``CONTENT_SECURITY_POLICY`` setting. The keys of the ``DIRECTIVES`` dictionary should be the
+CSP directive names in lowercase, and the values should be lists containing the corresponding
+sources. The Django check command output can help you identify the directive names and sources.
+
+For example, if you had the following old settings:
+
+.. code-block:: python
+
+    CSP_DEFAULT_SRC = ["'self'", "*.example.com"]
+    CSP_SCRIPT_SRC = ["'self'", "js.cdn.com/example/"]
+    CSP_IMG_SRC = ["'self'", "data:", "example.com"]
+    CSP_EXCLUDE_URL_PREFIXES = ["/admin"]
+
+The new settings would be:
+
+.. code-block:: python
+
+    CONTENT_SECURITY_POLICY = {
+        "EXCLUDE_URL_PREFIXES": ["/admin"],
+        "DIRECTIVES": {
+            "default-src": ["'self'", "*.example.com"],
+            "script-src": ["'self'", "js.cdn.com/example/"],
+            "img-src": ["'self'", "data:", "example.com"],
+        },
+    }
+
+.. note::
+
+    The keys in the ``DIRECTIVES`` dictionary, the directive names, are in lowercase and use dashes
+    instead of underscores to match the CSP specification.
+
+.. note::
+
+    If you were using the ``CSP_REPORT_PERCENTAGE`` setting, this should be updated to be an integer
+    percentage and not a decimal value in the new settings format. For example, if you had
+    ``CSP_REPORT_PERCENTAGE = 0.1``, this should be updated to:
+
+    .. code-block:: python
+
+        CONTENT_SECURITY_POLICY = {
+            "REPORT_PERCENTAGE": 10,
+            "DIRECTIVES": {
+                "report-uri": "/csp-report/",
+                # ...
+            },
+        }
+
+Remove Old Settings
+-------------------
+
+After migrating to the new settings format, remove all the old ``CSP_`` prefixed settings from your
+`settings.py` file.
+
+Update the CSP decorators
+-------------------------
+
+If you are using the CSP decorators in your views, those will need to be updated as well. The
+decorators now accept a dictionary containing the CSP directives as an argument. For example:
+
+.. code-block:: python
+
+    from csp.decorators import csp_update
+
+
+    @csp_update({"default-src": ["another-url.com"]})
+    def my_view(request): ...
+
+Additionally, each decorator now takes an optional ``report_only`` argument to specify whether the
+policy should be enforced or only report violations. For example:
+
+.. code-block:: python
+
+    from csp.decorators import csp
+
+
+    @csp({"default-src": ["'self'"]}, report_only=True)
+    def my_view(request): ...
+
+Due to the addition of the ``report_only`` argument and for consistency, the ``csp_exempt``
+decorator now requires parentheses when used with and without arguments. For example:
+
+.. code-block:: python
+
+    from csp.decorators import csp_exempt
+
+
+    @csp_exempt()
+    @csp_exempt(report_only=True)
+    def my_view(request): ...
+
+Look for uses of the following decorators in your code: ``@csp``, ``@csp_update``, ``@csp_replace``,
+and ``@csp_exempt``.
+
+Conclusion
+==========
+
+By following this migration guide, you should be able to successfully update your Django project to
+use the new dict-based CSP settings format introduced in the latest version of `django-csp`. This
+change aligns the package with the latest CSP specification and provides a more organized and
+flexible way to configure your Content Security Policy.
diff --git a/docs/nonce.rst b/docs/nonce.rst
index 7bfb5f6..12f1351 100644
--- a/docs/nonce.rst
+++ b/docs/nonce.rst
@@ -1,9 +1,10 @@
 ==============================
 Using the generated CSP nonce
 ==============================
-When ``CSP_INCLUDE_NONCE_IN`` is configured, the nonce value is returned in the CSP headers **if it is used**, e.g. by evaluating the nonce in your template.
-To actually make the browser do anything with this value, you will need to include it in the attributes of
-the tags that you wish to mark as safe.
+When ``include-nonce-in`` is configured, the nonce value is returned in the CSP headers **if it is
+used**, e.g. by evaluating the nonce in your template.  To actually make the browser do anything
+with this value, you will need to include it in the attributes of the tags that you wish to mark as
+safe.
 
 
 .. Note::
@@ -16,17 +17,19 @@ the tags that you wish to mark as safe.
 
 ``Middleware``
 ==============
-Installing the middleware creates a lazily evaluated property ``csp_nonce`` and attaches it to all incoming requests.
+Installing the middleware creates a lazily evaluated property ``csp_nonce`` and attaches it to all
+incoming requests.
 
 .. code-block:: python
 
     MIDDLEWARE = (
-    	#...
-    	'csp.middleware.CSPMiddleware',
-    	#...
+        # ...
+        "csp.middleware.CSPMiddleware",
+        # ...
     )
 
-This value can be accessed directly on the request object in any view or template and manually appended to any script element like so -
+This value can be accessed directly on the request object in any view or template and manually
+appended to any script element like so -
 
 .. code-block:: html
 
@@ -34,7 +37,8 @@ This value can be accessed directly on the request object in any view or templat
 		var hello="world";
 	</script>
 
-Assuming the ``CSP_INCLUDE_NONCE_IN`` list contains the ``script-src`` directive, this will result in the above script being allowed.
+Assuming the ``include-nonce-in`` list contains the ``script-src`` directive, this will result in
+the above script being allowed.
 
 .. Note::
 
@@ -43,7 +47,10 @@ Assuming the ``CSP_INCLUDE_NONCE_IN`` list contains the ``script-src`` directive
 
 ``Context Processor``
 =====================
-This library contains an optional context processor, adding ``csp.context_processors.nonce`` to your configured context processors exposes a variable called ``CSP_NONCE`` into the global template context. This is simple shorthand for ``request.csp_nonce``, but can be useful if you have many occurrences of script tags.
+This library contains an optional context processor, adding ``csp.context_processors.nonce`` to your
+configured context processors exposes a variable called ``CSP_NONCE`` into the global template
+context. This is simple shorthand for ``request.csp_nonce``, but can be useful if you have many
+occurrences of script tags.
 
 .. code-block:: jinja
 
@@ -57,12 +64,18 @@ This library contains an optional context processor, adding ``csp.context_proces
 
 .. note::
 
-   If you're making use of ``csp.extensions.NoncedScript`` you need to have ``jinja2>=2.9.6`` installed, so please make sure to either use ``django-csp[jinja2]`` in your requirements or define it yourself.
+   If you're making use of ``csp.extensions.NoncedScript`` you need to have ``jinja2>=2.9.6``
+   installed, so please make sure to either use ``django-csp[jinja2]`` in your requirements or
+   define it yourself.
 
 
-It can be easy to forget to include the ``nonce`` property in a script tag, so there is also a ``script`` template tag available for both Django templates and Jinja environments.
+It can be easy to forget to include the ``nonce`` property in a script tag, so there is also a
+``script`` template tag available for both Django templates and Jinja environments.
 
-This tag will output a properly nonced script every time. For the sake of syntax highlighting, you can wrap the content inside of the ``script`` tag in ``<script>`` html tags, which will be subsequently removed in the rendered output. Any valid script tag attributes can be specified and will be forwarded into the rendered html.
+This tag will output a properly nonced script every time. For the sake of syntax highlighting, you
+can wrap the content inside of the ``script`` tag in ``<script>`` html tags, which will be
+subsequently removed in the rendered output. Any valid script tag attributes can be specified and
+will be forwarded into the rendered html.
 
 
 Django Templates
@@ -72,15 +85,15 @@ Add the CSP template tags to the TEMPLATES section of your settings file:
 
 .. code-block:: python
 
-	TEMPLATES = [
-	    {
-		"OPTIONS": {
-		    'libraries':          {
-			'csp': 'csp.templatetags.csp',
-		    }
-		},
-	    }
-	]
+    TEMPLATES = [
+        {
+            "OPTIONS": {
+                "libraries": {
+                    "csp": "csp.templatetags.csp",
+                }
+            }
+        }
+    ]
 
 Then load the ``csp`` template tags and use ``script`` in the template:
 
@@ -101,16 +114,16 @@ Add ``csp.extensions.NoncedScript`` to the TEMPLATES section of your settings fi
 
 .. code-block:: python
 
-          TEMPLATES = [
-              {
-                  'BACKEND':'django.template.backends.jinja2.Jinja2',
-                  'OPTIONS': {
-                      'extensions': [
-                          'csp.extensions.NoncedScript',
-                      ],
-                  }
-             }
-          ]
+    TEMPLATES = [
+        {
+            "BACKEND": "django.template.backends.jinja2.Jinja2",
+            "OPTIONS": {
+                "extensions": [
+                    "csp.extensions.NoncedScript",
+                ],
+            },
+        }
+    ]
 
 
 .. code-block:: jinja
diff --git a/docs/reports.rst b/docs/reports.rst
index f2e6b8f..8337d58 100644
--- a/docs/reports.rst
+++ b/docs/reports.rst
@@ -4,26 +4,23 @@
 CSP Violation Reports
 =====================
 
-When something on a page violates the Content-Security-Policy, and the
-policy defines a ``report-uri`` directive, the user agent may POST a
-report_. Reports are JSON blobs containing information about how the
-policy was violated.
+When something on a page violates the Content-Security-Policy, and the policy defines a
+``report-uri`` directive, the user agent may POST a report_. Reports are JSON blobs containing
+information about how the policy was violated.
 
-Note: django-csp no longer handles report processing itself, so you will
-need to stand up your own app to receive them, or else make use of a
-third-party report processing service.
+Note: django-csp no longer handles report processing itself, so you will need to stand up your own
+app to receive them, or else make use of a third-party report processing service.
 
 
 Throttling the number of reports
 --------------------------------
-To throttle the number of requests made to your ``report-uri`` endpoint, you
-can use ``csp.contrib.rate_limiting.RateLimitedCSPMiddleware`` instead of
-``csp.middleware.CSPMiddleware`` and set the ``CSP_REPORT_PERCENTAGE`` option:
+To throttle the number of requests made to your ``report-uri`` endpoint, you can use
+``csp.contrib.rate_limiting.RateLimitedCSPMiddleware`` instead of ``csp.middleware.CSPMiddleware``
+and set the ``REPORT_PERCENTAGE`` option:
 
-``CSP_REPORT_PERCENTAGE``
-    Percentage of requests that should see the ``report-uri`` directive.
-    Use this to throttle the number of CSP violation reports made to your
-    ``CSP_REPORT_URI``. A **float** between 0 and 1 (0 = no reports at all).
-    Ignored if ``CSP_REPORT_URI`` isn't set.
+``REPORT_PERCENTAGE``
+    Percentage of requests that should see the ``report-uri`` directive.  Use this to throttle the
+    number of CSP violation reports made to your ``report-uri``. An **integer** between 0 and 100 (0
+    = no reports at all).  Ignored if ``report-uri`` isn't set.
 
 .. _report: http://www.w3.org/TR/CSP/#sample-violation-report
diff --git a/docs/trusted_types.rst b/docs/trusted_types.rst
index 58d1bee..4171f35 100644
--- a/docs/trusted_types.rst
+++ b/docs/trusted_types.rst
@@ -4,44 +4,44 @@ Implementing Trusted Types with CSP
 
 ``DOM Cross-site Scripting``
 ============================
-Cross-site scripting (XSS) is one of the most prevalent vulnerabilities
-on the web. Nonce-based CSP is used to prevent server-side XSS. Trusted
-Types are used to prevent client-side or DOM-XSS_. Trusted Types rely on
-the browser to enforce the policy that is provided to it. Currently, Trusted
-Types are supported on Chrome 83 and Android Webview. Many browsers are in the
-process of adding support. Check back for updated compatibility_.
+Cross-site scripting (XSS) is one of the most prevalent vulnerabilities on the web. Nonce-based CSP
+is used to prevent server-side XSS. Trusted Types are used to prevent client-side or DOM-XSS_.
+Trusted Types rely on the browser to enforce the policy that is provided to it. Currently, Trusted
+Types are supported on Chrome 83 and Android Webview. Many browsers are in the process of adding
+support. Check back for updated compatibility_.
 
-Follow the simple steps below to make your web application Trusted Types
-compliant.
+Follow the simple steps below to make your web application Trusted Types compliant.
 
 
 ``Step 1: Enable Trusted Types and Report Only Mode``
 =====================================================
-Trusted Types require data to be processed before being sent to a risky "sink" where DOM XSS might occur, such as when assigning to Element.innerHTML or calling document.write. When enforced, Trusted Types will tell the
-browser to block any data that is not properly processed. In order to avoid
-this, you must fix offending parts of your code. To see where adjustments will
-be required, turn on trusted types and report only mode.
+Trusted Types require data to be processed before being sent to a risky "sink" where DOM XSS might
+occur, such as when assigning to Element.innerHTML or calling document.write. When enforced, Trusted
+Types will tell the browser to block any data that is not properly processed. In order to avoid
+this, you must fix offending parts of your code. To see where adjustments will be required, turn on
+trusted types and report only mode.
 
-Configure django-csp so that ``CSP_REQUIRE_TRUSTED_TYPES_FOR`` is set to *‘script’*.
+Configure django-csp so that the directive ``require-trusted-types-for`` is set to *‘script’*.
 
-Configure django-csp so that ``CSP_REPORT_ONLY`` is set to *True*.
+Configure the django-csp report-only policy.
 
-Configure django-csp so that ``CSP_REPORT_URI`` is set to an app or CSP report processing service that you control.
+Configure django-csp so that the directive ``report-uri`` is set to an app or CSP report processing
+service that you control.
 
-Now trusted types violations will be reported to your ``CSP_REPORT_URI`` without blocking any of your application’s functionalities.
+Now trusted types violations will be reported to your ``report-uri`` without blocking any of your
+application’s functionalities.
 
 
 ``Step 2: Fixing Trusted Types Violations``
 ===========================================
-There are four ways to resolve trusted types violations. They are explained
-here in order of preference.
+There are four ways to resolve trusted types violations. They are explained here in order of
+preference.
 
 Rewrite the Code
 ----------------
-It may be possible for your code to be rewritten without using dangerous
-functions. For example, instead of dynamically placing an image using the
-dangerous ``innerHTML`` sink, the image could be created with
-``document.createElement`` and placed using the ``appendChild`` function.
+It may be possible for your code to be rewritten without using dangerous functions. For example,
+instead of dynamically placing an image using the dangerous ``innerHTML`` sink, the image could be
+created with ``document.createElement`` and placed using the ``appendChild`` function.
 
 Rewriting may be possible for any of the dangerous sinks, which are listed here.
 
@@ -56,7 +56,7 @@ Rewriting may be possible for any of the dangerous sinks, which are listed here.
         * Tip: Use createElement and appendChild as explained above
 * Executing plugin content:
     * ``<embed src>``, ``<object data>`` and ``<object codebase>``
-        * Tip: Consider limiting plugin content by setting``CSP_OBJECT_SRC`` to *none*
+        * Tip: Consider limiting plugin content by setting ``object-src`` to *none*
 * Runtime JavaScript code compilation:
     * ``eval``, ``setTimeout``, ``setInterval``, and ``new Function()``
         * Tip: Avoid using eval entirely
@@ -64,11 +64,11 @@ Rewriting may be possible for any of the dangerous sinks, which are listed here.
 
 Use a Library
 -------------
-When code cannot be rewritten to avoid dangerous sinks, Trusted Types require
-that data be processed before being passed to a dangerous sink. Processed data
-is wrapped in a TrustedHTML, TrustedScript, or TrustedScriptURL object to certify that
-it has been sanitized or otherwise assured to be safe in the given context. Some libraries will process data and return Trusted
-Types objects for you. For example, DOMPurify_ supports Trusted Types.
+When code cannot be rewritten to avoid dangerous sinks, Trusted Types require that data be processed
+before being passed to a dangerous sink. Processed data is wrapped in a TrustedHTML, TrustedScript,
+or TrustedScriptURL object to certify that it has been sanitized or otherwise assured to be safe in
+the given context. Some libraries will process data and return Trusted Types objects for you. For
+example, DOMPurify_ supports Trusted Types.
 
 .. note::
    Libraries are preferred to writing your own sanitation policies since they
@@ -76,10 +76,10 @@ Types objects for you. For example, DOMPurify_ supports Trusted Types.
 
 Create Trusted Types Policies
 -----------------------------
-Where code cannot be rewritten and an existing library cannot be used, you will
-have to create Trusted Types objects yourself. This is done using policies. Different policies can be created for use in different contexts.
-Policies produce Trusted Types after enforcing security rules on their input
-based on the sink context. Each policy should be given a distinct name.
+Where code cannot be rewritten and an existing library cannot be used, you will have to create
+Trusted Types objects yourself. This is done using policies. Different policies can be created for
+use in different contexts.  Policies produce Trusted Types after enforcing security rules on their
+input based on the sink context. Each policy should be given a distinct name.
 
 Here is an example policy that sanitizes HTML by escaping the ``<`` character.
 
@@ -100,33 +100,31 @@ Here is an example of how that policy can be used.
 	el.innerHTML = escaped;
 
 .. note::
-   Keep in mind that you are creating your own security rules with policies.
-   Your application is only protected from DOM XSS if you use strict sanitation
-   rules that consider which sink is accepting the data.
+   Keep in mind that you are creating your own security rules with policies.  Your application is
+   only protected from DOM XSS if you use strict sanitation rules that consider which sink is
+   accepting the data.
 
 Use a Default Policy
 --------------------
-In the event that you don’t have control over the offending code, you can use a
-default policy. This may happen if you are loading a third party library that
-is not Trusted Types compliant. A default policy is defined the same way as any
-other Trusted Types policy. In order to be used by the browser as the default
-policy it must be named *default*.
+In the event that you don’t have control over the offending code, you can use a default policy. This
+may happen if you are loading a third party library that is not Trusted Types compliant. A default
+policy is defined the same way as any other Trusted Types policy. In order to be used by the browser
+as the default policy it must be named *default*.
 
-The policy called *default* will be used wherever a string is sent to a
-dangerous sink that requires Trusted Types.
+The policy called *default* will be used wherever a string is sent to a dangerous sink that requires
+Trusted Types.
 
 
 ``Step 3: Enforce Trusted Types``
 =================================
-Once you have addressed all of the Trusted Types violations present in your
-application, you can begin enforcing Trusted Types to prevent DOM XSS.
+Once you have addressed all of the Trusted Types violations present in your application, you can
+begin enforcing Trusted Types to prevent DOM XSS.
 
-Configure django-csp so that ``CSP_REPORT_ONLY`` is set to *False*.
+Configure django-csp to remove the report-only policy from settings, if no longer needed.
 
 .. note::
    To learn more about trusted types or learn how to limit policy creation with
-   ``CSP_TRUSTED_TYPES`` take a look at the complete spec_ or the article_ this
-   guide is based on.
+   ``trusted-types`` take a look at the complete spec_ or the article_ this guide is based on.
 
 
 
diff --git a/tox.ini b/tox.ini
index 2390c44..a5b476c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -36,7 +36,6 @@ basepython =
 
 deps =
     pytest
-    3.2.x: Django>=3.2,<3.3
     4.2.x: Django>=4.2,<4.3
     5.0.x: Django>=5.0.1,<5.1
     main: https://github.com/django/django/archive/main.tar.gz


Test patch:
diff --git a/csp/tests/settings.py b/csp/tests/settings.py
index 7c97a1d..08b51a8 100644
--- a/csp/tests/settings.py
+++ b/csp/tests/settings.py
@@ -1,8 +1,8 @@
-import django
-
-CSP_REPORT_ONLY = False
-
-CSP_INCLUDE_NONCE_IN = ["default-src"]
+CONTENT_SECURITY_POLICY = {
+    "DIRECTIVES": {
+        "include-nonce-in": ["default-src"],
+    }
+}
 
 DATABASES = {
     "default": {
@@ -39,8 +39,3 @@
         "OPTIONS": {},
     },
 ]
-
-
-# Django >1.6 requires `setup` call to initialise apps framework
-if hasattr(django, "setup"):
-    django.setup()
diff --git a/csp/tests/test_checks.py b/csp/tests/test_checks.py
new file mode 100644
index 0000000..b3f6fb0
--- /dev/null
+++ b/csp/tests/test_checks.py
@@ -0,0 +1,52 @@
+from django.test.utils import override_settings
+
+from csp.checks import check_django_csp_lt_4_0, migrate_settings
+
+
+@override_settings(
+    CSP_REPORT_PERCENTAGE=0.25,
+    CSP_EXCLUDE_URL_PREFIXES=["/admin/"],
+    CSP_REPORT_ONLY=False,
+    CSP_DEFAULT_SRC=["'self'", "example.com"],
+)
+def test_migrate_settings():
+    config, report_only = migrate_settings()
+    assert config == {
+        "REPORT_PERCENTAGE": 25,
+        "EXCLUDE_URL_PREFIXES": ["/admin/"],
+        "DIRECTIVES": {"default-src": ["'self'", "example.com"]},
+    }
+    assert report_only is False
+
+
+@override_settings(
+    CSP_REPORT_ONLY=True,
+    CSP_DEFAULT_SRC=["'self'", "example.com"],
+    CSP_SCRIPT_SRC=["'self'", "example.com", "'unsafe-inline'"],
+    CSP_INCLUDE_NONCE_IN=["script-src"],
+)
+def test_migrate_settings_report_only():
+    config, report_only = migrate_settings()
+    assert config == {
+        "DIRECTIVES": {
+            "default-src": ["'self'", "example.com"],
+            "script-src": ["'self'", "example.com", "'unsafe-inline'"],
+            "include-nonce-in": ["script-src"],
+        }
+    }
+    assert report_only is True
+
+
+@override_settings(
+    CSP_DEFAULT_SRC=["'self'", "example.com"],
+)
+def test_check_django_csp_lt_4_0():
+    errors = check_django_csp_lt_4_0(None)
+    assert len(errors) == 1
+    error = errors[0]
+    assert error.id == "csp.E001"
+    assert "update your settings to use the new format" in error.msg
+
+
+def test_check_django_csp_lt_4_0_no_config():
+    assert check_django_csp_lt_4_0(None) == []
diff --git a/csp/tests/test_contrib.py b/csp/tests/test_contrib.py
index 6b7bbc0..5c16fec 100644
--- a/csp/tests/test_contrib.py
+++ b/csp/tests/test_contrib.py
@@ -2,15 +2,15 @@
 from django.test import RequestFactory
 from django.test.utils import override_settings
 
+from csp.constants import HEADER, HEADER_REPORT_ONLY
 from csp.contrib.rate_limiting import RateLimitedCSPMiddleware
 from csp.tests.utils import response
 
-HEADER = "Content-Security-Policy"
 mw = RateLimitedCSPMiddleware(response())
 rf = RequestFactory()
 
 
-@override_settings(CSP_REPORT_PERCENTAGE=0.1, CSP_REPORT_URI="x")
+@override_settings(CONTENT_SECURITY_POLICY={"REPORT_PERCENTAGE": 10, "DIRECTIVES": {"report-uri": "x"}})
 def test_report_percentage():
     times_seen = 0
     for _ in range(5000):
@@ -21,3 +21,32 @@ def test_report_percentage():
             times_seen += 1
     # Roughly 10%
     assert 400 <= times_seen <= 600
+
+
+@override_settings(CONTENT_SECURITY_POLICY_REPORT_ONLY={"REPORT_PERCENTAGE": 10, "DIRECTIVES": {"report-uri": "x"}})
+def test_report_percentage_report_only():
+    times_seen = 0
+    for _ in range(5000):
+        request = rf.get("/")
+        response = HttpResponse()
+        mw.process_response(request, response)
+        if "report-uri" in response[HEADER_REPORT_ONLY]:
+            times_seen += 1
+    # Roughly 10%
+    assert 400 <= times_seen <= 600
+
+
+@override_settings(CONTENT_SECURITY_POLICY=None)
+def test_no_csp():
+    request = rf.get("/")
+    response = HttpResponse()
+    mw.process_response(request, response)
+    assert HEADER not in response
+
+
+@override_settings(CONTENT_SECURITY_POLICY_REPORT_ONLY=None)
+def test_no_csp_ro():
+    request = rf.get("/")
+    response = HttpResponse()
+    mw.process_response(request, response)
+    assert HEADER_REPORT_ONLY not in response
diff --git a/csp/tests/test_decorators.py b/csp/tests/test_decorators.py
index a877bc4..45e7e93 100644
--- a/csp/tests/test_decorators.py
+++ b/csp/tests/test_decorators.py
@@ -1,118 +1,298 @@
+import pytest
 from django.http import HttpResponse
 from django.test import RequestFactory
 from django.test.utils import override_settings
 
+from csp.constants import HEADER, HEADER_REPORT_ONLY
 from csp.decorators import csp, csp_exempt, csp_replace, csp_update
 from csp.middleware import CSPMiddleware
 from csp.tests.utils import response
 
-REQUEST = RequestFactory().get("/")
 mw = CSPMiddleware(response())
 
 
 def test_csp_exempt():
-    @csp_exempt
+    @csp_exempt()
     def view(request):
         return HttpResponse()
 
-    response = view(REQUEST)
-    assert response._csp_exempt
+    response = view(RequestFactory().get("/"))
+    assert response._csp_exempt is True
+    assert not hasattr(response, "_csp_exempt_ro")
 
 
-@override_settings(CSP_IMG_SRC=["foo.com"])
+def test_csp_exempt_ro():
+    @csp_exempt(REPORT_ONLY=True)
+    def view(request):
+        return HttpResponse()
+
+    response = view(RequestFactory().get("/"))
+    assert not hasattr(response, "_csp_exempt")
+    assert response._csp_exempt_ro is True
+
+
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ["foo.com"]}})
 def test_csp_update():
+    request = RequestFactory().get("/")
+
+    def view_without_decorator(request):
+        return HttpResponse()
+
+    response = view_without_decorator(request)
+    mw.process_response(request, response)
+    assert HEADER_REPORT_ONLY not in response.headers
+    policy_list = sorted(response[HEADER].split("; "))
+    assert policy_list == ["default-src 'self'", "img-src foo.com"]
+
+    @csp_update({"img-src": ["bar.com"], "include-nonce-in": ["img-src"]})
+    def view_with_decorator(request):
+        return HttpResponse()
+
+    response = view_with_decorator(request)
+    assert response._csp_update == {"img-src": ["bar.com"], "include-nonce-in": ["img-src"]}
+    mw.process_request(request)
+    assert request.csp_nonce  # Here to trigger the nonce creation.
+    mw.process_response(request, response)
+    assert HEADER_REPORT_ONLY not in response.headers
+    policy_list = sorted(response[HEADER].split("; "))
+    assert policy_list == ["default-src 'self'", f"img-src foo.com bar.com 'nonce-{request.csp_nonce}'"]
+
+    response = view_without_decorator(request)
+    mw.process_response(request, response)
+    assert HEADER_REPORT_ONLY not in response.headers
+    policy_list = sorted(response[HEADER].split("; "))
+    assert policy_list == ["default-src 'self'", "img-src foo.com"]
+
+
+@override_settings(CONTENT_SECURITY_POLICY=None, CONTENT_SECURITY_POLICY_REPORT_ONLY={"DIRECTIVES": {"img-src": ["foo.com"]}})
+def test_csp_update_ro():
+    request = RequestFactory().get("/")
+
     def view_without_decorator(request):
         return HttpResponse()
 
-    response = view_without_decorator(REQUEST)
-    mw.process_response(REQUEST, response)
-    policy_list = sorted(response["Content-Security-Policy"].split("; "))
+    response = view_without_decorator(request)
+    mw.process_response(request, response)
+    assert HEADER not in response.headers
+    policy_list = sorted(response[HEADER_REPORT_ONLY].split("; "))
     assert policy_list == ["default-src 'self'", "img-src foo.com"]
 
-    @csp_update(IMG_SRC="bar.com")
+    @csp_update({"img-src": ["bar.com"], "include-nonce-in": ["img-src"]}, REPORT_ONLY=True)
     def view_with_decorator(request):
         return HttpResponse()
 
-    response = view_with_decorator(REQUEST)
-    assert response._csp_update == {"img-src": "bar.com"}
-    mw.process_response(REQUEST, response)
-    policy_list = sorted(response["Content-Security-Policy"].split("; "))
-    assert policy_list == ["default-src 'self'", "img-src foo.com bar.com"]
+    response = view_with_decorator(request)
+    assert response._csp_update_ro == {"img-src": ["bar.com"], "include-nonce-in": ["img-src"]}
+    mw.process_request(request)
+    assert request.csp_nonce  # Here to trigger the nonce creation.
+    mw.process_response(request, response)
+    assert HEADER not in response.headers
+    policy_list = sorted(response[HEADER_REPORT_ONLY].split("; "))
+    assert policy_list == ["default-src 'self'", f"img-src foo.com bar.com 'nonce-{request.csp_nonce}'"]
 
-    response = view_without_decorator(REQUEST)
-    mw.process_response(REQUEST, response)
-    policy_list = sorted(response["Content-Security-Policy"].split("; "))
+    response = view_without_decorator(request)
+    mw.process_response(request, response)
+    assert HEADER not in response.headers
+    policy_list = sorted(response[HEADER_REPORT_ONLY].split("; "))
     assert policy_list == ["default-src 'self'", "img-src foo.com"]
 
 
-@override_settings(CSP_IMG_SRC=["foo.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ["foo.com"]}})
 def test_csp_replace():
+    request = RequestFactory().get("/")
+
     def view_without_decorator(request):
         return HttpResponse()
 
-    response = view_without_decorator(REQUEST)
-    mw.process_response(REQUEST, response)
-    policy_list = sorted(response["Content-Security-Policy"].split("; "))
+    response = view_without_decorator(request)
+    mw.process_response(request, response)
+    assert HEADER_REPORT_ONLY not in response.headers
+    policy_list = sorted(response[HEADER].split("; "))
     assert policy_list == ["default-src 'self'", "img-src foo.com"]
 
-    @csp_replace(IMG_SRC="bar.com")
+    @csp_replace({"img-src": ["bar.com"]})
     def view_with_decorator(request):
         return HttpResponse()
 
-    response = view_with_decorator(REQUEST)
-    assert response._csp_replace == {"img-src": "bar.com"}
-    mw.process_response(REQUEST, response)
-    policy_list = sorted(response["Content-Security-Policy"].split("; "))
+    response = view_with_decorator(request)
+    assert response._csp_replace == {"img-src": ["bar.com"]}
+    mw.process_response(request, response)
+    assert HEADER_REPORT_ONLY not in response.headers
+    policy_list = sorted(response[HEADER].split("; "))
     assert policy_list == ["default-src 'self'", "img-src bar.com"]
 
-    response = view_without_decorator(REQUEST)
-    mw.process_response(REQUEST, response)
-    policy_list = sorted(response["Content-Security-Policy"].split("; "))
+    response = view_without_decorator(request)
+    mw.process_response(request, response)
+    assert HEADER_REPORT_ONLY not in response.headers
+    policy_list = sorted(response[HEADER].split("; "))
     assert policy_list == ["default-src 'self'", "img-src foo.com"]
 
-    @csp_replace(IMG_SRC=None)
+    @csp_replace({"img-src": None})
     def view_removing_directive(request):
         return HttpResponse()
 
-    response = view_removing_directive(REQUEST)
-    mw.process_response(REQUEST, response)
-    policy_list = sorted(response["Content-Security-Policy"].split("; "))
+    response = view_removing_directive(request)
+    mw.process_response(request, response)
+    assert HEADER_REPORT_ONLY not in response.headers
+    policy_list = sorted(response[HEADER].split("; "))
+    assert policy_list == ["default-src 'self'"]
+
+
+@override_settings(CONTENT_SECURITY_POLICY=None, CONTENT_SECURITY_POLICY_REPORT_ONLY={"DIRECTIVES": {"img-src": ["foo.com"]}})
+def test_csp_replace_ro():
+    request = RequestFactory().get("/")
+
+    def view_without_decorator(request):
+        return HttpResponse()
+
+    response = view_without_decorator(request)
+    mw.process_response(request, response)
+    assert HEADER not in response.headers
+    policy_list = sorted(response[HEADER_REPORT_ONLY].split("; "))
+    assert policy_list == ["default-src 'self'", "img-src foo.com"]
+
+    @csp_replace({"img-src": ["bar.com"]}, REPORT_ONLY=True)
+    def view_with_decorator(request):
+        return HttpResponse()
+
+    response = view_with_decorator(request)
+    assert response._csp_replace_ro == {"img-src": ["bar.com"]}
+    mw.process_response(request, response)
+    assert HEADER not in response.headers
+    policy_list = sorted(response[HEADER_REPORT_ONLY].split("; "))
+    assert policy_list == ["default-src 'self'", "img-src bar.com"]
+
+    response = view_without_decorator(request)
+    mw.process_response(request, response)
+    assert HEADER not in response.headers
+    policy_list = sorted(response[HEADER_REPORT_ONLY].split("; "))
+    assert policy_list == ["default-src 'self'", "img-src foo.com"]
+
+    @csp_replace({"img-src": None}, REPORT_ONLY=True)
+    def view_removing_directive(request):
+        return HttpResponse()
+
+    response = view_removing_directive(request)
+    mw.process_response(request, response)
+    assert HEADER not in response.headers
+    policy_list = sorted(response[HEADER_REPORT_ONLY].split("; "))
     assert policy_list == ["default-src 'self'"]
 
 
 def test_csp():
+    request = RequestFactory().get("/")
+
     def view_without_decorator(request):
         return HttpResponse()
 
-    response = view_without_decorator(REQUEST)
-    mw.process_response(REQUEST, response)
-    policy_list = sorted(response["Content-Security-Policy"].split("; "))
+    response = view_without_decorator(request)
+    mw.process_response(request, response)
+    assert HEADER_REPORT_ONLY not in response.headers
+    policy_list = sorted(response[HEADER].split("; "))
     assert policy_list == ["default-src 'self'"]
 
-    @csp(IMG_SRC=["foo.com"], FONT_SRC=["bar.com"])
+    @csp({"img-src": ["foo.com"], "font-src": ["bar.com"]})
     def view_with_decorator(request):
         return HttpResponse()
 
-    response = view_with_decorator(REQUEST)
+    response = view_with_decorator(request)
     assert response._csp_config == {"img-src": ["foo.com"], "font-src": ["bar.com"]}
-    mw.process_response(REQUEST, response)
-    policy_list = sorted(response["Content-Security-Policy"].split("; "))
+    mw.process_response(request, response)
+    assert HEADER_REPORT_ONLY not in response.headers
+    policy_list = sorted(response[HEADER].split("; "))
+    assert policy_list == ["font-src bar.com", "img-src foo.com"]
+
+    response = view_without_decorator(request)
+    mw.process_response(request, response)
+    assert HEADER_REPORT_ONLY not in response.headers
+    policy_list = sorted(response[HEADER].split("; "))
+    assert policy_list == ["default-src 'self'"]
+
+
+def test_csp_ro():
+    request = RequestFactory().get("/")
+
+    def view_without_decorator(request):
+        return HttpResponse()
+
+    response = view_without_decorator(request)
+    mw.process_response(request, response)
+    assert HEADER_REPORT_ONLY not in response.headers
+    policy_list = sorted(response[HEADER].split("; "))
+    assert policy_list == ["default-src 'self'"]
+
+    @csp({"img-src": ["foo.com"], "font-src": ["bar.com"]}, REPORT_ONLY=True)
+    @csp({})  # CSP with no directives effectively removes the header.
+    def view_with_decorator(request):
+        return HttpResponse()
+
+    response = view_with_decorator(request)
+    assert response._csp_config_ro == {"img-src": ["foo.com"], "font-src": ["bar.com"]}
+    mw.process_response(request, response)
+    assert HEADER not in response.headers
+    policy_list = sorted(response[HEADER_REPORT_ONLY].split("; "))
     assert policy_list == ["font-src bar.com", "img-src foo.com"]
 
-    response = view_without_decorator(REQUEST)
-    mw.process_response(REQUEST, response)
-    policy_list = sorted(response["Content-Security-Policy"].split("; "))
+    response = view_without_decorator(request)
+    mw.process_response(request, response)
+    assert HEADER_REPORT_ONLY not in response.headers
+    policy_list = sorted(response[HEADER].split("; "))
     assert policy_list == ["default-src 'self'"]
 
 
 def test_csp_string_values():
     # Test backwards compatibility where values were strings
-    @csp(IMG_SRC="foo.com", FONT_SRC="bar.com")
+    request = RequestFactory().get("/")
+
+    @csp({"img-src": "foo.com", "font-src": "bar.com"})
     def view_with_decorator(request):
         return HttpResponse()
 
-    response = view_with_decorator(REQUEST)
+    response = view_with_decorator(request)
     assert response._csp_config == {"img-src": ["foo.com"], "font-src": ["bar.com"]}
-    mw.process_response(REQUEST, response)
-    policy_list = sorted(response["Content-Security-Policy"].split("; "))
+    mw.process_response(request, response)
+    policy_list = sorted(response[HEADER].split("; "))
     assert policy_list == ["font-src bar.com", "img-src foo.com"]
+
+
+# Deprecation tests
+
+
+def test_csp_exempt_error():
+    with pytest.raises(RuntimeError) as excinfo:
+
+        @csp_exempt
+        def view(request):
+            return HttpResponse()
+
+    assert "Incompatible `csp_exempt` decorator usage" in str(excinfo.value)
+
+
+def test_csp_update_error():
+    with pytest.raises(RuntimeError) as excinfo:
+
+        @csp_update(IMG_SRC="bar.com")
+        def view(request):
+            return HttpResponse()
+
+    assert "Incompatible `csp_update` decorator arguments" in str(excinfo.value)
+
+
+def test_csp_replace_error():
+    with pytest.raises(RuntimeError) as excinfo:
+
+        @csp_replace(IMG_SRC="bar.com")
+        def view(request):
+            return HttpResponse()
+
+    assert "Incompatible `csp_replace` decorator arguments" in str(excinfo.value)
+
+
+def test_csp_error():
+    with pytest.raises(RuntimeError) as excinfo:
+
+        @csp(IMG_SRC=["bar.com"])
+        def view(request):
+            return HttpResponse()
+
+    assert "Incompatible `csp` decorator arguments" in str(excinfo.value)
diff --git a/csp/tests/test_middleware.py b/csp/tests/test_middleware.py
index 742ad1c..e52971a 100644
--- a/csp/tests/test_middleware.py
+++ b/csp/tests/test_middleware.py
@@ -6,10 +6,10 @@
 from django.test import RequestFactory
 from django.test.utils import override_settings
 
+from csp.constants import HEADER, HEADER_REPORT_ONLY
 from csp.middleware import CSPMiddleware
 from csp.tests.utils import response
 
-HEADER = "Content-Security-Policy"
 mw = CSPMiddleware(response())
 rf = RequestFactory()
 
@@ -21,6 +21,18 @@ def test_add_header():
     assert HEADER in response
 
 
+@override_settings(
+    CONTENT_SECURITY_POLICY={"DIRECTIVES": {"default-src": ["example.com"]}},
+    CONTENT_SECURITY_POLICY_REPORT_ONLY={"DIRECTIVES": {"default-src": ["'self'"]}},
+)
+def test_both_headers():
+    request = rf.get("/")
+    response = HttpResponse()
+    mw.process_response(request, response)
+    assert HEADER in response
+    assert HEADER_REPORT_ONLY in response
+
+
 def test_exempt():
     request = rf.get("/")
     response = HttpResponse()
@@ -29,7 +41,7 @@ def test_exempt():
     assert HEADER not in response
 
 
-@override_settings(CSP_EXCLUDE_URL_PREFIXES=("/inlines-r-us"))
+@override_settings(CONTENT_SECURITY_POLICY={"EXCLUDE_URL_PREFIXES": ["/inlines-r-us"]})
 def text_exclude():
     request = rf.get("/inlines-r-us/foo")
     response = HttpResponse()
@@ -37,7 +49,10 @@ def text_exclude():
     assert HEADER not in response
 
 
-@override_settings(CSP_REPORT_ONLY=True)
+@override_settings(
+    CONTENT_SECURITY_POLICY=None,
+    CONTENT_SECURITY_POLICY_REPORT_ONLY={"DIRECTIVES": {"default-src": ["'self'"]}},
+)
 def test_report_only():
     request = rf.get("/")
     response = HttpResponse()
@@ -70,7 +85,7 @@ def test_use_update():
     assert response[HEADER] == "default-src 'self' example.com"
 
 
-@override_settings(CSP_IMG_SRC=["foo.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ["foo.com"]}})
 def test_use_replace():
     request = rf.get("/")
     response = HttpResponse()
@@ -130,7 +145,7 @@ def test_nonce_regenerated_on_new_request():
     assert nonce2 not in response1[HEADER]
 
 
-@override_settings(CSP_INCLUDE_NONCE_IN=[])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"include-nonce-in": []}})
 def test_no_nonce_when_disabled_by_settings():
     request = rf.get("/")
     mw.process_request(request)
diff --git a/csp/tests/test_utils.py b/csp/tests/test_utils.py
index e4e30b6..188fa3b 100644
--- a/csp/tests/test_utils.py
+++ b/csp/tests/test_utils.py
@@ -1,19 +1,13 @@
-from django.conf import settings
 from django.test.utils import override_settings
 from django.utils.functional import lazy
 
-from csp.utils import build_policy
+from csp.utils import build_policy, default_config, DEFAULT_DIRECTIVES
 
 
-def policy_eq(a, b, msg="%r != %r"):
+def policy_eq(a, b):
     parts_a = sorted(a.split("; "))
     parts_b = sorted(b.split("; "))
-    assert parts_a == parts_b, msg % (a, b)
-
-
-def test_empty_policy():
-    policy = build_policy()
-    assert "default-src 'self'" == policy
+    assert parts_a == parts_b, f"{a!r} != {b!r}"
 
 
 def literal(s):
@@ -23,121 +17,147 @@ def literal(s):
 lazy_literal = lazy(literal, str)
 
 
-@override_settings(CSP_DEFAULT_SRC=["example.com", "example2.com"])
+def test_default_config_none():
+    assert default_config(None) is None
+
+
+def test_default_config_empty():
+    # Test `default_config` with an empty dict returns defaults.
+    assert default_config({}) == DEFAULT_DIRECTIVES
+
+
+def test_default_config_drops_unknown():
+    # Test `default_config` drops unknown keys.
+    config = {"foo-src": ["example.com"]}
+    assert default_config(config) == DEFAULT_DIRECTIVES
+
+
+def test_default_config():
+    # Test `default_config` keeps config along with defaults.
+    config = {"img-src": ["example.com"]}
+    assert default_config(config) == {**DEFAULT_DIRECTIVES, **config}
+
+
+def test_empty_policy():
+    policy = build_policy()
+    policy_eq("default-src 'self'", policy)
+
+
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"default-src": ["example.com", "example2.com"]}})
 def test_default_src():
     policy = build_policy()
-    assert "default-src example.com example2.com" == policy
+    policy_eq("default-src example.com example2.com", policy)
 
 
-@override_settings(CSP_SCRIPT_SRC=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"script-src": ["example.com"]}})
 def test_script_src():
     policy = build_policy()
     policy_eq("default-src 'self'; script-src example.com", policy)
 
 
-@override_settings(CSP_SCRIPT_SRC_ATTR=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"script-src-attr": ["example.com"]}})
 def test_script_src_attr():
     policy = build_policy()
     policy_eq("default-src 'self'; script-src-attr example.com", policy)
 
 
-@override_settings(CSP_SCRIPT_SRC_ELEM=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"script-src-elem": ["example.com"]}})
 def test_script_src_elem():
     policy = build_policy()
     policy_eq("default-src 'self'; script-src-elem example.com", policy)
 
 
-@override_settings(CSP_OBJECT_SRC=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"object-src": ["example.com"]}})
 def test_object_src():
     policy = build_policy()
     policy_eq("default-src 'self'; object-src example.com", policy)
 
 
-@override_settings(CSP_PREFETCH_SRC=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"prefetch-src": ["example.com"]}})
 def test_prefetch_src():
     policy = build_policy()
     policy_eq("default-src 'self'; prefetch-src example.com", policy)
 
 
-@override_settings(CSP_STYLE_SRC=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"style-src": ["example.com"]}})
 def test_style_src():
     policy = build_policy()
     policy_eq("default-src 'self'; style-src example.com", policy)
 
 
-@override_settings(CSP_STYLE_SRC_ATTR=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"style-src-attr": ["example.com"]}})
 def test_style_src_attr():
     policy = build_policy()
     policy_eq("default-src 'self'; style-src-attr example.com", policy)
 
 
-@override_settings(CSP_STYLE_SRC_ELEM=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"style-src-elem": ["example.com"]}})
 def test_style_src_elem():
     policy = build_policy()
     policy_eq("default-src 'self'; style-src-elem example.com", policy)
 
 
-@override_settings(CSP_IMG_SRC=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ["example.com"]}})
 def test_img_src():
     policy = build_policy()
     policy_eq("default-src 'self'; img-src example.com", policy)
 
 
-@override_settings(CSP_MEDIA_SRC=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"media-src": ["example.com"]}})
 def test_media_src():
     policy = build_policy()
     policy_eq("default-src 'self'; media-src example.com", policy)
 
 
-@override_settings(CSP_FRAME_SRC=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"frame-src": ["example.com"]}})
 def test_frame_src():
     policy = build_policy()
     policy_eq("default-src 'self'; frame-src example.com", policy)
 
 
-@override_settings(CSP_FONT_SRC=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"font-src": ["example.com"]}})
 def test_font_src():
     policy = build_policy()
     policy_eq("default-src 'self'; font-src example.com", policy)
 
 
-@override_settings(CSP_CONNECT_SRC=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"connect-src": ["example.com"]}})
 def test_connect_src():
     policy = build_policy()
     policy_eq("default-src 'self'; connect-src example.com", policy)
 
 
-@override_settings(CSP_SANDBOX=["allow-scripts"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"sandbox": ["allow-scripts"]}})
 def test_sandbox():
     policy = build_policy()
     policy_eq("default-src 'self'; sandbox allow-scripts", policy)
 
 
-@override_settings(CSP_SANDBOX=[])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"sandbox": []}})
 def test_sandbox_empty():
     policy = build_policy()
     policy_eq("default-src 'self'; sandbox", policy)
 
 
-@override_settings(CSP_REPORT_URI="/foo")
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"report-uri": "/foo"}})
 def test_report_uri():
     policy = build_policy()
     policy_eq("default-src 'self'; report-uri /foo", policy)
 
 
-@override_settings(CSP_REPORT_URI=lazy_literal("/foo"))
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"report-uri": lazy_literal("/foo")}})
 def test_report_uri_lazy():
     policy = build_policy()
     policy_eq("default-src 'self'; report-uri /foo", policy)
 
 
-@override_settings(CSP_REPORT_TO="some_endpoint")
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"report-to": "some_endpoint"}})
 def test_report_to():
     policy = build_policy()
     policy_eq("default-src 'self'; report-to some_endpoint", policy)
 
 
-@override_settings(CSP_IMG_SRC=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ["example.com"]}})
 def test_update_img():
     policy = build_policy(update={"img-src": "example2.com"})
     policy_eq("default-src 'self'; img-src example.com example2.com", policy)
@@ -149,7 +169,7 @@ def test_update_missing_setting():
     policy_eq("default-src 'self'; img-src example.com", policy)
 
 
-@override_settings(CSP_IMG_SRC=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ["example.com"]}})
 def test_replace_img():
     policy = build_policy(replace={"img-src": "example2.com"})
     policy_eq("default-src 'self'; img-src example2.com", policy)
@@ -166,7 +186,7 @@ def test_config():
     policy_eq("default-src 'none'; img-src 'self'", policy)
 
 
-@override_settings(CSP_IMG_SRC=("example.com",))
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ("example.com",)}})
 def test_update_string():
     """
     GitHub issue #40 - given project settings as a tuple, and
@@ -176,7 +196,7 @@ def test_update_string():
     policy_eq("default-src 'self'; img-src example.com example2.com", policy)
 
 
-@override_settings(CSP_IMG_SRC=("example.com",))
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ("example.com",)}})
 def test_replace_string():
     """
     Demonstrate that GitHub issue #40 doesn't affect replacements
@@ -185,67 +205,67 @@ def test_replace_string():
     policy_eq("default-src 'self'; img-src example2.com", policy)
 
 
-@override_settings(CSP_FORM_ACTION=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"form-action": ["example.com"]}})
 def test_form_action():
     policy = build_policy()
     policy_eq("default-src 'self'; form-action example.com", policy)
 
 
-@override_settings(CSP_BASE_URI=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"base-uri": ["example.com"]}})
 def test_base_uri():
     policy = build_policy()
     policy_eq("default-src 'self'; base-uri example.com", policy)
 
 
-@override_settings(CSP_CHILD_SRC=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"child-src": ["example.com"]}})
 def test_child_src():
     policy = build_policy()
     policy_eq("default-src 'self'; child-src example.com", policy)
 
 
-@override_settings(CSP_FRAME_ANCESTORS=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"frame-ancestors": ["example.com"]}})
 def test_frame_ancestors():
     policy = build_policy()
     policy_eq("default-src 'self'; frame-ancestors example.com", policy)
 
 
-@override_settings(CSP_NAVIGATE_TO=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"navigate-to": ["example.com"]}})
 def test_navigate_to():
     policy = build_policy()
     policy_eq("default-src 'self'; navigate-to example.com", policy)
 
 
-@override_settings(CSP_MANIFEST_SRC=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"manifest-src": ["example.com"]}})
 def test_manifest_src():
     policy = build_policy()
     policy_eq("default-src 'self'; manifest-src example.com", policy)
 
 
-@override_settings(CSP_WORKER_SRC=["example.com"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"worker-src": ["example.com"]}})
 def test_worker_src():
     policy = build_policy()
     policy_eq("default-src 'self'; worker-src example.com", policy)
 
 
-@override_settings(CSP_PLUGIN_TYPES=["application/pdf"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"plugin-types": ["application/pdf"]}})
 def test_plugin_types():
     policy = build_policy()
     policy_eq("default-src 'self'; plugin-types application/pdf", policy)
 
 
-@override_settings(CSP_REQUIRE_SRI_FOR=["script"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"require-sri-for": ["script"]}})
 def test_require_sri_for():
     policy = build_policy()
     policy_eq("default-src 'self'; require-sri-for script", policy)
 
 
-@override_settings(CSP_REQUIRE_TRUSTED_TYPES_FOR=["'script'"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"require-trusted-types-for": ["'script'"]}})
 def test_require_trusted_types_for():
     policy = build_policy()
     policy_eq("default-src 'self'; require-trusted-types-for 'script'", policy)
 
 
-@override_settings(CSP_TRUSTED_TYPES=["strictPolicy", "laxPolicy", "default", "'allow-duplicates'"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"trusted-types": ["strictPolicy", "laxPolicy", "default", "'allow-duplicates'"]}})
 def test_trusted_types():
     policy = build_policy()
     policy_eq(
@@ -254,13 +274,13 @@ def test_trusted_types():
     )
 
 
-@override_settings(CSP_UPGRADE_INSECURE_REQUESTS=True)
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"upgrade-insecure-requests": True}})
 def test_upgrade_insecure_requests():
     policy = build_policy()
     policy_eq("default-src 'self'; upgrade-insecure-requests", policy)
 
 
-@override_settings(CSP_BLOCK_ALL_MIXED_CONTENT=True)
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"block-all-mixed-content": True}})
 def test_block_all_mixed_content():
     policy = build_policy()
     policy_eq("default-src 'self'; block-all-mixed-content", policy)
@@ -271,7 +291,7 @@ def test_nonce():
     policy_eq("default-src 'self' 'nonce-abc123'", policy)
 
 
-@override_settings(CSP_INCLUDE_NONCE_IN=["script-src", "style-src"])
+@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"include-nonce-in": ["script-src", "style-src"]}})
 def test_nonce_include_in():
     policy = build_policy(nonce="abc123")
     policy_eq(
@@ -280,8 +300,16 @@ def test_nonce_include_in():
     )
 
 
-@override_settings()
 def test_nonce_include_in_absent():
-    del settings.CSP_INCLUDE_NONCE_IN
     policy = build_policy(nonce="abc123")
     policy_eq("default-src 'self' 'nonce-abc123'", policy)
+
+
+def test_boolean_directives():
+    for directive in ["upgrade-insecure-requests", "block-all-mixed-content"]:
+        with override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {directive: True}}):
+            policy = build_policy()
+            policy_eq(f"default-src 'self'; {directive}", policy)
+        with override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {directive: False}}):
+            policy = build_policy()
+            policy_eq("default-src 'self'", policy)