CSRF When accepting an answer as the solution to a question
Categories
(support.mozilla.org :: General, defect)
Tracking
(Not tracked)
People
(Reporter: roldanbrandon62, Unassigned)
References
()
Details
(Keywords: reporter-external, sec-moderate, wsec-csrf, Whiteboard: [reporter-external] [web-bounty-form] [verif?])
Attachments
(1 file)
|
100.73 KB,
image/png
|
Details |
SUMMARY
Hi. While reading the code, i found out an interesting endpoint /questions/<questionID>/solve/<answerID>, This endpoint, allows the creator of a question to mark an answer as the solution. What makes this interesting is that it is done in a get request, and in django, get requests are not protected by csrf making it vulnerable to csrf attacks
It calls the function solve, and this function doesnt have the require_POST middleware, nor does it check if the request is a post request in the function.
I havent really tested it yet since the production site is still down, but theoretically, it is possible.
This is the function handling the endpoint
def solve(request, question_id, answer_id):
"""Accept an answer as the solution to the question."""
question = get_object_or_404(Question, pk=question_id, is_spam=False)
# It is possible this was clicked from the email.
if not request.user.is_authenticated:
watch_secret = request.GET.get("watch", None)
try:
watch = Watch.objects.get(
secret=watch_secret, event_type="question reply", user=question.creator
)
# Create a new secret.
distinguishable_letters = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXYZ"
new_secret = "".join(random.choice(distinguishable_letters) for x in range(10))
watch.update(secret=new_secret)
request.user = question.creator
except Watch.DoesNotExist:
# This user is neither authenticated nor using the correct secret
return HttpResponseForbidden()
answer = get_object_or_404(Answer, pk=answer_id, is_spam=False)
if not question.allows_solve(request.user):
raise PermissionDenied
if question.creator != request.user and not request.user.has_perm("questions.change_solution"):
return HttpResponseForbidden()
if not question.solution:
question.set_solution(answer, request.user)
messages.add_message(request, messages.SUCCESS, _("Thank you for choosing a solution!"))
else:
# The question was already solved.
messages.add_message(request, messages.ERROR, _("This question already has a solution."))
return HttpResponseRedirect(question.get_absolute_url())
Comment 1•4 years ago
•
|
||
Hello,
Thank you for your report.
Can you please test on staging, https://support.allizom.org, it seems to be working today. Ah never mind, it is down today
Thanks,
Frida
| Reporter | ||
Comment 2•4 years ago
|
||
Hi. It seems to be still down for me
| Reporter | ||
Comment 3•4 years ago
|
||
The site seems to be up and running now, will try to reproduce it and also take a video poc about it in a moment
| Reporter | ||
Comment 4•4 years ago
|
||
| Reporter | ||
Comment 5•4 years ago
|
||
Hi. Also, because of the Same-Site: Lax of the cookies, a normal poc, with a get request wont work. However, we when making a question or an answer, we can add our own img tag, if we point the src of the img tag to /questions/<questionID>/solve/<answerID>, it will make the request without the user knowing, This is a case of osrf. You can read more about it in https://portswigger.net/blog/on-site-request-forgery
Comment 6•4 years ago
|
||
Can you please send us the steps to reproduce in text as well?
Thanks,
Frida
| Reporter | ||
Comment 7•4 years ago
|
||
Sure.
STEPS TO REPRODUCE
- Have two accounts, we will call these two users as user a and user b
- As user a, make a question.
- As user b, answer the question of user a. Take note of the id of the question of user a, and the answer of user b
- As user b, edit your answer, and add
<img src="https://support.allizom.org/en-US/questions/<questionID>/solve/<answerID>">. Replace questionID with the Id of user a's question, and replace answerID with the id of user b's answer. - Save your answer.
- Now as user a, look at user b's answer. This will make a get request to https://support.allizom.org/en-US/questions/<questionID>/solve/<answerID> bypassing the Same Site protection.
- Now, you can see that the answer of user b, is accepted without user a knowing
Comment 8•4 years ago
|
||
I can see that the solve endpoint can be requested using both a POST and GET methods. Using POST, there is a CSRF token that must be sent in the body but the GET request could be vulnerable to CSRF.
However, I was not able to post a comment with an img tag or edit my comment to include img tag, I get a message that a moderator must approve my comment before it becomes visible as you can see in the screenshot. I think that could reduce the risk here.
Comment 9•4 years ago
|
||
| Reporter | ||
Comment 10•4 years ago
|
||
Hi. I am not sure what you did wrong, but in my case, i am not getting that message. Afaik, anyone can post a reply on anyones comment. Maybe try it again. Also if you like, i can provide a video poc, if that will help in triaging
| Reporter | ||
Comment 11•4 years ago
|
||
Hi. frida, ignore my last comment. I can now see what you mean. It seems like whenever we have an img tag with https://<domain> in the src, it will be up for moderation. However, using relative path instead of absolute one, will bypass this. Eg. <img src="/en-US/questions/1207213"> instead of <img src="https://support.allizom.org/en-US/questions/1207213">
Comment 12•4 years ago
|
||
That is interesting, I tried to add img tag with a relative path which was accepted. Go to the question with user A who posted the question, it will send the request to the solve endpoint, refresh again and can see the answer was chosen as the solution, https://support.allizom.org/en-US/questions/1207227.
Updated•4 years ago
|
| Comment hidden (offtopic) |
Updated•4 years ago
|
| Comment hidden (offtopic) |
| Comment hidden (offtopic) |
| Comment hidden (offtopic) |
| Comment hidden (offtopic) |
Updated•4 years ago
|
| Reporter | ||
Comment 18•4 years ago
|
||
Thanks for the bounty
Comment 20•4 years ago
|
||
Comment 21•4 years ago
|
||
Fix has been released to prod
Updated•3 years ago
|
Updated•3 years ago
|
Updated•2 years ago
|
Description
•