Closed Bug 1761746 Opened 4 years ago Closed 4 years ago

CSRF When accepting an answer as the solution to a question

Categories

(support.mozilla.org :: General, defect)

defect

Tracking

(Not tracked)

RESOLVED FIXED

People

(Reporter: roldanbrandon62, Unassigned)

References

()

Details

(Keywords: reporter-external, sec-moderate, wsec-csrf, Whiteboard: [reporter-external] [web-bounty-form] [verif?])

Attachments

(1 file)

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())
Flags: sec-bounty?

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

Hi. It seems to be still down for me

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

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

Can you please send us the steps to reproduce in text as well?

Thanks,
Frida

Sure.

STEPS TO REPRODUCE

  1. Have two accounts, we will call these two users as user a and user b
  2. As user a, make a question.
  3. 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
  4. 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.
  5. Save your answer.
  6. 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.
  7. Now, you can see that the answer of user b, is accepted without user a knowing

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.

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

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">

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.

Status: UNCONFIRMED → NEW
Type: task → defect
Ever confirmed: true
Keywords: wsec-csrf
Component: Other → General
Product: Websites → support.mozilla.org
Flags: sec-bounty? → sec-bounty+

Thanks for the bounty

Status: NEW → RESOLVED
Closed: 4 years ago
Resolution: --- → FIXED

Fix has been released to prod

Group: websites-security
Keywords: sec-moderate
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: