From b61b5e4186ad26d7caf67e905d4f611e373c93ca Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Fri, 20 Feb 2026 09:22:39 -0600 Subject: [PATCH 1/3] Fix CodeQL security alerts across the codebase - Add permissions: contents: read to CI and static workflows - Use sandboxed Django template Engine for mailing templates (SSTI) - Sanitize file paths in fix_success_story_images command (path injection) - Validate redirect URL path in MediaMigrationView (open redirect) - Use textContent instead of innerHTML in font demo (DOM XSS) - Validate image src URLs in sponsor application form (DOM XSS) - Validate select value is relative URL in event detail (DOM XSS) - Dismiss SHA1 alert as won't-fix (PyCon API requirement) Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 3 +++ .github/workflows/static.yml | 3 +++ .../events/templates/events/event_detail.html | 5 +++- apps/mailing/forms.py | 4 ++-- apps/mailing/models.py | 15 ++++++++---- .../commands/fix_success_story_images.py | 19 ++++++++++----- .../create_pycon_vouchers_for_sponsors.py | 2 +- pydotorg/views.py | 2 ++ static/fonts/demo/demo.js | 2 +- static/js/sponsors/applicationForm.js | 24 ++++++++++++++++--- 10 files changed, 60 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90d53ef00..1e0a70a2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,9 @@ name: CI on: [push, pull_request, workflow_dispatch] +permissions: + contents: read + jobs: migrations: if: github.event_name != 'push' || github.event.repository.fork == true || github.ref == 'refs/heads/main' diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index d82cae171..d221d3a47 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -2,6 +2,9 @@ name: Check collectstatic on: [push, pull_request, workflow_dispatch] +permissions: + contents: read + jobs: collectstatic: if: github.event_name != 'push' || github.event.repository.fork == true || github.ref == 'refs/heads/main' diff --git a/apps/events/templates/events/event_detail.html b/apps/events/templates/events/event_detail.html index 80773bd6b..ad71340a4 100644 --- a/apps/events/templates/events/event_detail.html +++ b/apps/events/templates/events/event_detail.html @@ -184,7 +184,10 @@

More events in b.getAttribute('data-benefit-id') == benefitId)[0]; hiddenInput.checked = !hiddenInput.checked; - clickedImg.src = newSrc; + if (isSafeImageSrc(newSrc)) { + clickedImg.src = newSrc; + } // Check if there are any type of customization. If so, display custom-fee label. let pkgBenefits = Array(...document.getElementById(`package_benefits_${packageId}`).children).map(div => div.textContent).sort(); From b59161e0de2f50f397eaa64cea347b368c45047d Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Fri, 20 Feb 2026 09:26:31 -0600 Subject: [PATCH 2/3] Address CodeQL review comments on fix_success_story_images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use Django storage API (ContentFile + ImageField.save) instead of raw file I/O to eliminate path injection taint chain. Dismiss remaining alerts as false positives — data originates from server-rendered Django templates, not user input. Co-Authored-By: Claude Opus 4.6 --- .../commands/fix_success_story_images.py | 49 +++++-------------- 1 file changed, 12 insertions(+), 37 deletions(-) diff --git a/apps/pages/management/commands/fix_success_story_images.py b/apps/pages/management/commands/fix_success_story_images.py index 70b598bc0..a0f05dd6b 100644 --- a/apps/pages/management/commands/fix_success_story_images.py +++ b/apps/pages/management/commands/fix_success_story_images.py @@ -1,13 +1,12 @@ import re -from pathlib import Path, PurePosixPath +from pathlib import PurePosixPath from urllib.parse import urlparse import requests -from django.conf import settings -from django.core.files import File +from django.core.files.base import ContentFile from django.core.management.base import BaseCommand -from apps.pages.models import Image, Page, page_image_path +from apps.pages.models import Image, Page HTTP_OK = 200 @@ -18,14 +17,6 @@ class Command(BaseCommand): def get_success_pages(self): return Page.objects.filter(path__startswith="about/success/") - def image_url(self, path): - """ - Given a full filesystem path to an image, return the proper media - url for it - """ - new_url = path.replace(settings.MEDIA_ROOT, settings.MEDIA_URL) - return new_url.replace("//", "/") - def fix_image(self, path, page): url = f"http://legacy.python.org{path}" # Retrieve the image @@ -34,34 +25,18 @@ def fix_image(self, path, page): if r.status_code != HTTP_OK: return None - # Create new associated image and generate ultimate path - img = Image() - img.page = page - - # Sanitize filename from URL to prevent path traversal - parsed_name = PurePosixPath(urlparse(url).path).name - filename = Path(parsed_name).name # strip any remaining path components - output_path = page_image_path(img, filename) - - # Ensure output stays within MEDIA_ROOT - resolved = Path(output_path).resolve() - media_root = Path(settings.MEDIA_ROOT).resolve() - if not str(resolved).startswith(str(media_root)): + # Extract and validate filename (alphanumeric, hyphens, dots only) + raw_name = PurePosixPath(urlparse(url).path).name + filename = re.sub(r"[^\w.\-]", "_", raw_name) + if not filename or filename.startswith("."): return None - # Make sure our directories exist - resolved.parent.mkdir(parents=True, exist_ok=True) - - # Write image data to our location - with resolved.open("wb") as f: - f.write(r.content) - - # Re-open the image as a Django File object - with resolved.open("rb") as reopen: - new_file = File(reopen) - img.image.save(filename, new_file, save=True) + # Use Django's storage API to safely write the file + img = Image() + img.page = page + img.image.save(filename, ContentFile(r.content), save=True) - return self.image_url(output_path) + return img.image.url def find_image_paths(self, page): content = page.content.raw From 9337d654862bbee4e6241fa6f4c29b17f50deff0 Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Fri, 20 Feb 2026 09:35:52 -0600 Subject: [PATCH 3/3] Address PR review feedback - Use permissions: {} per Hugo's suggestion (cherry-picker pattern) - Fix protocol-relative URL bypass (//evil.com) in event detail and isSafeImageSrc - Use PurePosixPath + part filtering for robust path traversal prevention in MediaMigrationView - var -> let in JS Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 3 +-- .github/workflows/static.yml | 3 +-- apps/events/templates/events/event_detail.html | 4 ++-- pydotorg/views.py | 10 ++++++---- static/js/sponsors/applicationForm.js | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e0a70a2a..905a6c200 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,7 @@ name: CI on: [push, pull_request, workflow_dispatch] -permissions: - contents: read +permissions: {} jobs: migrations: diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index d221d3a47..edb234a97 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -2,8 +2,7 @@ name: Check collectstatic on: [push, pull_request, workflow_dispatch] -permissions: - contents: read +permissions: {} jobs: collectstatic: diff --git a/apps/events/templates/events/event_detail.html b/apps/events/templates/events/event_detail.html index ad71340a4..35dcdd55a 100644 --- a/apps/events/templates/events/event_detail.html +++ b/apps/events/templates/events/event_detail.html @@ -184,8 +184,8 @@

More events in