diff --git a/tasks/views.py b/tasks/views.py index 4aa3cfd..8c86c02 100644 --- a/tasks/views.py +++ b/tasks/views.py @@ -7,6 +7,7 @@ from django.contrib.auth.decorators import login_required from django.db.models import Q from django.shortcuts import render, redirect, get_object_or_404 from django.utils import timezone +from django.utils.http import url_has_allowed_host_and_scheme from django.views import View from django.views.decorators.http import require_POST from rest_framework import generics, permissions, status, filters @@ -27,6 +28,21 @@ from .permissions import IsOwnerOrReadOnlyIfShared logger = logging.getLogger(__name__) +def safe_redirect(request, next_url, fallback='dashboard'): + """ + Safely redirect to next_url after validating it's not an open redirect. + + Only allows relative URLs or URLs to the same host. + """ + if next_url and url_has_allowed_host_and_scheme( + url=next_url, + allowed_hosts={request.get_host()}, + require_https=request.is_secure() + ): + return redirect(next_url) + return redirect(fallback) + + # ============================================================================= # API Views # ============================================================================= @@ -428,7 +444,7 @@ class TaskDetailView(View): # Redirect to next URL if provided, otherwise back to task detail next_url = request.POST.get('next') if next_url: - return redirect(next_url) + return safe_redirect(request, next_url, fallback=f'/task/{task.id}/') return redirect('task-detail', task_id=task.id) @@ -482,7 +498,7 @@ def task_quick_add(request): task.tags.add(tag_id) next_url = request.POST.get('next', 'dashboard') - return redirect(next_url) + return safe_redirect(request, next_url, fallback='dashboard') @login_required @@ -502,7 +518,7 @@ def subtask_create(request, task_id): subtask.tags.set(parent_task.tags.all()) next_url = request.POST.get('next', request.META.get('HTTP_REFERER', 'dashboard')) - return redirect(next_url) + return safe_redirect(request, next_url, fallback='dashboard') @login_required @@ -518,7 +534,7 @@ def task_toggle_status(request, task_id): task.save() next_url = request.POST.get('next', 'dashboard') - return redirect(next_url) + return safe_redirect(request, next_url, fallback='dashboard') @login_required @@ -530,7 +546,7 @@ def task_delete(request, task_id): task.save() next_url = request.POST.get('next', 'dashboard') - return redirect(next_url) + return safe_redirect(request, next_url, fallback='dashboard') @login_required @@ -582,7 +598,7 @@ def web_timer_start(request, task_id): logger.debug(f"Created new timer entry: {entry.id}") next_url = request.POST.get('next', 'dashboard') - return redirect(next_url) + return safe_redirect(request, next_url, fallback='dashboard') @login_required @@ -607,7 +623,7 @@ def web_timer_stop(request, task_id): logger.debug(f"Stopped {stopped_count} timer entries") next_url = request.POST.get('next', 'dashboard') - return redirect(next_url) + return safe_redirect(request, next_url, fallback='dashboard') class TagListView(View): @@ -653,7 +669,7 @@ class TagDetailView(View): tag.save() next_url = request.POST.get('next', 'tag-list') - return redirect(next_url) + return safe_redirect(request, next_url, fallback='tag-list') @login_required @@ -665,4 +681,4 @@ def tag_delete(request, tag_id): tag.save() next_url = request.POST.get('next', 'tag-list') - return redirect(next_url) + return safe_redirect(request, next_url, fallback='tag-list')