Skip to content

Commit 8f32f89

Browse files
authored
fix unsafe redirect (#308)
1 parent 8b48d18 commit 8f32f89

File tree

15 files changed

+111
-64
lines changed

15 files changed

+111
-64
lines changed

Diff for: spirit/admin/views.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# -*- coding: utf-8 -*-
22

3-
from django.shortcuts import render, redirect
3+
from django.shortcuts import render
44
from django.contrib import messages
55
from django.utils.translation import gettext as _
66
from django.contrib.auth import get_user_model
77

88
import spirit
99
import django
10+
from spirit.core.utils.http import safe_redirect
1011
from spirit.category.models import Category
1112
from spirit.comment.flag.models import CommentFlag
1213
from spirit.comment.like.models import CommentLike
@@ -25,7 +26,7 @@ def config_basic(request):
2526
if is_post(request) and form.is_valid():
2627
form.save()
2728
messages.info(request, _("Settings updated!"))
28-
return redirect(request.GET.get("next", request.get_full_path()))
29+
return safe_redirect(request, "next", request.get_full_path())
2930
return render(
3031
request=request,
3132
template_name='spirit/admin/config_basic.html',

Diff for: spirit/comment/flag/views.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# -*- coding: utf-8 -*-
22

33
from django.contrib.auth.decorators import login_required
4-
from django.shortcuts import render, redirect, get_object_or_404
4+
from django.shortcuts import render, get_object_or_404
55

6-
from ...core.utils.views import is_post, post_data
6+
from spirit.core.utils.http import safe_redirect
7+
from spirit.core.utils.views import is_post, post_data
78
from ..models import Comment
89
from .forms import FlagForm
910

@@ -18,7 +19,7 @@ def create(request, comment_id):
1819

1920
if is_post(request) and form.is_valid():
2021
form.save()
21-
return redirect(request.POST.get('next', comment.get_absolute_url()))
22+
return safe_redirect(request, 'next', comment.get_absolute_url(), method='POST')
2223

2324
return render(
2425
request=request,

Diff for: spirit/comment/like/views.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# -*- coding: utf-8 -*-
22

33
from django.contrib.auth.decorators import login_required
4-
from django.shortcuts import render, redirect, get_object_or_404
4+
from django.shortcuts import render, get_object_or_404
55
from django.urls import reverse
66

7+
from spirit.core.utils.http import safe_redirect
78
from spirit.core.utils.views import is_post, post_data, is_ajax
89
from spirit.core.utils import json_response
910
from spirit.comment.models import Comment
@@ -28,7 +29,7 @@ def create(request, comment_id):
2829
if is_ajax(request):
2930
return json_response({'url_delete': like.get_delete_url()})
3031

31-
return redirect(request.POST.get('next', comment.get_absolute_url()))
32+
return safe_redirect(request, 'next', comment.get_absolute_url(), method='POST')
3233

3334
return render(
3435
request=request,
@@ -52,7 +53,8 @@ def delete(request, pk):
5253
kwargs={'comment_id': like.comment.pk})
5354
return json_response({'url_create': url, })
5455

55-
return redirect(request.POST.get('next', like.comment.get_absolute_url()))
56+
return safe_redirect(
57+
request, 'next', like.comment.get_absolute_url(), method='POST')
5658

5759
return render(
5860
request=request,

Diff for: spirit/comment/poll/views.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22

33
from django.contrib.auth.decorators import login_required
4-
from django.shortcuts import render, redirect, get_object_or_404
4+
from django.shortcuts import render, get_object_or_404
55
from django.views.decorators.http import require_POST
66
from django.contrib import messages
77
from django.contrib.auth.views import redirect_to_login
@@ -10,8 +10,9 @@
1010

1111
from djconfig import config
1212

13-
from ...core import utils
14-
from ...core.utils.paginator import yt_paginate
13+
from spirit.core.utils.http import safe_redirect
14+
from spirit.core import utils
15+
from spirit.core.utils.paginator import yt_paginate
1516
from .models import CommentPoll, CommentPollChoice, CommentPollVote
1617
from .forms import PollVoteManyForm
1718

@@ -35,7 +36,7 @@ def close_or_open(request, pk, close=True):
3536
.filter(pk=poll.pk)
3637
.update(close_at=close_at))
3738

38-
return redirect(request.GET.get('next', poll.get_absolute_url()))
39+
return safe_redirect(request, 'next', poll.get_absolute_url())
3940

4041

4142
@require_POST
@@ -55,10 +56,10 @@ def vote(request, pk):
5556
CommentPollChoice.decrease_vote_count(poll=poll, voter=request.user)
5657
form.save_m2m()
5758
CommentPollChoice.increase_vote_count(poll=poll, voter=request.user)
58-
return redirect(request.POST.get('next', poll.get_absolute_url()))
59+
return safe_redirect(request, 'next', poll.get_absolute_url(), method='POST')
5960

6061
messages.error(request, utils.render_form_errors(form))
61-
return redirect(request.POST.get('next', poll.get_absolute_url()))
62+
return safe_redirect(request, 'next', poll.get_absolute_url(), method='POST')
6263

6364

6465
@login_required

Diff for: spirit/comment/views.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from djconfig import config
1010

11+
from spirit.core.utils.http import safe_redirect
1112
from spirit.core.utils.views import is_post, post_data, is_ajax
1213
from spirit.core.utils.ratelimit.decorators import ratelimit
1314
from spirit.core.utils.decorators import moderator_required
@@ -41,15 +42,14 @@ def publish(request, topic_id, pk=None):
4142
if is_post(request) and not request.is_limited() and form.is_valid():
4243
if not user.st.update_post_hash(form.get_comment_hash()):
4344
# Hashed comment may have not been saved yet
44-
return redirect(
45-
request.POST.get('next', None) or
46-
Comment
45+
default_url = lambda: (Comment
4746
.get_last_for_topic(topic_id)
4847
.get_absolute_url())
48+
return safe_redirect(request, 'next', default_url, method='POST')
4949

5050
comment = form.save()
5151
comment_posted(comment=comment, mentions=form.mentions)
52-
return redirect(request.POST.get('next', comment.get_absolute_url()))
52+
return safe_redirect(request, 'next', comment.get_absolute_url(), method='POST')
5353

5454
return render(
5555
request=request,
@@ -67,7 +67,7 @@ def update(request, pk):
6767
pre_comment_update(comment=Comment.objects.get(pk=comment.pk))
6868
comment = form.save()
6969
post_comment_update(comment=comment)
70-
return redirect(request.POST.get('next', comment.get_absolute_url()))
70+
return safe_redirect(request, 'next', comment.get_absolute_url(), method='POST')
7171
return render(
7272
request=request,
7373
template_name='spirit/comment/update.html',
@@ -81,7 +81,7 @@ def delete(request, pk, remove=True):
8181
(Comment.objects
8282
.filter(pk=pk)
8383
.update(is_removed=remove))
84-
return redirect(request.GET.get('next', comment.get_absolute_url()))
84+
return safe_redirect(request, 'next', comment.get_absolute_url())
8585
return render(
8686
request=request,
8787
template_name='spirit/comment/moderate.html',
@@ -104,7 +104,7 @@ def move(request, topic_id):
104104
else:
105105
messages.error(request, render_form_errors(form))
106106

107-
return redirect(request.POST.get('next', topic.get_absolute_url()))
107+
return safe_redirect(request, 'next', topic.get_absolute_url(), method='POST')
108108

109109

110110
def find(request, pk):

Diff for: spirit/core/utils/decorators.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
from django.core.exceptions import PermissionDenied
66
from django.contrib.auth.views import redirect_to_login
7-
from django.shortcuts import redirect
87

98
from spirit.core.conf import settings
9+
from spirit.core.utils.http import safe_redirect
1010

1111

1212
def moderator_required(view_func):
@@ -48,7 +48,7 @@ def guest_only(view_func):
4848
@wraps(view_func)
4949
def wrapper(request, *args, **kwargs):
5050
if request.user.is_authenticated:
51-
return redirect(request.GET.get('next', request.user.st.get_absolute_url()))
51+
return safe_redirect(request, 'next', request.user.st.get_absolute_url())
5252

5353
return view_func(request, *args, **kwargs)
5454

Diff for: spirit/core/utils/http.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from django.shortcuts import redirect
4+
from django.utils.encoding import iri_to_uri
5+
6+
try:
7+
from django.utils.http import url_has_allowed_host_and_scheme
8+
except ImportError:
9+
from django.utils.http import is_safe_url as url_has_allowed_host_and_scheme
10+
11+
12+
def _resolve_lazy_url(url):
13+
if callable(url):
14+
return url()
15+
return url
16+
17+
18+
def safe_redirect(request, key, default_url='', method='GET'):
19+
next = (
20+
getattr(request, method).get(key, None) or
21+
_resolve_lazy_url(default_url)
22+
)
23+
url_is_safe = url_has_allowed_host_and_scheme(
24+
url=next, allowed_hosts=None)
25+
#allowed_hosts=settings.ALLOWED_HOSTS,
26+
#require_https=request.is_secure())
27+
if url_is_safe:
28+
return redirect(iri_to_uri(next))
29+
return redirect('/')

Diff for: spirit/topic/favorite/views.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
from django.contrib.auth.decorators import login_required
44
from django.shortcuts import get_object_or_404
5-
from django.shortcuts import redirect
65
from django.views.decorators.http import require_POST
76
from django.contrib import messages
87

98
from .models import TopicFavorite
109
from .forms import FavoriteForm
1110
from ..models import Topic
12-
from ...core import utils
11+
from spirit.core import utils
12+
from spirit.core.utils.http import safe_redirect
1313

1414

1515
@require_POST
@@ -23,12 +23,12 @@ def create(request, topic_id):
2323
else:
2424
messages.error(request, utils.render_form_errors(form))
2525

26-
return redirect(request.POST.get('next', topic.get_absolute_url()))
26+
return safe_redirect(request, 'next', topic.get_absolute_url(), method='POST')
2727

2828

2929
@require_POST
3030
@login_required
3131
def delete(request, pk):
3232
favorite = get_object_or_404(TopicFavorite, pk=pk, user=request.user)
3333
favorite.delete()
34-
return redirect(request.POST.get('next', favorite.topic.get_absolute_url()))
34+
return safe_redirect(request, 'next', favorite.topic.get_absolute_url(), method='POST')

Diff for: spirit/topic/moderate/views.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# -*- coding: utf-8 -*-
22

33
from django.utils import timezone
4-
from django.shortcuts import render, redirect, get_object_or_404
4+
from django.shortcuts import render, get_object_or_404
55
from django.contrib import messages
66
from django.utils.translation import gettext as _
77

8+
from spirit.core.utils.http import safe_redirect
89
from spirit.core.utils.views import is_post
910
from spirit.core.utils.decorators import moderator_required
1011
from spirit.comment.models import Comment
@@ -33,8 +34,7 @@ def _moderate(request, pk, field_name, to_value, action=None, message=None):
3334
if message is not None:
3435
messages.info(request, message)
3536

36-
return redirect(request.POST.get(
37-
'next', topic.get_absolute_url()))
37+
return safe_redirect(request, 'next', topic.get_absolute_url(), method='POST')
3838

3939
return render(
4040
request=request,

Diff for: spirit/topic/notification/views.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import json
44

55
from django.contrib.auth.decorators import login_required
6-
from django.shortcuts import render, redirect, get_object_or_404
6+
from django.shortcuts import render, get_object_or_404
77
from django.views.decorators.http import require_POST
88
from django.http import Http404, HttpResponse
99
from django.contrib import messages
@@ -18,6 +18,7 @@
1818
from spirit.core.utils.paginator import yt_paginate
1919
from spirit.core.utils.paginator.infinite_paginator import paginate
2020
from spirit.core.utils.views import is_ajax
21+
from spirit.core.utils.http import safe_redirect
2122
from spirit.topic.models import Topic
2223
from .models import TopicNotification
2324
from .forms import NotificationForm, NotificationCreationForm
@@ -39,7 +40,7 @@ def create(request, topic_id):
3940
else:
4041
messages.error(request, utils.render_form_errors(form))
4142

42-
return redirect(request.POST.get('next', topic.get_absolute_url()))
43+
return safe_redirect(request, 'next', topic.get_absolute_url(), method='POST')
4344

4445

4546
@require_POST
@@ -53,8 +54,8 @@ def update(request, pk):
5354
else:
5455
messages.error(request, utils.render_form_errors(form))
5556

56-
return redirect(request.POST.get(
57-
'next', notification.topic.get_absolute_url()))
57+
return safe_redirect(
58+
request, 'next', notification.topic.get_absolute_url(), method='POST')
5859

5960

6061
@login_required
@@ -124,5 +125,5 @@ def mark_all_as_read(request):
124125
.for_access(request.user)
125126
.filter(is_read=False)
126127
.update(is_read=True))
127-
return redirect(request.POST.get(
128-
'next', reverse('spirit:topic:notification:index')))
128+
return safe_redirect(
129+
request, 'next', reverse('spirit:topic:notification:index'), method='POST')

Diff for: spirit/topic/private/views.py

+17-14
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@
1010

1111
from djconfig import config
1212

13-
from ...core.conf import settings
14-
from ...core import utils
15-
from ...core.utils.views import is_post, post_data
16-
from ...core.utils.paginator import paginate, yt_paginate
17-
from ...core.utils.ratelimit.decorators import ratelimit
18-
from ...comment.forms import CommentForm
19-
from ...comment.utils import comment_posted
20-
from ...comment.models import Comment
13+
from spirit.core.conf import settings
14+
from spirit.core import utils
15+
from spirit.core.utils.http import safe_redirect
16+
from spirit.core.utils.views import is_post, post_data
17+
from spirit.core.utils.paginator import paginate, yt_paginate
18+
from spirit.core.utils.ratelimit.decorators import ratelimit
19+
from spirit.comment.forms import CommentForm
20+
from spirit.comment.utils import comment_posted
21+
from spirit.comment.models import Comment
2122
from ..models import Topic
2223
from ..utils import topic_viewed
2324
from .utils import notify_access
@@ -50,9 +51,8 @@ def publish(request, user_id=None):
5051
all([tform.is_valid(), cform.is_valid(), tpform.is_valid()]) and
5152
not request.is_limited()):
5253
if not user.st.update_post_hash(tform.get_topic_hash()):
53-
return redirect(
54-
request.POST.get('next', None) or
55-
tform.category.get_absolute_url())
54+
return safe_redirect(
55+
request, 'next', lambda: tform.category.get_absolute_url(), method='POST')
5656

5757
# wrap in transaction.atomic?
5858
topic = tform.save()
@@ -123,7 +123,8 @@ def create_access(request, topic_id):
123123
else:
124124
messages.error(request, utils.render_form_errors(form))
125125

126-
return redirect(request.POST.get('next', topic_private.get_absolute_url()))
126+
return safe_redirect(
127+
request, 'next', topic_private.get_absolute_url(), method='POST')
127128

128129

129130
@login_required
@@ -136,7 +137,8 @@ def delete_access(request, pk):
136137
if request.user.pk == topic_private.user_id:
137138
return redirect(reverse("spirit:topic:private:index"))
138139

139-
return redirect(request.POST.get('next', topic_private.get_absolute_url()))
140+
return safe_redirect(
141+
request, 'next', topic_private.get_absolute_url(), method='POST')
140142

141143
return render(
142144
request=request,
@@ -160,7 +162,8 @@ def join_in(request, topic_id):
160162
if is_post(request) and form.is_valid():
161163
topic_private = form.save()
162164
notify_access(user=form.get_user(), topic_private=topic_private)
163-
return redirect(request.POST.get('next', topic.get_absolute_url()))
165+
return safe_redirect(
166+
request, 'next', topic.get_absolute_url(), method='POST')
164167
return render(
165168
request=request,
166169
template_name='spirit/topic/private/join.html',

0 commit comments

Comments
 (0)