Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions CSP_VERIFICATION_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Django 6.0 CSP Security Implementation - Verification Guide

## Summary of Changes

### Step 4: Middleware Configuration ✅
- **File**: `dejacode/settings.py`
- **Change**: Added `django.middleware.security.ContentSecurityPolicyMiddleware` after `SecurityMiddleware`
- **Location**: Line 176
- **Status**: ✅ Complete

### Step 5: CSP Dictionary Configuration ✅
- **File**: `dejacode/settings.py`
- **Changes**:
- Imported CSP utility: `from django.utils.csp import CSP` (Line 18)
- Added CSP configuration starting at line 205
- Set `SECURE_CSP_REPORT_ONLY = True` for initial audit phase
- Configured CSP directives:
- `default-src`: `[CSP.SELF]` - Only allow same-origin content by default
- `script-src`: Allows self, nonces, and CloudFront CDN
- `style-src`: Allows self, Google Fonts, and CloudFront CDN
- `img-src`: Allows self, data URIs, and HTTPS sources
- `connect-src`: Allows self (for API calls to PurlDB/VulnerableCode)
- **Status**: ✅ Complete

### Step 6: Template Updates with Nonce Support ✅
- **Method**: Automated Python script (`add_nonces_to_templates.py`)
- **Results**:
- Processed: 254 HTML template files
- Updated: 52 files with nonce attributes
- Pattern Applied: `<script nonce="{{ request.csp_nonce }}">`

**Key Templates Updated**:
- `dje/templates/bootstrap_base_js.html` - Base JavaScript template with inline client data
- `component_catalog/templates/` - Multiple component catalog templates
- `product_portfolio/templates/` - Product portfolio templates
- `workflow/templates/` - Workflow templates
- And 48 other template files with inline scripts

### Step 7: Verification Procedure

When Docker is available on your system, follow these steps to verify the CSP implementation:

#### Build and Run
```bash
cd path/to/dejacode
docker compose up --build
```

#### Verify Headers in Browser
1. Open your browser's Developer Tools (F12)
2. Navigate to Network tab
3. Refresh the page and click the main document
4. In the Response Headers section, look for:
- `Content-Security-Policy-Report-Only` (in Report-Only mode)
- Should show the CSP directives we configured

#### Sample Expected Header
```
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-XXXXX' https://cdnjs.cloudflare.com; style-src 'self' https://fonts.googleapis.com https://cdnjs.cloudflare.com; img-src 'self' data: https:; connect-src 'self'
```

#### Check Browser Console
1. Open the Console tab in DevTools
2. Look for CSP violation reports (should be minimal if nonces are working)
3. Any blocked resources will appear here

#### Monitor CSP Violations
- CSP violations are logged in Report-Only mode
- Review the console for any blocked resources
- Add additional domains to `SECURE_CSP` if needed:
```python
SECURE_CSP["script-src"].append("https://additional-domain.com")
```

#### Transitioning to Enforced CSP
Once you've verified all required domains:
1. Change `SECURE_CSP_REPORT_ONLY = False` in settings.py
2. Redeploy and monitor for any CSP violations
3. If violations occur, add the missing domains and redeploy

## Security Benefits

✅ **XSS Protection**: Inline script execution is restricted to those with valid nonces
✅ **Unauthorized Content Blocking**: External scripts/styles from non-whitelisted origins are blocked
✅ **Audit Trail**: Report-Only mode allows testing without breaking functionality
✅ **CSP Nonce Support**: Django 6.0's automatic nonce generation for each request

## Files Modified

| File | Changes |
|------|---------|
| `dejacode/settings.py` | Added CSP import, middleware, and configuration |
| `dje/templates/bootstrap_base_js.html` | Added nonce to inline script |
| 51 additional template files | Added nonce attributes to inline scripts |

## Notes for Production

1. **Report-Only Phase**: Keep `SECURE_CSP_REPORT_ONLY = True` initially to identify all required domains
2. **Monitoring**: Monitor browser console and CSP reports during testing
3. **Domain Whitelist**: Review and validate all whitelisted domains for security
4. **Performance**: CSP has minimal performance impact
5. **Browser Support**: CSP is supported in all modern browsers

## Troubleshooting

**Issue**: CSP violations in console
**Solution**: Check the Resource Name and add missing domain to appropriate CSP directive

**Issue**: Inline scripts not executing
**Solution**: Verify nonce is present in template `{{ request.csp_nonce }}`

**Issue**: Inline styles not applying
**Solution**: Verify CSS files are externalized or domains are whitelisted in `style-src`

## References

- Django 6.0 Security Features: https://docs.djangoproject.com/en/6.0/topics/security/
- Content Security Policy Guide: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
- CSP Nonce: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
61 changes: 61 additions & 0 deletions add_nonces_to_templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env python
"""
Helper script to add nonce attributes to all inline <script> tags in HTML templates.
This ensures CSP compliance for inline scripts in DejaCode.

Usage: python add_nonces_to_templates.py
"""

import re
from pathlib import Path

def add_nonce_to_scripts(file_path):
"""
Add nonce="{{ request.csp_nonce }}" attribute to inline <script> tags.
Skips script tags that already have nonce attributes or have src attributes.
"""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()

original_content = content

# Pattern to match <script> tags without src attribute and without nonce
# This matches: <script> but not <script src="..." or <script nonce="..."
pattern = r'<script(?![^>]*(?:src|nonce)[^>]*?)>'

# Replace with <script nonce="{{ request.csp_nonce }}">
updated_content = re.sub(
pattern,
'<script nonce="{{ request.csp_nonce }}">',
content
)

# Only write if changes were made
if updated_content != original_content:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(updated_content)
return True
return False

def main():
"""Find and update all HTML template files with inline scripts."""

# Find all HTML files in templates directories
templates_dir = Path('.')
html_files = list(templates_dir.glob('**/templates/**/*.html'))

updated_count = 0
total_count = 0

for html_file in sorted(html_files):
total_count += 1
if add_nonce_to_scripts(html_file):
updated_count += 1
print(f"✓ Updated: {html_file}")
else:
print(f" Skipped: {html_file}")

print(f"\n✓ Processed {total_count} files, updated {updated_count} files with nonce attributes")

if __name__ == '__main__':
main()
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ <h2>The following {% trans opts.verbose_name_plural %} will be added to the prod

{% block javascripts %}
{{ block.super }}
<script>
<script nonce="{{ request.csp_nonce }}">
(function($) {
document.addEventListener('DOMContentLoaded', function () {
var api_url = "{% url 'api_v2:product-list' %}";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script>
<script nonce="{{ request.csp_nonce }}">
(function($) {
document.addEventListener('DOMContentLoaded', function () {
// the `form` is only available if not perms_lacking and not protected
Expand Down
2 changes: 1 addition & 1 deletion component_catalog/templates/admin/set_policy_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

{% block javascripts %}
{{ block.super }}
<script>
<script nonce="{{ request.csp_nonce }}">
(function ($) {
document.addEventListener('DOMContentLoaded', function () {
$('#action-toggle').on('change', function(e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<script src="{% static 'json-viewer/jquery.json-viewer-1.4.0.js' %}" integrity="sha384-mCd7P/7rxz1zpQAb195/BFZG4pDkLO6GdkRi772EZRiLTGdfnlhC74NrrwtSHvBI" crossorigin="anonymous"></script>
{% include 'includes/dependencies-json-viewer.js.html' %}
{% if open_add_to_package_modal %}
<script>
<script nonce="{{ request.csp_nonce }}">
document.addEventListener('DOMContentLoaded', function () {
$('#add-to-product-modal').modal('show');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
{% include 'component_catalog/includes/add_to.js.html' %}
{% endif %}

<script>
<script nonce="{{ request.csp_nonce }}">
$(document).ready(function () {
let download_aboutcode_btn = $('#download-aboutcode-files');
let download_aboutcode_wrapper = download_aboutcode_btn.parent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ <h5 class="modal-title">Add Package</h5>
</div>

<script src="{% static 'js/csrf_header.js' %}" integrity="sha384-H61e46QMjASwnZFb/rwCl9PANtdqt1dbKU8gnGOh9lIGQEoi1B6qkWROHnrktD3R" crossorigin="anonymous"></script>
<script>
<script nonce="{{ request.csp_nonce }}">
document.addEventListener('DOMContentLoaded', function () {
$('#add-package-modal').on('shown.bs.modal', function () {
$("#download-urls").focus();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script>
<script nonce="{{ request.csp_nonce }}">
document.addEventListener('DOMContentLoaded', function () {
let add_to_btn = document.getElementById('add-to-btn');
let add_to_btn_wrapper = add_to_btn.parentElement;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ <h5 class="modal-title">
</div>
</div>

<script>
<script nonce="{{ request.csp_nonce }}">
document.getElementById('scan-matches-modal').addEventListener('show.bs.modal', function(event) {
let modal = event.target;
let button = event.relatedTarget;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ <h5 class="modal-title">Set values from Scan Summary to Package</h5>
</div>
</div>

<script>
<script nonce="{{ request.csp_nonce }}">
const ScanSummarylicenseExpressionInputIds = [
'id_scan-summary-to-package-license_expression',
'id_scan-summary-to-package-declared_license_expression',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ <h5 class="modal-title">Set values from Scan to Package</h5>
</div>
</div>

<script>
<script nonce="{{ request.csp_nonce }}">
const ScanToPackagelicenseExpressionInputIds = [
'id_scan-to-package-license_expression',
'id_scan-to-package-declared_license_expression',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

{% block javascripts %}
{{ block.super }}
<script>
<script nonce="{{ request.csp_nonce }}">
document.addEventListener('DOMContentLoaded', function () {
function build_purl(type, namespace, name, version, qualifiers, subpath) {
if (!type || !name) return '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

{% block javascripts %}
{{ block.super }}
<script>
<script nonce="{{ request.csp_nonce }}">
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.scan_delete_link').forEach(link => {
link.addEventListener('click', function() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ <h5 class="modal-title"></h5>
{% if forloop.last %}</dl>{% endif %}

<script src="{% static 'js/jquery.mark-8.11.1.min.js' %}" integrity="sha384-iqnguDoMujGknA4B5Jk7pbSn7sb7M8Tc0zVsTNQXm629Xx00jGEpD9TsZXbfNjKO" crossorigin="anonymous"></script>
<script>
<script nonce="{{ request.csp_nonce }}">
$('#key-files-modal').on('show.bs.modal', function (event) {
let button = $(event.relatedTarget);
let content = button.data('content');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</div>
</div>
{% include 'component_catalog/includes/scan_package_modal.html' %}
<script>
<script nonce="{{ request.csp_nonce }}">
document.querySelector('a#submit-scan-request').addEventListener('click', function() {
NEXB.displayOverlay("Submitting Scan Request...");
});
Expand All @@ -30,7 +30,7 @@
{% endif %}
{% include 'component_catalog/modals/scan_delete_modal.html' %}
{% include 'component_catalog/modals/scan_refresh_modal.html' %}
<script>
<script nonce="{{ request.csp_nonce }}">
document.querySelectorAll('.scan_delete_link').forEach(link => {
link.addEventListener('click', function() {
let deleteUrl = this.getAttribute('data-delete-url');
Expand Down
22 changes: 22 additions & 0 deletions dejacode/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import ldap
from django_auth_ldap.config import GroupOfNamesType
from django_auth_ldap.config import LDAPSearch
from django.utils.csp import CSP

# The home directory of the dejacode user that owns the installation.
PROJECT_DIR = environ.Path(__file__) - 1
Expand Down Expand Up @@ -172,6 +173,7 @@ def gettext_noop(s):
# read or write the response body so that compression happens afterward.
"django.middleware.gzip.GZipMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.middleware.security.ContentSecurityPolicyMiddleware", # New in Django 6.0
"dje.middleware.ProhibitInQueryStringMiddleware",
"django.middleware.http.ConditionalGetMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
Expand Down Expand Up @@ -199,6 +201,26 @@ def gettext_noop(s):
"SECURE_CROSS_ORIGIN_OPENER_POLICY", default="same-origin"
)

# Content Security Policy (CSP) - Django 6.0
# Initially set to Report Only to identify all required domains
SECURE_CSP_REPORT_ONLY = True

SECURE_CSP = {
"default-src": [CSP.SELF],
"script-src": [
CSP.SELF,
CSP.NONCE, # Crucial for inline scripts in DejaCode templates
"https://cdnjs.cloudflare.com", # Used for external JS libraries
],
"style-src": [
CSP.SELF,
"https://fonts.googleapis.com",
"https://cdnjs.cloudflare.com",
],
"img-src": [CSP.SELF, "data:", "https:"],
"connect-src": [CSP.SELF], # Allows API calls to PurlDB/VulnerableCode
}

X_FRAME_OPTIONS = "DENY"
# Note: The CSRF_COOKIE_HTTPONLY cannot be activated yet without breaking all
# the AJAX (POST, PUT, etc..) requests, like the annotation system for example.
Expand Down
2 changes: 1 addition & 1 deletion dje/templates/account/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ <h5 class="modal-title">Regenerate API key</h5>
{% endblock %}

{% block javascripts %}
<script>
<script nonce="{{ request.csp_nonce }}">
document.addEventListener('DOMContentLoaded', function () {
var showApiKey = document.getElementById('show_api_key');
var apiKeyField = document.getElementById('api_key');
Expand Down
2 changes: 1 addition & 1 deletion dje/templates/admin/base_site.html
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ <h1 id="grp-admin-title">
{% block javascripts %}
{{ block.super }}
{{ client_data|json_script:"client_data" }}
<script>
<script nonce="{{ request.csp_nonce }}">
NEXB = {};
NEXB.client_data = JSON.parse(document.getElementById("client_data").textContent);

Expand Down
2 changes: 1 addition & 1 deletion dje/templates/admin/docs/models.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ <h4>Long description:</h4>
</div>
</div>

<script>
<script nonce="{{ request.csp_nonce }}">
document.addEventListener('DOMContentLoaded', function () {
// Turns the menu into an accordion-like menu to avoid overflow issues
$('#menu-content').on('show.bs.collapse', function () {
Expand Down
2 changes: 1 addition & 1 deletion dje/templates/admin/includes/activity_log_dialog.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</form>
</div>

<script>
<script nonce="{{ request.csp_nonce }}">
(function($){
$(document).ready(function(){
$("#activity_log_dialog").dialog({
Expand Down
2 changes: 1 addition & 1 deletion dje/templates/admin/includes/search_help_dialog.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
</p>
</div>

<script>
<script nonce="{{ request.csp_nonce }}">
(function($){
$(document).ready(function(){
$("#search_help_dialog").dialog({
Expand Down
Loading