Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d7d16c5
Security: Fix insecure defaults, enable CSRF protection, implement CS…
claude Dec 27, 2025
cad5ffb
Security: Add CSRF endpoint, improve logging, strengthen rate limitin…
claude Dec 27, 2025
a0a7596
Security: Add file upload validation, strengthen chat rate limiting, …
claude Dec 27, 2025
4c211e5
Security: Strengthen export rate limiting and remove production stack…
claude Dec 27, 2025
a7700af
Security: Add JSON schema validation for critical model fields
claude Dec 27, 2025
d191626
Security: Implement encrypted client-side API key storage using Web C…
claude Dec 27, 2025
a72428f
Security: Update .env.example with production security requirements
claude Dec 27, 2025
548f0f3
Feature: Add automated file cleanup system for Render deployment
claude Dec 27, 2025
ce1dab9
Refactor: Simplify file cleanup for Render free tier (remove manageme…
claude Dec 27, 2025
74a3f9b
Docs: Update GitHub Actions workflow comments to reflect current impl…
claude Dec 27, 2025
be278e1
Delete project/.github/workflows/cleanup_files.yml
RETR0-OS Dec 27, 2025
8c1fd69
Add GitHub Actions workflow for file cleanup
RETR0-OS Dec 27, 2025
5579d50
Add cleanup-uploads workflow configuration
RETR0-OS Dec 27, 2025
045b0aa
Change cleanup workflow schedule to every 5 hours
RETR0-OS Dec 27, 2025
50fe207
Delete project/CLEANUP_SETUP.md
RETR0-OS Dec 27, 2025
11be5a8
Delete project/render.yaml
RETR0-OS Dec 27, 2025
3673b88
Refactor Gemini and TensorFlow services to use updated SDKs; enhance …
RETR0-OS Dec 27, 2025
5cfeb3b
Merge pull request #45 from ForgeOpus/claude/security-audit-deploy-amiDI
RETR0-OS Dec 27, 2025
0d0aa20
Merge branch 'deploy' of https://github.com/ForgeOpus/visionforge int…
RETR0-OS Dec 28, 2025
1cd005a
Potential fix for code scanning alert no. 21: Code injection
RETR0-OS Dec 28, 2025
43d61bd
Initial plan
Copilot Dec 28, 2025
2362395
Fix path traversal and XSS security vulnerabilities
Copilot Dec 28, 2025
0a6250b
Improve filename sanitization to preserve valid filenames
Copilot Dec 28, 2025
c179d1a
Add security fixes documentation
Copilot Dec 28, 2025
a6b0ade
Fix timing attack and information disclosure vulnerabilities
Copilot Dec 28, 2025
38abc48
Update security documentation with all vulnerabilities fixed
Copilot Dec 28, 2025
5d39626
Delete SECURITY_FIXES.md
RETR0-OS Dec 28, 2025
4a0a9dc
Merge pull request #48 from ForgeOpus/copilot/fix-security-alerts-codeql
RETR0-OS Dec 28, 2025
1862b00
fix for code scanning alert no. 27: Information exposure through an e…
RETR0-OS Dec 28, 2025
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
23 changes: 23 additions & 0 deletions .github/workflows/cleanup-uploads.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# GitHub Actions workflow to clean up uploaded files on Render
# This runs every 5 hours
# The workflow calls the cleanup API endpoint

name: Cleanup Uploaded Files

on:
schedule:
# Run every 5 hours
- cron: '0 */5 * * *'
workflow_dispatch: # Allow manual triggering

jobs:
cleanup:
runs-on: ubuntu-latest

steps:
- name: Trigger cleanup endpoint
run: |
curl -X POST \
-H "Content-Type: application/json" \
"${{ secrets.CLEANUP_ENDPOINT_URL }}" \
-d '{"secret": "${{ secrets.CLEANUP_SECRET }}"}'
Comment on lines +20 to +23
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GitHub Actions workflow secret CLEANUP_ENDPOINT_URL is used directly without validation. If this URL is misconfigured or points to an unintended endpoint, the cleanup secret could be exposed to a third party. Add validation in the workflow or documentation to ensure the URL must be an HTTPS endpoint and should match expected domain patterns. Consider using environment-specific secrets or adding a verification step.

Suggested change
curl -X POST \
-H "Content-Type: application/json" \
"${{ secrets.CLEANUP_ENDPOINT_URL }}" \
-d '{"secret": "${{ secrets.CLEANUP_SECRET }}"}'
set -euo pipefail
CLEANUP_URL="${{ secrets.CLEANUP_ENDPOINT_URL }}"
if [[ -z "$CLEANUP_URL" ]]; then
echo "CLEANUP_ENDPOINT_URL is not set. Aborting cleanup job." >&2
exit 1
fi
# Require HTTPS to avoid sending secrets over insecure transport
if [[ "$CLEANUP_URL" != https://* ]]; then
echo "CLEANUP_ENDPOINT_URL must use HTTPS. Aborting cleanup job." >&2
exit 1
fi
# Ensure URL points to an approved Render domain to avoid exfiltrating secrets
if [[ "$CLEANUP_URL" != https://*.onrender.com/* && "$CLEANUP_URL" != https://*.render.com/* ]]; then
echo "CLEANUP_ENDPOINT_URL must point to an approved Render domain. Aborting cleanup job." >&2
exit 1
fi
curl -X POST \
-H "Content-Type: application/json" \
"$CLEANUP_URL" \
-d '{"secret": "'"${{ secrets.CLEANUP_SECRET }}"'"}'

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +23
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API call in the GitHub workflow doesn't check the HTTP response status. If the cleanup endpoint returns an error (4xx, 5xx), the workflow will still appear successful. Add response validation to fail the workflow job if the cleanup fails, enabling proper alerting. Consider adding: --fail flag to curl or checking the response with jq to verify the 'success' field.

Suggested change
curl -X POST \
-H "Content-Type: application/json" \
"${{ secrets.CLEANUP_ENDPOINT_URL }}" \
-d '{"secret": "${{ secrets.CLEANUP_SECRET }}"}'
set -euo pipefail
echo "Calling cleanup endpoint..."
response=$(
curl --fail --show-error -sS -X POST \
-H "Content-Type: application/json" \
"${{ secrets.CLEANUP_ENDPOINT_URL }}" \
-d '{"secret": "${{ secrets.CLEANUP_SECRET }}"}'
)
echo "Cleanup response: $response"
success=$(echo "$response" | jq -r '.success // empty')
if [ "$success" != "true" ]; then
echo "Cleanup endpoint reported failure (success=$success)."
exit 1
fi

Copilot uses AI. Check for mistakes.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ sqlnet.ora
# Django staticfiles and media
staticfiles/
media/
temp_uploads/

# Python build artifacts
*.egg-info/
Expand Down
23 changes: 19 additions & 4 deletions project/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,26 @@
# =====================================================

# Django Settings
DJANGO_SECRET_KEY=your-secret-key-here
DJANGO_DEBUG=True
# CRITICAL: Generate a secure secret key for production
# python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
DJANGO_SECRET_KEY=your-secret-key-here-CHANGE-THIS-IN-PRODUCTION
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1

# Deployment Environment
# Set to "DEV" or "LOCAL" to use server-side API keys (for local development)
# Set to "PROD" or leave empty to require users to provide their own keys (for remote deployment)
# Default: PROD (users bring their own keys)
# Set to "PROD" for production deployment (users bring their own keys)
# IMPORTANT: In production, also set CORS_ALLOWED_ORIGINS and CSRF_TRUSTED_ORIGINS
ENVIRONMENT=DEV

# Production CORS Configuration (required when DEBUG=False)
# Comma-separated list of allowed origins
# CORS_ALLOWED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com

# Production CSRF Configuration (required when DEBUG=False)
# Comma-separated list of trusted origins
# CSRF_TRUSTED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com

# AI Provider Configuration
# Choose which AI provider to use: 'gemini' or 'claude'
AI_PROVIDER=gemini
Expand Down Expand Up @@ -60,3 +70,8 @@ ORACLE_WALLET_PASSWORD=
# GitHub OAuth (configured in Firebase Console)
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

# File Cleanup Configuration
# Secret token for triggering remote file cleanup
# Generate with: python -c 'import secrets; print(secrets.token_urlsafe(32))'
CLEANUP_SECRET_TOKEN=your-cleanup-secret-token-here
14 changes: 12 additions & 2 deletions project/authentication/firebase_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
Firebase Admin SDK initialization and authentication utilities.
"""
import os
import logging
import firebase_admin
from firebase_admin import credentials, auth
from dotenv import load_dotenv

# Load environment variables
logger = logging.getLogger('authentication')
load_dotenv()


Expand Down Expand Up @@ -53,8 +54,17 @@ def verify_firebase_token(id_token):
initialize_firebase()
decoded_token = auth.verify_id_token(id_token)
return decoded_token
except auth.InvalidIdTokenError as e:
logger.warning(f"Invalid Firebase ID token: {str(e)}")
return None
except auth.ExpiredIdTokenError as e:
logger.warning(f"Expired Firebase ID token: {str(e)}")
return None
except auth.RevokedIdTokenError as e:
logger.warning(f"Revoked Firebase ID token: {str(e)}")
return None
except Exception as e:
print(f"Firebase token verification error: {str(e)}")
logger.error(f"Unexpected Firebase token verification error: {str(e)}", exc_info=True)
return None


Expand Down
9 changes: 7 additions & 2 deletions project/authentication/middleware.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""
Firebase authentication middleware for Django.
"""
import logging
from django.utils.deprecation import MiddlewareMixin
from django.http import JsonResponse
from .firebase_auth import verify_firebase_token, get_user_info_from_token
from .models import User

logger = logging.getLogger('authentication')


class FirebaseAuthenticationMiddleware(MiddlewareMixin):
"""
Expand All @@ -25,7 +28,8 @@ def process_request(self, request):
# Skip authentication for certain paths
exempt_paths = [
'/admin/',
'/api/auth/verify-token', # Allow token verification endpoint
'/api/auth/verify-token',
'/api/auth/csrf',
]

if any(request.path.startswith(path) for path in exempt_paths):
Expand Down Expand Up @@ -63,7 +67,7 @@ def process_request(self, request):
else:
request.firebase_user = None
except Exception as e:
print(f"Firebase authentication error: {str(e)}")
logger.error(f"Firebase authentication error: {str(e)}", exc_info=True)
request.firebase_user = None

return None
Expand All @@ -81,6 +85,7 @@ def my_view(request):
"""
def wrapper(request, *args, **kwargs):
if not hasattr(request, 'firebase_user') or request.firebase_user is None:
logger.warning(f"Unauthorized access attempt to {request.path} from IP {request.META.get('REMOTE_ADDR')}")
return JsonResponse({
'error': 'Authentication required',
'message': 'You must be logged in to access this endpoint'
Expand Down
1 change: 1 addition & 0 deletions project/authentication/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from . import views

urlpatterns = [
path('csrf', views.get_csrf_token, name='csrf_token'),
path('verify-token', views.verify_token, name='verify_token'),
path('me', views.get_current_user, name='get_current_user'),
path('update-session', views.update_session, name='update_session'),
Expand Down
64 changes: 48 additions & 16 deletions project/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,40 @@
Authentication API views for Firebase integration.
"""
from django.http import JsonResponse, HttpRequest
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
from django.views.decorators.http import require_http_methods
from django.utils import timezone
from django.conf import settings
from django_ratelimit.decorators import ratelimit
import json
import traceback
import logging

from .firebase_auth import verify_firebase_token, get_user_info_from_token
from .models import User
from .middleware import require_authentication

logger = logging.getLogger('authentication')


@require_http_methods(["GET"])
@ensure_csrf_cookie
def get_csrf_token(request: HttpRequest) -> JsonResponse:
"""
Get CSRF token for subsequent requests.

GET /api/auth/csrf

Returns:
200: CSRF token in response header and body
"""
response = JsonResponse({'success': True, 'detail': 'CSRF cookie set'})
return response


@csrf_exempt
@require_http_methods(["POST"])
@ratelimit(key='ip', rate='30/m', method='POST', block=True)
@ratelimit(key='ip', rate='10/m', method='POST', block=True)
def verify_token(request: HttpRequest) -> JsonResponse:
"""
Verify Firebase ID token and create/update user in database.
Expand Down Expand Up @@ -44,6 +63,7 @@ def verify_token(request: HttpRequest) -> JsonResponse:
# Verify token
decoded_token = verify_firebase_token(token)
if not decoded_token:
logger.warning(f"Failed token verification from IP {request.META.get('REMOTE_ADDR')}")
return JsonResponse({
'error': 'Invalid token',
'message': 'Failed to verify Firebase token'
Expand Down Expand Up @@ -114,11 +134,14 @@ def verify_token(request: HttpRequest) -> JsonResponse:
'message': 'Request body must be valid JSON'
}, status=400)
except Exception as e:
traceback.print_exc() # Print full traceback to console for debugging
return JsonResponse({
logger.error(f"Unexpected error in verify_token: {str(e)}", exc_info=True)
response = {
'error': 'Server error',
'message': 'An unexpected error occurred. Please try again later.'
}, status=500)
}
if settings.DEBUG:
response['traceback'] = traceback.format_exc()
return JsonResponse(response, status=500)


@csrf_exempt
Expand Down Expand Up @@ -161,7 +184,7 @@ def get_current_user(request: HttpRequest) -> JsonResponse:
@csrf_exempt
@require_http_methods(["POST"])
@require_authentication
@ratelimit(key='user_or_ip', rate='120/h', method='POST', block=True)
@ratelimit(key='user_or_ip', rate='60/h', method='POST', block=True)
def update_session(request):
"""
Update session information (increment session count, add time spent).
Expand Down Expand Up @@ -204,11 +227,14 @@ def update_session(request):
'message': 'Request body must be valid JSON'
}, status=400)
except Exception as e:
traceback.print_exc() # Print full traceback to console for debugging
return JsonResponse({
logger.error(f"Error in update_session: {str(e)}", exc_info=True)
response = {
'error': 'Server error',
'message': 'An unexpected error occurred. Please try again later.'
}, status=500)
}
if settings.DEBUG:
response['traceback'] = traceback.format_exc()
return JsonResponse(response, status=500)


@csrf_exempt
Expand All @@ -235,17 +261,20 @@ def logout(request):
}, status=200)

except Exception as e:
traceback.print_exc() # Print full traceback to console for debugging
return JsonResponse({
logger.error(f"Error in logout: {str(e)}", exc_info=True)
response = {
'error': 'Server error',
'message': 'An unexpected error occurred. Please try again later.'
}, status=500)
}
if settings.DEBUG:
response['traceback'] = traceback.format_exc()
return JsonResponse(response, status=500)


@csrf_exempt
@require_http_methods(["POST"])
@require_authentication
@ratelimit(key='user_or_ip', rate='60/h', method='POST', block=True)
@ratelimit(key='user_or_ip', rate='30/h', method='POST', block=True)
def mark_milestone(request):
"""
Mark user milestones (first model created, first export).
Expand Down Expand Up @@ -293,8 +322,11 @@ def mark_milestone(request):
'message': 'Request body must be valid JSON'
}, status=400)
except Exception as e:
traceback.print_exc() # Print full traceback to console for debugging
return JsonResponse({
logger.error(f"Error in mark_milestone: {str(e)}", exc_info=True)
response = {
'error': 'Server error',
'message': 'An unexpected error occurred. Please try again later.'
}, status=500)
}
if settings.DEBUG:
response['traceback'] = traceback.format_exc()
return JsonResponse(response, status=500)
57 changes: 42 additions & 15 deletions project/backend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure-oy%j%4%)w%7#sx@e!h+m-hai9zvl*)-5$5uz%wlro4ry1*4vc-')
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')
if not SECRET_KEY:
raise ValueError("DJANGO_SECRET_KEY environment variable must be set")
Comment on lines +28 to +30
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SECRET_KEY environment variable is now required but will cause the Django application to fail to start during settings import if not set. This breaks local development setup where developers may not have immediately configured their .env file. Consider allowing a development default when DEBUG=True while enforcing the requirement only in production (when DEBUG=False), similar to how CORS_ALLOWED_ORIGINS is handled.

Copilot uses AI. Check for mistakes.

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DJANGO_DEBUG', 'True') == 'True'
DEBUG = os.getenv('DJANGO_DEBUG', 'False') == 'True'

# Environment Variable Validation (Production Only)
REQUIRED_ENV_VARS = [
Expand Down Expand Up @@ -106,7 +108,12 @@
]

# CORS configuration - read from environment variable for production
cors_origins = os.getenv('CORS_ALLOWED_ORIGINS', 'http://localhost:3000,http://localhost:5173,http://localhost:5000')
if DEBUG:
cors_origins = os.getenv('CORS_ALLOWED_ORIGINS', 'http://localhost:3000,http://localhost:5173,http://localhost:5000')
else:
cors_origins = os.getenv('CORS_ALLOWED_ORIGINS')
if not cors_origins:
raise ValueError("CORS_ALLOWED_ORIGINS environment variable must be set in production")
CORS_ALLOWED_ORIGINS = [origin.strip() for origin in cors_origins.split(',')]

CORS_ALLOW_CREDENTIALS = True
Expand All @@ -125,12 +132,19 @@
'x-firebase-token',
]

csrf_origins = os.getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000,http://localhost:5173,http://localhost:5000')
if DEBUG:
csrf_origins = os.getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000,http://localhost:5173,http://localhost:5000')
else:
csrf_origins = os.getenv('CSRF_TRUSTED_ORIGINS')
if not csrf_origins:
raise ValueError("CSRF_TRUSTED_ORIGINS environment variable must be set in production")
CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in csrf_origins.split(',')]

# CSRF configuration: keep cookie HTTP-only; frontend should use X-CSRFToken header pattern
# CSRF Protection
CSRF_COOKIE_HTTPONLY = True
CSRF_USE_SESSIONS = False # Use cookie-based CSRF tokens
CSRF_USE_SESSIONS = False
CSRF_COOKIE_SAMESITE = 'Lax'
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'

# Environment mode configuration
# Controls API key behavior: PROD/missing = BYOK, DEV/LOCAL = server keys
Expand All @@ -139,18 +153,16 @@
REQUIRES_USER_API_KEY = IS_PRODUCTION

Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global REST_FRAMEWORK default permission has changed from AllowAny to IsAuthenticatedOrReadOnly, but multiple view functions are decorated with @permission_classes([AllowAny]). This creates inconsistency and confusion about the authentication strategy. Based on the PR description emphasizing security improvements and the custom guideline requiring authentication by default, these AllowAny decorators should be reviewed. Either remove them to use the global default, or document why specific endpoints need to remain public (e.g., validation_views.py, chat_views.py, export_views.py).

Suggested change
# Django REST Framework configuration
# NOTE:
# - We require authentication by default for all modifying requests via
# IsAuthenticatedOrReadOnly. This aligns with the "authentication by default"
# guideline and recent security hardening PRs.
# - Endpoints that truly need to be public (e.g. certain validation, chat, or
# export endpoints) may explicitly use @permission_classes([AllowAny]) in
# their view modules, but such use must be rare and documented in that
# module's docstring explaining why the endpoint is unauthenticated.
# - When adding new views, prefer the global default and avoid AllowAny unless
# there is a clear, reviewed product requirement for a public endpoint.

Copilot uses AI. Check for mistakes.
REST_FRAMEWORK = {
# For local dev only: allow unauthenticated access to endpoints
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.AllowAny",
"rest_framework.permissions.IsAuthenticatedOrReadOnly",
],
# Rate limiting via DRF throttling (global fallback)
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"anon": "100/hour",
"user": "1000/hour",
"anon": "60/hour",
"user": "600/hour",
},
}

Expand Down Expand Up @@ -260,10 +272,16 @@
# https://github.com/firebase/firebase-js-sdk/issues/8541
SECURE_CROSS_ORIGIN_OPENER_POLICY = 'same-origin-allow-popups'

# Content Security Policy (basic - customize as needed)
# CSP_DEFAULT_SRC = ("'self'",)
# CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'") # Adjust based on your needs
# CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
# Content Security Policy
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'", "'unsafe-eval'", "https://www.gstatic.com", "https://apis.google.com")
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CSP_SCRIPT_SRC includes both 'unsafe-inline' and 'unsafe-eval', which significantly weaken the Content Security Policy and allow XSS attacks. These directives permit inline scripts and eval(), which are common XSS vectors. If these are required for specific libraries (e.g., Firebase), consider using nonces or hashes for inline scripts, or refactor the code to eliminate the need for these unsafe directives. Document which dependencies require these settings.

Suggested change
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'", "'unsafe-eval'", "https://www.gstatic.com", "https://apis.google.com")
# NOTE: 'unsafe-inline' is currently retained for existing inline scripts in the frontend.
# No known dependency requires eval(), so 'unsafe-eval' is intentionally omitted to reduce XSS risk.
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'", "https://www.gstatic.com", "https://apis.google.com")

Copilot uses AI. Check for mistakes.
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", "https://fonts.googleapis.com")
CSP_IMG_SRC = ("'self'", "data:", "https:", "blob:")
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CSP_IMG_SRC directive allows 'https:' which permits images from ANY https origin. This is overly permissive and defeats much of the purpose of CSP. Specify explicit trusted domains instead, such as the domains actually used by your application (Firebase Storage, etc.). If a wildcard is truly necessary, document the security rationale.

Suggested change
CSP_IMG_SRC = ("'self'", "data:", "https:", "blob:")
CSP_IMG_SRC = ("'self'", "data:", "blob:", "https://firebasestorage.googleapis.com", "https://www.gstatic.com")

Copilot uses AI. Check for mistakes.
CSP_FONT_SRC = ("'self'", "https://fonts.gstatic.com", "data:")
CSP_CONNECT_SRC = ("'self'", "https://firebasestorage.googleapis.com", "https://identitytoolkit.googleapis.com", "https://securetoken.googleapis.com", "https://*.googleapis.com")
CSP_FRAME_SRC = ("https://accounts.google.com", "https://github.com")
CSP_OBJECT_SRC = ("'none'",)
CSP_BASE_URI = ("'self'",)

else:
# Development settings - less strict
Expand Down Expand Up @@ -295,3 +313,12 @@
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

# ==========================================
# FILE UPLOAD CONFIGURATION
# ==========================================
TEMP_UPLOAD_DIR = BASE_DIR / 'temp_uploads'
UPLOAD_RETENTION_HOURS = 2 # Delete files older than 2 hours

# Create upload directory if it doesn't exist
TEMP_UPLOAD_DIR.mkdir(exist_ok=True)
Comment on lines +322 to +324
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating the upload directory at module import time (during settings.py execution) can cause issues in several scenarios: 1) Settings may be imported in contexts where filesystem operations are not appropriate (e.g., during migrations, management commands), 2) The BASE_DIR may not be writable in containerized deployments, 3) This violates the principle of lazy initialization. Move this directory creation to application startup (e.g., in AppConfig.ready()) or to the first use within the file cleanup utilities.

Suggested change
# Create upload directory if it doesn't exist
TEMP_UPLOAD_DIR.mkdir(exist_ok=True)

Copilot uses AI. Check for mistakes.
Loading