4 puntos por GN⁺ 2024-09-02 | 1 comentarios | Compartir por WhatsApp
  • El portapapeles web puede guardar los datos copiados una vez junto con varias representaciones MIME, para que cada app elija y pegue solo el formato que necesita
  • La async Clipboard API está limitada principalmente a text/plain, text/html e image/png, por lo que los tipos personalizados como application/json son rechazados al escribir por motivos de seguridad
  • La antigua Clipboard Events API puede manejar cadenas de tipo arbitrarias, pero el portapapeles real del sistema solo cambia dentro de eventos de usuario confiables
  • Google Docs genera un evento copy confiable con execCommand("copy"), y Figma procesa la copia entre el navegador y la app nativa poniendo datos personalizados en atributos HTML en base64
  • Web Custom Formats habilita tipos personalizados en la async Clipboard API con el prefijo "web ", pero requiere navegadores basados en Chromium y apps nativas actualizadas

Modelo básico del portapapeles web

  • El portapapeles puede guardar varias representaciones (representations) con tipos MIME para una misma acción de copia
    • text/plain: texto plano
    • text/html: HTML
    • image/png: imagen PNG
  • Google Docs lee la representación text/html para conservar formato como enlaces, tamaño de letra y color
  • Las apps que solo necesitan el texto original, como VS Code, pueden leer únicamente la representación text/plain

Restricciones de tipos en la async Clipboard API

  • navigator.clipboard.read() revisa los types del elemento del portapapeles y luego lee una representación específica con algo como getType("text/html")
  • navigator.clipboard.write() puede escribir de una sola vez varios Blob dentro de un ClipboardItem cuyas claves son tipos MIME
  • Si intentas escribir application/json como ClipboardItem, se produce la excepción Type application/json not supported on write
  • La especificación Clipboard de W3C hace que write rechace los tipos que no sean text/plain, text/html o image/png
  • application/json estuvo en la lista de tipos de datos obligatorios desde 2012 hasta 2021, pero fue eliminado en w3c/clipboard-apis#155
    • Antes del cambio, los tipos de datos obligatorios eran 16 para lectura y 8 para escritura
    • La lista se redujo porque los navegadores no soportaban muchos tipos obligatorios por preocupaciones de seguridad
  • La especificación advierte que scripts no confiables podrían poner en el portapapeles datos capaces de provocar vulnerabilidades en software local, por lo que limita los tipos permitidos

Clipboard Events API e isTrusted

  • ClipboardEvent ocurre en eventos copy, cut y paste, y entrega un objeto DataTransfer mediante la propiedad clipboardData
  • En un evento copy se pueden escribir datos con e.clipboardData.setData("text/plain", "...") y setData("text/html", "...")
  • En un evento paste se puede leer la representación HTML con e.clipboardData.getData("text/html")
  • clipboardData.setData("application/json", json) no lanza una excepción, y en el manejador de paste también se puede ver el tipo application/json y la cadena JSON
  • clipboardData es una interfaz mucho más antigua que la async Clipboard API
    • La async Clipboard API se agregó a la especificación en 2017
    • clipboardData, setData y getData ya aparecían en un borrador de W3C de 2006
    • Ese borrador reflejaba en gran medida funciones implementadas en Internet Explorer
    • Como Internet Explorer 4 se lanzó en 1997, puede considerarse que la interfaz clipboardData tiene al menos 26 años al momento de escribir el artículo
  • Desde la especificación de 2011 se incorporaron los tipos MIME, pero la característica de que el argumento de tipo de setData() acepte cualquier cadena se mantiene hasta hoy
  • Event.isTrusted es una propiedad de solo lectura que es true únicamente cuando el evento fue generado por el agente de usuario
    • Un evento copy producido cuando el usuario presiona Command+C tiene isTrusted en true
    • Un evento sintético creado con dispatchEvent() tiene isTrusted en false
  • Los eventos copy/cut sintéticos no pueden modificar el portapapeles del sistema, y los eventos paste sintéticos no pueden acceder a los datos reales del portapapeles del sistema
  • Por lo tanto, la Clipboard Events API puede manejar tipos arbitrarios, pero la interacción con el portapapeles real queda limitada a manejadores de eventos copy/paste generados por el agente de usuario

Escribir tipos personalizados desde un botón Copy

  • El botón Copy del menú contextual de Google Docs escribe tres representaciones en el portapapeles
    • text/plain
    • text/html
    • application/x-vnd.google-docs-document-slice-clip+wrapped
  • La tercera representación es un tipo personalizado que contiene datos JSON y no puede escribirse usando solo la async Clipboard API
  • Google Docs llama a document.execCommand("copy") cuando se hace clic en el botón Copy
  • execCommand("copy") puede generar programáticamente un evento copy confiable, como si el usuario hubiera ejecutado el comando de copiar
  • En Safari, execCommand("copy") necesita una selección activa para generar el evento copy
    • Se puede crear una selección agregando al DOM un input no vacío, seleccionándolo, llamando a execCommand("copy") y eliminándolo después

Diferencias de navegador y sistema operativo en el botón Paste

  • El botón Paste de Google Docs exigía instalar una extensión en macOS, pero funcionaba en una laptop con Windows
  • Se puede verificar si el comando paste está soportado con document.queryCommandSupported("paste")
    • En macOS, Chrome y Firefox devuelven false, Safari devuelve true
    • En Windows, Chrome y Edge devuelven true, Firefox devuelve false
  • Safari muestra un prompt para confirmar la acción de pegar, dejando claro que el sitio web leerá el portapapeles
  • No se confirmó por qué Chrome permite execCommand("paste") en Windows y no lo permite en macOS
  • Google Docs no recurre a la async Clipboard API cuando no puede usar execCommand("paste")
    • Con la async Clipboard API no se puede leer la representación application/x-vnd.google-[...]
    • La representación HTML contiene IDs internos

Copiar y pegar en Figma basado en HTML

  • El botón Copy de Figma escribe dos representaciones en el portapapeles: text/plain y text/html
  • La representación HTML contiene dos span vacíos, cada uno con atributos data-metadata y data-buffer
  • data-metadata es una cadena JSON codificada en base64; al decodificarla aparecen valores como fileKey, pasteID y dataType
  • data-buffer tiene alrededor de 26,000 caracteres incluso al copiar solo un frame vacío, y parece crecer linealmente según la cantidad de contenido copiado
  • Al decodificar data-buffer desde base64, aparece un dato binario que empieza con fig-kiwi; se trata del Kiwi message format
    • Kiwi es un formato creado por Evan Wallace, cofundador de Figma y ex CTO
    • Se usa para codificar archivos .fig
  • Con el parser de archivos .fig publicado por Evan Wallace se puede convertir data-buffer en un archivo .fig para inspeccionarlo
  • Figma crea un archivo Figma pequeño, lo codifica en base64 y lo guarda en el portapapeles dentro del atributo data-buffer de un span HTML vacío

Por qué se usa la representación HTML

  • Windows, macOS y Linux ofrecen distintos formatos de sistema operativo para escribir datos en el portapapeles
    • Windows ofrece CF_HTML para HTML
    • macOS ofrece NSPasteboard.PasteboardType.html
  • El texto plano, HTML y las imágenes PNG pueden mapearse a formatos estándar en varios sistemas operativos
  • Los tipos arbitrarios como application/foo-bar no encajan bien con los formatos comunes del portapapeles del sistema operativo
  • Los navegadores no escriben estas representaciones personalizadas en los formatos comunes del portapapeles del sistema operativo; las guardan dentro de formatos de portapapeles personalizados de cada navegador
    • Como resultado, los tipos personalizados pueden copiarse y pegarse entre pestañas del navegador
    • Pero no pueden copiarse y pegarse entre aplicaciones
  • Al usar text/html, se mapea a un formato común del portapapeles del sistema operativo, lo que facilita que otras aplicaciones lo lean
  • Figma usa text/html para poder copiar y pegar elementos entre figma.com en el navegador y la app nativa de Figma

Cómo almacenan datos personalizados los distintos navegadores

  • En macOS, si se escriben text/plain, text/html, application/json y foo bar baz dentro de un evento copy y se revisa con Pasteboard Viewer, cada navegador los almacena de forma distinta
  • Chrome agrega cuatro elementos al Pasteboard
    • public.html: representación HTML
    • public.utf8-plain-text: representación de texto plano
    • org.chromium.web-custom-data: representación personalizada
    • org.chromium.source-url: URL de la página web donde se realizó la copia
  • Firefox también crea public.html y public.utf8-plain-text, pero escribe los datos personalizados en org.mozilla.custom-clipdata
    • A diferencia de Chrome, no guarda la URL de origen
  • Safari también crea public.html y public.utf8-plain-text, y escribe los datos personalizados en com.apple.WebKit.custom-pasteboard-data
    • Dentro de este elemento también guarda la lista completa de representaciones, incluidos texto plano y HTML, además de la URL de origen
  • Safari permite copiar y pegar tipos personalizados entre pestañas del navegador solo cuando el dominio de la URL de origen es el mismo
    • En Chrome y Firefox no se observa la misma restricción

Raw Clipboard Access y Web Custom Formats

  • Raw Clipboard Access fue una API propuesta en 2019 que buscaba dar a las aplicaciones web acceso de lectura y escritura en bruto al portapapeles nativo del sistema operativo
  • Raw Clipboard Access abordaba el problema de que las aplicaciones web, al estar limitadas a unos pocos formatos, dificultaban que Figma y Photopea interoperaran con la mayoría de los formatos de imagen
  • La propuesta no avanzó más debido a preocupaciones de seguridad, como la ejecución remota de código en aplicaciones nativas
  • Web Custom Formats es una propuesta para escribir tipos personalizados en la async Clipboard API, y Chromium la implementó en 2022
  • Web Custom Formats se usa anteponiendo "web " al tipo MIME
    • Ejemplo: web application/json
    • Para leer también se usan read() de la async Clipboard API y getType("web application/json")
  • Web Custom Formats escribe dos tipos de datos en el portapapeles nativo del sistema operativo
    • El mapeo entre tipos de datos y nombres de elementos del portapapeles
    • El elemento del portapapeles de cada tipo de datos
  • En macOS, el mapeo se guarda en org.w3.web-custom-format.map
    • Ejemplo: "application/json": "org.w3.web-custom-format.type-0"
  • Los elementos org.w3.web-custom-format.type-[index] contienen los datos sin procesar del Blob
  • Este enfoque evita los problemas de seguridad de Raw Clipboard Access al impedir que las aplicaciones web escriban directamente datos sin procesar en el formato del portapapeles del sistema operativo que quieran
  • En cambio, no busca interoperar con aplicaciones nativas legacy sin actualizaciones
    • Las aplicaciones nativas deben actualizarse para leer el formato de Web Custom Formats
  • Web Custom Formats está disponible desde 2022 en navegadores basados en Chromium, pero otros navegadores aún no lo implementan

Opción unsanitized

  • Al leer el portapapeles con la async Clipboard API, el navegador puede sanear los datos
    • Puede eliminar etiquetas script potencialmente peligrosas del HTML
    • Puede volver a codificar imágenes PNG para evitar ataques de zip bomb
  • La opción unsanitized de read() permite solicitar datos no saneados
  • Actualmente, esta opción solo está soportada en navegadores basados en Chromium
    • Se agregó a fines de 2023
    • Otros navegadores podrían soportarla en el futuro, pero hay comentarios que indican que Safari parece poco probable

Opciones realistas

  • Todavía no existe una forma de escribir tipos personalizados de portapapeles que funcione bien en todos los navegadores
  • El enfoque de Figma, que mete una cadena base64 dentro de la representación HTML, es rudimentario, pero permite pasar datos personalizados por el portapapeles esquivando varias restricciones de la Clipboard API
  • Web Custom Formats ofrece una dirección más segura y práctica para usar tipos personalizados, pero necesita implementación en todos los navegadores principales

1 comentarios

 
GN⁺ 2024-09-02
Opiniones de Hacker News
  • Buen artículo. Ahora entiendo por qué en Firefox para Mac no funcionaba bien pegar con/sin formato en Google Docs.
    Es interesante que Google esté usando una API obsoleta para intentar meter su propio formato de contenido en el portapapeles.

    • Quizá algún día haya que declarar oficialmente a la web de Google como un fork de la web oficial.
  • La forma más confiable de enviar datos personalizados de una app al portapapeles parece ser incrustar los datos dentro del HTML, como hace Figma.
    También tiene la ventaja de poder definir el comportamiento de error con un mensaje HTML cuando la app receptora no lo soporta.

  • WordPress también se topa con este problema. Si cortas y pegas varios párrafos en modo editor, puede romperse bastante.
    Probablemente porque esos párrafos en realidad están en distintas áreas DIV, bajo lógicas de control y efectos diferentes.
    Además, la máquina toma algo que yo creía ASCII y lo sube a UTF-8 o ISO Latin-1, haciendo algo tipo Clippy: “te cambié las comillas para que se vean más bonitas; ya me lo agradecerás después”, cosa que nunca pedí. Si hubiera querido `this', no habría escrito 'this'.

  • Cuando era estudiante, JavaScript podía leer el portapapeles sin consentimiento del usuario, y creé un sitio web llamado getpasted.
    Era un sitio que pegaba automáticamente el contenido del portapapeles y lo subía a una base de datos pública; por supuesto, a algunas personas no les gustó.
    Aun así, como proyecto para mostrar que el portapapeles se podía leer en cualquier momento, estaba bien.

    • La gente se sorprendería al enterarse de que en Windows 11, por defecto, todos los datos del portapapeles se envían a los servidores de Microsoft mediante Clipboard History / Cloud Clipboard.
  • Sindre Sorhus, el creador de Pasteboard Manager[0], también hizo la biblioteca de atajos Actions[1] y varias apps para iPhone y Mac. No sé si está en HN.
    0. https://apps.apple.com/us/app/pasteboard-viewer/id1499215709

    1. https://apps.apple.com/us/app/actions/id1586435171
  • La combinación de alcance y profundidad es especialmente buena. Cubre incluso las diferencias por sistema y navegador, pero también explica las trampas y soluciones alternativas.

    • He estado intentando reducir el exceso de detalles y desvíos en el texto. Es un artículo largo, pero me alegra que esta vez parezca haber logrado un buen equilibrio.
  • En relación con lo de obtener datos “privados” desde el navegador, la webapp de mi banco, al iniciar sesión, me mostró mi nombre de host, aluminium, y me pidió autenticación de dos factores.
    No sé cómo lo averiguó. Al acceder desde el móvil, también podía ver el SMS con el código de autenticación de dos factores y pegarlo automáticamente. Era en entornos Pixel+Chrome o Linux+Chrome.

    • El código de autenticación de dos factores probablemente sea una función de Chrome. Safari también muestra los códigos de verificación por SMS como sugerencias de autocompletado, y Chrome en Android probablemente pueda simplemente autocompletarlos.
      Lo del nombre de host es más difícil. Habría que verificar si de verdad nunca le diste al banco esa información en forma de nombre de usuario o algo parecido. También me da curiosidad qué banco era.
  • Este artículo muestra bien por qué las webapps no pueden llegar a ser tan buenas como las apps nativas.
    Las webapps siempre son código “no confiable”, así que el acceso a los recursos de la máquina local está restringido de manera arbitraria y artificial.

    • No confiar por defecto es una función. A menos que el usuario sea un profesional de seguridad muy entrenado y siempre alerta, es difícil que pueda protegerse por sí mismo.
      Ya no estamos en la época temprana de Internet, cuando los script kiddies andaban sueltos, ni en un espacio usado solo por geeks. Vivimos en una época en la que organizaciones vinculadas a gobiernos lanzan ataques de ransomware contra infraestructura crítica para financiar programas nucleares. El acceso arbitrario a recursos de la máquina local es justamente lo que permite ese tipo de cosas.
      Las webapps en navegadores modernos están naturalmente dentro de un sandbox más fuerte, pero en los sistemas operativos modernos las apps nativas también se tratan como no confiables. Al ejecutar una app nueva, aparece un diálogo de confirmación antes de que pueda acceder a algo fuera de su directorio aislado de datos de app.
  • Por cierto, me da curiosidad cuál era la extensión de Chrome que hizo aparecer la advertencia en la MacBook.