Update to Django 6 + patch of 2 important vuln#566
Conversation
Mortinat
commented
Mar 6, 2026
- Bump version of all lib use to latest one
- removed django_nyt (seems vestige of the past + not django 6 compatible)
- Bump all docker image too
- Fixed a xss vuln in the wiki page, using nh3 sanitizer (https://github.com/messense/nh3)
- Fixed a open redirect vuln on the login url
- Patch CVE-2024-42005, CVE-2024-38875, CVE-2024-39329, CVE-2024-39330, CVE-2024-39614, CVE-2024-45230, CVE-2024-45231, CVE-2025-48432
- Use python 3.14 instead of 3.10
- docker compose instead of docker-compose (might need to update package on the host before)
There was a problem hiding this comment.
Pull request overview
This PR upgrades the project’s runtime stack (Python/Django + Docker images) while also addressing web security issues (XSS sanitization for rendered Markdown and an open-redirect fix on login).
Changes:
- Upgrade to Django 6.x, Python 3.14, and bump multiple dependencies/images (uv lockfile, Docker, CI workflows).
- Replace prior Markdown “safe_mode” usage with
nh3-based HTML sanitization for wiki/flatpages/email content. - Harden authentication/UX flows by validating
nextURLs on login and enforcing POST-only for certain profile actions.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
pyproject.toml |
Updates Python/Django requirements and adds nh3. |
uv.lock |
Refreshes the full dependency lock to match new runtime/deps. |
Dockerfile |
Moves base image to Python 3.14. |
.github/workflows/push.yml |
Updates CI to run on Python 3.14. |
.github/workflows/pull_request.yml |
Updates PR checks to run on Python 3.14. |
docker-compose.yml |
Bumps Redis/Postgres images (notably Postgres 12 → 17). |
upgrade_postgres.sh |
Adds a scripted Postgres 12 → 17 migration workflow. |
backup_db.sh |
Updates DB backup to use docker compose and new dump method. |
users/views.py |
Fixes open redirect on login; requires POST for pamela show/hide actions. |
incubator/templatetags/formatting.py |
Centralizes Markdown rendering + sanitization via nh3. |
manmail/models.py |
Sanitizes newsletter HTML output via nh3. |
incubator/settings.py |
Adds SecurityMiddleware + production security settings; updates/cleans config. |
incubator/urls.py |
Removes django_nyt notifications route and reformats URL patterns. |
incubator/apiurls.py |
Replaces deprecated url() usage with re_path(). |
projects/migrations/0008_alter_project_dependencies.py |
Adds a migration to align the dependencies field definition. |
nginx/Dockerfile |
Bumps nginx base image version. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Security settings (applied in production when DEBUG=False) | ||
| if not DEBUG: | ||
| SECURE_SSL_REDIRECT = True | ||
| SECURE_HSTS_SECONDS = 31536000 # 1 year | ||
| SECURE_HSTS_INCLUDE_SUBDOMAINS = True | ||
| SECURE_HSTS_PRELOAD = True | ||
| SESSION_COOKIE_SECURE = True | ||
| CSRF_COOKIE_SECURE = True | ||
| SECURE_CONTENT_TYPE_NOSNIFF = True |
There was a problem hiding this comment.
Enabling SECURE_SSL_REDIRECT = True in production can cause redirect loops when the app is behind a reverse proxy (e.g. Traefik/nginx) unless Django is configured to trust the forwarded proto. Consider adding SECURE_PROXY_SSL_HEADER (and ensuring X-Forwarded-Proto is forwarded to Django) so request.is_secure() is correct in production.
| # echo "[1/6] Dumping database from PostgreSQL 12..." | ||
| # docker compose exec -T db pg_dumpall -U "$DB_USER" > "$DUMP_FILE" | ||
| # DUMP_SIZE=$(du -h "$DUMP_FILE" | cut -f1) | ||
| # echo " Dump complete: $DUMP_FILE ($DUMP_SIZE)" |
There was a problem hiding this comment.
The dump step is commented out, but the script later restores from $DUMP_FILE. With set -euo pipefail, this will fail because the dump file is never created. Either re-enable the dump step or change the script so restore only runs after a successful dump (and consider failing fast if the dump file is missing).
| echo "[6/6] Restoring dump into PostgreSQL 17..." | ||
| docker compose exec -T db psql -U "$DB_USER" -d "$DB_NAME" < "$DUMP_FILE" |
There was a problem hiding this comment.
pg_dumpall output typically contains cluster-level statements (roles, CREATE DATABASE, \connect, etc.). Restoring it by piping into psql -d "$DB_NAME" can lead to errors (e.g. database already exists) and may partially restore without failing the script. Consider using pg_dump/pg_restore for the single DB, or ensure restore runs with ON_ERROR_STOP and targets the right scope (globals vs db dump).
| mkdir -p /home/incubator/dumps/$(date +"%y-%m") | ||
| docker-compose -f /home/incubator/docker/apps/incubator/docker-compose.yml exec db /usr/local/bin/pg_dump --dbname="incubator_db" --username="incubator" -f /backup.dump | ||
| docker cp incubator_db_1:/backup.dump /home/incubator/dumps/$(date +"%y-%m")/$(date +"%y-%m-%d").dump | ||
| docker compose -f /home/incubator/docker/apps/incubator/docker-compose.yml exec -T db pg_dumpall --username="incubator" > /home/incubator/dumps/$(date +"%y-%m")/$(date +"%y-%m-%d").sql |
There was a problem hiding this comment.
This switches backups from pg_dump (single DB) to pg_dumpall (entire cluster). pg_dumpall often requires superuser permissions and produces a plain SQL script that may be harder to restore selectively. If the intent is to back up only incubator_db, consider using pg_dump (optionally in custom format) instead of pg_dumpall.
| docker compose -f /home/incubator/docker/apps/incubator/docker-compose.yml exec -T db pg_dumpall --username="incubator" > /home/incubator/dumps/$(date +"%y-%m")/$(date +"%y-%m-%d").sql | |
| docker compose -f /home/incubator/docker/apps/incubator/docker-compose.yml exec -T db pg_dump --username="incubator" --dbname="incubator_db" > /home/incubator/dumps/$(date +"%y-%m")/$(date +"%y-%m-%d").sql |
| return SafeText(html) | ||
| @register.filter(is_safe=False, name="unsafeMarkdown") | ||
| @stringfilter | ||
| def unsafe_markdown(value): |
There was a problem hiding this comment.
The unsafeMarkdown filter now uses the same sanitized rendering path as the markdown filter. This makes the name misleading (templates like flatpages/default.html still use unsafeMarkdown expecting less-restricted output). Consider renaming the filter (and updating templates) or adding an explicit comment/docstring explaining that unsafeMarkdown is intentionally sanitized now.
| def unsafe_markdown(value): | |
| def unsafe_markdown(value): | |
| """ | |
| Render markdown to sanitized HTML. | |
| Despite the name, this filter uses the same sanitized rendering path as the | |
| ``markdown`` filter via ``_render_markdown``. It is retained for backwards | |
| compatibility with existing templates that refer to ``unsafeMarkdown``, and | |
| does not intentionally bypass HTML sanitization. | |
| """ |
| DEFAULT_FILE_STORAGE = { | ||
| "default": { | ||
| "BACKEND": "django.core.files.storage.FileSystemStorage", | ||
| }, | ||
| } |
There was a problem hiding this comment.
DEFAULT_FILE_STORAGE is being set to a dict, but Django expects DEFAULT_FILE_STORAGE to be a string setting (and on newer Django versions, storage backends are configured via STORAGES). As-is, this will likely break file storage initialization. Consider replacing this with STORAGES = {"default": {"BACKEND": ...}} (or set DEFAULT_FILE_STORAGE to the backend path string if targeting older Django).