El portapapeles web y cómo almacena distintos tipos de datos
(alexharri.com)- 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/htmleimage/png, por lo que los tipos personalizados comoapplication/jsonson 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 planotext/html: HTMLimage/png: imagen PNG
- Google Docs lee la representación
text/htmlpara 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 lostypesdel elemento del portapapeles y luego lee una representación específica con algo comogetType("text/html")navigator.clipboard.write()puede escribir de una sola vez variosBlobdentro de unClipboardItemcuyas claves son tipos MIME- Si intentas escribir
application/jsoncomoClipboardItem, se produce la excepciónType application/json not supported on write - La especificación Clipboard de W3C hace que
writerechace los tipos que no seantext/plain,text/htmloimage/png application/jsonestuvo 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
ClipboardEventocurre en eventos copy, cut y paste, y entrega un objetoDataTransfermediante la propiedadclipboardData- En un evento copy se pueden escribir datos con
e.clipboardData.setData("text/plain", "...")ysetData("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 tipoapplication/jsony la cadena JSONclipboardDataes una interfaz mucho más antigua que la async Clipboard API- La async Clipboard API se agregó a la especificación en 2017
clipboardData,setDataygetDataya 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
clipboardDatatiene 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.isTrustedes una propiedad de solo lectura que estrueúnicamente cuando el evento fue generado por el agente de usuario- Un evento copy producido cuando el usuario presiona Command+C tiene
isTrustedentrue - Un evento sintético creado con
dispatchEvent()tieneisTrustedenfalse
- Un evento copy producido cuando el usuario presiona Command+C tiene
- 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/plaintext/htmlapplication/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
- Se puede crear una selección agregando al DOM un input no vacío, seleccionándolo, llamando a
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 devuelvetrue - En Windows, Chrome y Edge devuelven
true, Firefox devuelvefalse
- En macOS, Chrome y Firefox devuelven
- 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
- Con la async Clipboard API no se puede leer la representación
Copiar y pegar en Figma basado en HTML
- El botón Copy de Figma escribe dos representaciones en el portapapeles:
text/plainytext/html - La representación HTML contiene dos
spanvacíos, cada uno con atributosdata-metadataydata-buffer data-metadataes una cadena JSON codificada en base64; al decodificarla aparecen valores comofileKey,pasteIDydataTypedata-buffertiene 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-bufferdesde base64, aparece un dato binario que empieza config-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
.figpublicado por Evan Wallace se puede convertirdata-bufferen un archivo.figpara inspeccionarlo - Figma crea un archivo Figma pequeño, lo codifica en base64 y lo guarda en el portapapeles dentro del atributo
data-bufferde unspanHTML 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_HTMLpara HTML - macOS ofrece
NSPasteboard.PasteboardType.html
- Windows ofrece
- 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-barno 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/htmlpara poder copiar y pegar elementos entrefigma.comen 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/jsonyfoo bar bazdentro 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 HTMLpublic.utf8-plain-text: representación de texto planoorg.chromium.web-custom-data: representación personalizadaorg.chromium.source-url: URL de la página web donde se realizó la copia
- Firefox también crea
public.htmlypublic.utf8-plain-text, pero escribe los datos personalizados enorg.mozilla.custom-clipdata- A diferencia de Chrome, no guarda la URL de origen
- Safari también crea
public.htmlypublic.utf8-plain-text, y escribe los datos personalizados encom.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 ygetType("web application/json")
- Ejemplo:
- 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"
- Ejemplo:
- 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
unsanitizedderead()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
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.
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.
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
https://www.npmjs.com/~sindresorhus
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.
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.
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.
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.