Reports
BAJA[IDOR / BOLA]#3543475

Fizzy (Basecamp): importar una cuenta permitía mostrar contenido de otra mediante referencias de ActionText

La importación de cuentas de Fizzy convertía identificadores GID del ZIP subido en referencias firmadas SGID sin comprobar que el registro perteneciera a la cuenta que importaba, lo que permitía mostrar datos de otro cliente.

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

Fizzy, el producto de Basecamp, permite importar los datos de una cuenta a partir de un fichero ZIP. Entre esos datos hay texto enriquecido de ActionText, cuyo HTML puede incluir adjuntos que apuntan a otros registros de la aplicación.

El problema estaba en el método convert_gids_to_sgids del fichero app/models/account/data_transfer/action_text_rich_text_record_set.rb. Durante la importación, ese método toma los valores gid (Global ID de Rails) que vienen en el HTML subido, busca el registro al que apuntan en toda la base de datos y genera a partir de él un sgid (Signed Global ID) que queda guardado. En ningún momento comprueba que ese registro pertenezca a la cuenta que está importando.

Como el contenido del ZIP lo controla quien lo sube, un atacante podía poner en él el gid de un registro de otra cuenta. La aplicación lo resolvía, le ponía su firma y lo guardaba como una referencia válida dentro de la cuenta del atacante.

Lo llamativo es que el camino contrario sí estaba protegido: en el mismo fichero (línea 69), la exportación solo incluye registros que cumplen record&.account_id == account.id. La importación no tenía la comprobación equivalente.

El investigador señaló las líneas implicadas del método vulnerable:

app/models/account/data_transfer/action_text_rich_text_record_set.rb:83
app/models/account/data_transfer/action_text_rich_text_record_set.rb:87
app/models/account/data_transfer/action_text_rich_text_record_set.rb:88
app/models/account/data_transfer/action_text_rich_text_record_set.rb:89

Y el camino desde la subida normal de una importación hasta ese código:

app/controllers/account/imports_controller.rb:38
app/models/account/import.rb:37
app/models/account/import.rb:39

HackerOne lo clasificó como control de acceso inadecuado, con severidad baja. El reporte se envió el 7 de febrero de 2026, se cerró como resuelto y se hizo público el 14 de abril de 2026. Hubo recompensa, pero su importe no es público.

Pasos de reproducción

El investigador preparó dos pruebas de concepto que se ejecutan sobre el código del repositorio de Fizzy. Los scripts (security-poc/integration_test_standalone.rb, security-poc/demo_import_cross_account_impact.rb y security-poc/patch_action_text_gid.py) se adjuntaron al reporte, pero su contenido no está en la versión pública.

Opción A: prueba directa del método vulnerable. Requiere Ruby y Bundler y el repositorio clonado. Carga el fichero real app/models/account/data_transfer/action_text_rich_text_record_set.rb y ejecuta Account::DataTransfer::ActionTextRichTextRecordSet#import_batch, sin montar el flujo web completo.

  1. Situarse en el repositorio:
cd /path/to/fizzy
  1. Comprobar o instalar las dependencias:
bundle check || bundle install
  1. Lanzar la prueba:
bundle exec ruby security-poc/integration_test_standalone.rb
  1. Si la salida muestra lo siguiente, el fallo está confirmado: el texto importado pertenece a la cuenta del atacante, pero la referencia se resuelve en un registro de la víctima (otra cuenta) y se obtiene su contenido.
RESULT: IMPACT_CONFIRMED=true
imported.account_id = <attacker_account_id>
resolved_account_id = <victim_account_id>
attachable_text = @alice_secret

Opción B: flujo de importación completo dentro de Rails.

  1. Situarse en el repositorio:
cd /path/to/fizzy
  1. Comprobar o instalar las dependencias:
bundle check || bundle install
  1. Ejecutar la demostración con el entorno de Rails cargado (el arranque puede tardar unos minutos en equipos lentos):
DISABLE_BOOTSNAP=1 RAILS_ENV=development \
bundle exec rails runner security-poc/demo_import_cross_account_impact.rb
  1. Resultado esperado:
cross_account_reference=true
IMPACT_CONFIRMED=true
imported_rich_text_account_id=<attacker_account_id>
resolved_account_id=<victim_account_id>
leaked_attachable_text=@alice

El investigador indica que obtuvo IMPACT_CONFIRMED=true con las dos opciones en sus pruebas locales.

Impacto

  • Un usuario de una cuenta podía crear, dentro de su propio texto enriquecido, referencias a registros adjuntables que pertenecían a otra cuenta.
  • Esos registros de la víctima se resolvían y se mostraban en el contexto de la cuenta del atacante, con lo que podía leer su contenido.
  • La referencia firmada (sgid) quedaba guardada en la base de datos y seguía funcionando hasta que alguien la eliminara.

Remediación

El reporte figura como resuelto, aunque no detalla el cambio aplicado. La corrección que propuso el investigador para convert_gids_to_sgids era:

  • Resolver el registro de forma segura, controlando el caso de que no exista.
  • Generar el sgid solo si record.respond_to?(:account_id) && record.account_id == account.id.
  • Descartar o ignorar las referencias que apunten a otras cuentas.

Qué aprender de este caso

  • En las aplicaciones Rails que importan datos, busca dónde se resuelven o se firman (to_sgid) identificadores que vienen del fichero subido: si se buscan en toda la base de datos sin filtrar por cuenta, sirven para alcanzar registros ajenos.
  • Compara la exportación con la importación del mismo modelo. Aquí la exportación filtraba por account_id y la importación no; las asimetrías de este tipo son un buen punto de partida.
  • Una firma (sgid) no da permiso por sí misma: solo garantiza que la referencia la generó el servidor. Antes de firmar algo que llega del usuario, hay que comprobar que pertenece a quien lo pide.
  • En las aplicaciones con varias cuentas, haz que las búsquedas pasen por la cuenta actual en lugar de buscar en toda la base de datos y comprobar después.