Cosas sobre S3 que ojalá no necesitaras saber
(blog.plerion.com)- En los riesgos de filtración de datos en AWS, el acceso no autorizado a buckets de S3 aparece repetidamente, y por el diseño antiguo de la API y algunos comportamientos excepcionales no es fácil determinarlo solo como “público/privado”.
- Muchas operaciones de S3 no se invocan mediante un endpoint general de AWS, sino con la URL del propio bucket; con una política de bucket mal configurada, incluso una solicitud
curlsin autenticación puede ejecutar operaciones peligrosas. - No basta con bloquear
s3:ListBucketpara considerarlo seguro; rutas comoListBucketVersions,ListMultipartUploadsyfetch-ownerpueden exponer claves de objetos e identificadores de cuenta. - Quien sube archivos puede influir en propiedades del objeto como la clase de almacenamiento, etiquetas, Object Lock y algunos headers relacionados con redirecciones, por lo que se necesitan controles adicionales como condiciones de IAM y políticas de ciclo de vida.
- Si se concluye que es privado solo por ver ACL y la configuración de bloqueo de acceso público, se pueden pasar por alto otras rutas; mediante una distribución de CloudFront o un identity pool de Cognito, usuarios de internet pueden acceder a objetos de S3.
Diseño antiguo de la API de S3 y llamadas anónimas
- S3 es uno de los servicios iniciales de AWS, por lo que es sólido y está bien probado, pero por rastros previos a los patrones de diseño estandarizados tiene una forma de API distinta a la de otros servicios de AWS.
- Algunas API de S3 usan endpoints generales como
s3.us-east-2.amazonaws.com, pero muchas operaciones deben enviarse directamente a la URL del bucket de destino.- Un ejemplo para consultar la lista del bucket es
GET /con la formaHost: [bucketname].s3.amazonaws.com. - Consultar las etiquetas del bucket también envía
GET /?taggingal host de ese bucket.
- Un ejemplo para consultar la lista del bucket es
- Muchos servicios de AWS, como EC2 o DynamoDB, usan endpoints generales y suelen pasar el recurso de destino mediante headers HTTP o parámetros.
- Como los buckets de S3 admiten tanto acceso público como autenticado, no siempre está claro qué operaciones de la API pueden realizarse sin autenticación.
- Si una política de bucket de ejemplo permite
Principal: "*"yAction: "s3:*"sobre el recurso del bucket, incluso una solicitud sin autenticación puede borrar el bucket.- La solicitud de ejemplo es
curl -X DELETE https://[bucketname].s3-ap-southeast-2.amazonaws.com
- La solicitud de ejemplo es
- Algunas operaciones no admiten solicitudes anónimas y devuelven errores como
s3:GetBucketOwnershipControls does not support Anonymous requests!. - Las solicitudes anónimas a la API quedan registradas en CloudTrail como cuenta anonymous.
- Si la solicitud no está autenticada, no se puede identificar quién borró el bucket ni quién consultó la configuración de cifrado o el estado del logging.
- Rutas como
/?logging,/?taggingy/?encryptionpueden probarse incluso desde el navegador. - También hay operaciones como
GetObjectTorrentque siguen documentadas, pero ya no pueden ejecutarse.
Bloquear ListBucket no basta para evitar la exposición de claves de objetos
- Para descargar objetos de S3 se necesita la clave de cada objeto, que funciona como una ruta de archivo.
- Una solicitud
GETal bucket raíz puede devolver el contenido del bucket según las condiciones, por lo que negars3:ListBucketsuele parecer una defensa común. - Incluso usando un ACL
public-readjunto con una política que niegas3:ListBucket, siguen existiendo rutas para obtener claves de objetos.GET /?versions, es decirs3:ListBucketVersions, devuelve metadatos de versiones de objetos dentro del bucket.GET /?uploads, es decirs3:ListMultipartUploads, devuelve la lista de cargas multipart en curso.
- La documentación de
HeadBuckethabla de verificar la existencia del bucket y los permisos de acceso, pero en la práctica comprueba si existe permiso para realizar la operaciónListBucket. - Si solo se valida si
ListBucketestá denegado, puede pasarse por alto la posibilidad de exposición de claves de objetos en S3.
Costos y exposición de cargas multipart no completadas
- Una carga multipart empieza con
create-multipart-uploady luego se suben partes conupload-part. - Las cargas multipart no completadas no son fáciles de revisar desde la consola web; pueden consultarse con
/?uploadso conaws s3api list-multipart-uploads --bucket [bucket-name]. - Si la solicitud de finalización no se envía correctamente, Amazon S3 no ensambla las partes ni crea el objeto.
- Las partes subidas permanecen en la cuenta hasta que la carga multipart se complete o se aborte.
- Las partes almacenadas generan costos de almacenamiento de S3.
- No se encontró una forma de descargar partes del objeto antes de completarlo, pero sí es posible borrarlas.
- AWS recomienda aplicar una regla de ciclo de vida para eliminar cargas no completadas después de cierta cantidad de días.
- Al listar cargas multipart no completadas con
/?uploads, se devuelve el ARN del principal que inició la carga.- Si no se consideran sensibles identificadores como el ID de cuenta o el ARN, esto puede no parecer un problema.
- Si se quiere evitar exponer identificadores útiles para un atacante, sí puede considerarse una filtración.
ACL y verificación de cuentas basada en email
- La documentación de ACL de S3 todavía conserva rastros de la época en que las cuentas de AWS se identificaban por el email del usuario root.
- La operación
PutBucketACLpermite especificar al grantee mediante dirección de email.- Usa
Type: AmazonCustomerByEmailyEmailAddress.
- Usa
- Si no existe una cuenta de AWS asociada al email indicado, se produce el error
UnresolvableGrantByEmailAddress.- El mensaje de error toma la forma de “la dirección de email proporcionada no coincide con ninguna cuenta registrada”.
- Debido a este comportamiento, es posible verificar si una dirección de email específica tiene una cuenta de AWS registrada.
Clase de almacenamiento y metadatos del objeto que puede elegir quien sube archivos
- La clase de almacenamiento de S3 se aplica al objeto, no al bucket.
- No existe una configuración para fijar una clase de almacenamiento deseada a nivel de bucket; quien sube el archivo puede especificar la clase de almacenamiento del objeto.
- El ejemplo es
aws s3 cp "my.txt" "s3://mybucket/myobject.txt" --storage-class [CLASS]
- El ejemplo es
- Quien sube archivos puede influir, dentro de una lista predefinida, en los costos de almacenamiento y acceso por GB que asume el propietario del bucket.
- En políticas de IAM, la clave de condición
s3:x-amz-storage-classpermite restringir las clases de almacenamiento permitidas.- La política de ejemplo permite solo
STANDARDparas3:PutObject.
- La política de ejemplo permite solo
- Si se configura una política de ciclo de vida, todos los objetos pueden trasladarse a una clase de almacenamiento específica después de cierto tiempo.
- En cargas que usan URL prefirmadas, AWS Signature Version 4 exige firmar todos los headers que comiencen con
X-Amz-.- La clase de almacenamiento se especifica con el header
x-amz-storage-class. - A menos que la aplicación esté muy mal implementada, no parece haber una forma clara de manipularlo de inmediato.
- La clase de almacenamiento se especifica con el header
Etiquetas, Object Lock y redirecciones también están bajo influencia del uploader
- Muchas propiedades asociadas a objetos de S3 están controladas por quien sube el archivo.
- Las etiquetas del objeto pueden definirse al momento de la carga.
- El ejemplo es
--tagging "AllYourTags=AreBelong&To=Us"
- El ejemplo es
- Los sistemas que ejecutan automatizaciones según el valor de las etiquetas pueden verse afectados por valores creados por quien sube el archivo.
- Object Lock permite configurar retención del objeto y legal hold cuando el object locking está habilitado en el bucket.
- El comando de ejemplo usa
--object-lock-retain-until-date "2099-01-01T00:00:00+0000",--object-lock-legal-hold-status "ON"y--object-lock-mode "COMPLIANCE".
- El comando de ejemplo usa
- En buckets con hosting de sitio web estático habilitado, es posible un open redirect usando la configuración del archivo subido.
- También hay que prestar atención a la lista completa de headers admitidos por
PutObject.- Las URL prefirmadas tienen restricciones.
- En configuraciones que dependen de Cognito identity y políticas de IAM, la solicitud puede firmarse con un contexto autenticado de Cognito.
Exposición del propietario del bucket e identificadores de cuenta
- Para comprobar si cierto ID de cuenta es el propietario de un bucket al que se puede acceder, se puede incluir el header
x-amz-expected-bucket-owneren una solicitudListBucket.- Si se envía un ID de cuenta incorrecto, se devuelve
AccessDenied. - Si se envía el ID de cuenta correcto y quien llama tiene permiso
ListBucket, la respuesta se devuelve normalmente.
- Si se envía un ID de cuenta incorrecto, se devuelve
- Si se usa el parámetro
fetch-owner=truede la APIListBucket, cada clave de la respuesta incluye un elemento Owner. - El
IDdentro deOwneres una cadena hexadecimal de 64 caracteres que la documentación de AWS llama canonical user ID.- Es una forma ofuscada del ID de cuenta de AWS.
- Si se guarda ese canonical user ID en
PrincipalcomoCanonicalUserdentro de una política de IAM y luego se refresca, AWS lo interpreta como un ID de cuenta de AWS. ListBucketVersionsyListMultipartUploadsfuncionan de manera similar incluso sinfetch-owner.
Las claves de objetos de S3 parecen nombres de archivo, pero funcionan distinto
- Las claves de objetos de S3 distinguen entre mayúsculas y minúsculas.
- Aunque parezcan el mismo nombre, si cambia el uso de mayúsculas se pueden subir varios objetos.
- Si una aplicación trata las claves de objetos de S3 como nombres de archivo sin distinción entre mayúsculas y minúsculas, pueden surgir problemas.
- En el ejemplo, la aplicación guarda contraseñas de usuarios en archivos de S3 y usa el nombre de archivo como nombre de usuario.
- Al registrarse, solo verifica si existe el archivo; al cambiar la contraseña, convierte el nombre de usuario a minúsculas y escribe en el archivo.
- Aunque ya exista
jeff, es posible registrarJEFF, y el usuarioJEFFpodría sobrescribir el archivo dejeffal cambiar su contraseña.
- En las claves de objetos de S3 puede usarse cualquier carácter UTF-8.
- Ciertos caracteres pueden causar problemas en algunas aplicaciones y protocolos.
- Espacios, barras diagonales y el carácter de porcentaje también son válidos en claves de objetos.
Aunque parezca un “bucket privado”, pueden quedar rutas de acceso
- Incluso si los ACL están desactivados, la política del recurso es restrictiva y block public access está habilitado, el bucket puede seguir siendo accesible públicamente.
- La ruta más común es una distribución de Amazon CloudFront.
- Cuando se coloca un CDN delante de un bucket de S3, normalmente existe la intención de entregar contenido a internet.
- Las herramientas de seguridad pueden concluir que no es público si la política del recurso del bucket está restringida a CloudFront.
- En el ejemplo,
get-bucket-policy-statusdevuelveIsPublic: false.- Si se hace una solicitud directa al bucket, devuelve
AccessDenied. - Si se envía la misma solicitud al dominio de la distribución de CloudFront, devuelve el contenido del objeto.
- Si se hace una solicitud directa al bucket, devuelve
- Un identity pool de Cognito también puede exponer buckets con políticas de recurso restringidas.
- Cognito entrega credenciales temporales de AWS del rol preconfigurado después de un inicio de sesión exitoso.
- Si ese rol tiene permisos
s3:ListBucketys3:GetObject, el usuario puede invocar la API de S3.
- Hay dos configuraciones de Cognito que pueden equivaler a acceso público.
- Self-registration: si usuarios de internet pueden registrarse e iniciar sesión en la app, en la práctica se trata de acceso público.
- Guest access: entrega un identificador único y credenciales de AWS incluso a usuarios no autenticados.
- El ejemplo de acceso de invitado sigue el flujo de obtener
IdentityIdconget-id, luego credenciales temporales conget-credentials-for-identity, y después ejecutaraws s3 lscon ese perfil. - CloudFront y los identity pools de Cognito se usan con frecuencia real en internet, pero son rutas de acceso público que rara vez aparecen marcadas en herramientas de seguridad.
1 comentarios
Opiniones de Hacker News
Hay muchos puntos interesantes, pero me cuesta estar de acuerdo con ver como una queja que el sistema de archivos distinga entre mayúsculas y minúsculas.
Me parece que así debería ser, y más bien me molesta que macOS no lo haga.
Que los nombres de archivo distingan mayúsculas y minúsculas puede resultar inesperado incluso para usuarios no técnicos. Si alguien dice que envió “Book Draft 1.docx”, pero en el buzón aparece “Book draft 1.docx”, normalmente uno no diría: “Creo que enviaste otro archivo”.
En el texto, las mayúsculas y minúsculas por lo general tampoco cambian el significado. “Hi, how are you?” y “hi, how are you?” significan lo mismo, y las mayúsculas solo cambian el sentido en casos como distinguir nombres propios de sustantivos comunes, algo que rara vez importa en nombres de archivos.
Más allá de preferencias personales, me cuesta entender que a un desarrollador o administrador de sistemas le sorprenda o le moleste que un sistema de archivos distinga mayúsculas y minúsculas. Si hace falta, el desarrollador puede abstraer eso para el usuario final, como ocurre con los resultados de búsqueda.
Tampoco es como en la programación, donde imponer un estilo de código ayuda a la legibilidad. Incluso en programación solía ser fuente de bugs hasta que los IDE se volvieron lo bastante inteligentes como para detectar errores tipográficos en nombres de variables. Una de las cosas buenas de Pascal, a diferencia de C, era que no había que preocuparse por las mayúsculas y minúsculas.
Puedes escribir el nombre del archivo con el estilo que quieras y esa forma se conserva, pero al buscarlo o procesarlo no tienes que recordar exactamente ese estilo, porque la búsqueda no distingue mayúsculas y minúsculas.
La distinción entre mayúsculas y minúsculas es de lo más fácil; lo menos intuitivo es que las rutas de S3 son falsas.
S3 acepta subir “/builds/1/installer.exe” y también muestra el listado dentro de /builds, pero en realidad lo que subiste es una sola clave llamada '/builds/1/installer.exe', con '/' incluido en el nombre.
Por eso también puedes subir “/builds/1//installer.exe” y “/builds//1/installer.exe”, y son archivos completamente distintos. Son solo nombres de clave; no hay directorios reales.
[1] https://docs.aws.amazon.com/AmazonS3/latest/userguide/direct...
Este año, nuestro sistema de producción tuvo un bug raro y recién lo encontramos con 5 personas investigando. La causa era un objeto cuyo nombre era literalmente “/”, y el software intentaba tratarlo como una ruta, no como un archivo.
Me cuesta confiar en S3 o en otros servicios de AWS. Nada es intuitivo, hay demasiadas piezas móviles y demasiada documentación que leer.
Aun así, como en el texto original, puedes terminar publicando todo al mundo por error. Preferiría usar servicios realmente simples como Hetzner Storage Boxes o DigitalOcean Spaces.
Hace poco descubrí que, si subes por pipe un archivo de video de más de unos pocos MB, en el Location devuelto omite https://. Así que en cada carga de archivo hay que verificar si Location empieza con https y, si no, agregárselo.
Como era de esperarse, en los issues de GitHub del cliente Node de S3 dicen “parece un bug de DigitalOcean”, y en el foro de DigitalOcean dicen “parece un bug del cliente Node de S3”.
Muchísimas funciones y rarezas que originalmente se diseñaron para ayudar en algunos casos especiales ahora forman parte del protocolo común. Parece que terminó así por intentar que, por razones de negocio, nadie se quedara afuera.
También hay que tener cuidado al borrar decenas de miles de millones de objetos. Llamar directamente a la API de eliminación puede salir caro.
En cambio, puedes configurar gratis reglas de ciclo de vida que fijen el vencimiento en now para un comodín o para todo el bucket. Entonces el cobro por almacenamiento se detiene de inmediato y AWS se encarga de procesar la eliminación.
También se evita que los servidores de la API de S3 sean golpeados por la cantidad de solicitudes por segundo.
Es realmente malo que las subidas multipart fallidas queden ahí de forma invisible y, si no configuras explícitamente el ciclo de vida, incluso generen costos de almacenamiento.
Pensé que la S de “Simple” significaba simplicidad.
Mi propuesta era que las partes de subidas incompletas solo permanecieran 24 horas después de la última actividad y que durante ese tiempo tampoco se cobrara almacenamiento. ahenry@ la rechazó.
En un servidor muy antiguo, durante casi 10 años se estuvo ejecutando todas las noches un script de cron que iniciaba una subida multipart. Servía para empujar backups a un bucket, pero ese bucket también almacenaba contenido subido por usuarios, así que parecía normal que creciera un poco cada día.
El script estaba “sin funcionar”, así que no dependíamos de esos datos de backup; los archivos no se veían en S3, y el tamaño del bucket aumentaba de forma constante pero no exagerada. Pero esta primavera vimos que estaba almacenando casi 3 TB de subidas multipart incompletas.
Por supuesto, sé que esta anécdota está llena de malas prácticas.
Siento que las discusiones sobre sensibilidad/no sensibilidad a mayúsculas y minúsculas suelen ser demasiado anglocéntricas.
Dicho de otra forma, especialmente en TI, demasiadas discusiones relacionadas con el lenguaje son excesivamente anglocéntricas.
Lo digo como alguien cuya lengua materna no es el inglés: la programación ya tiene demasiados conceptos y elementos. Es mejor no aumentar la complejidad teniendo que considerar además 101 idiomas distintos.
Unicode y las zonas horarias son ejemplos representativos de elementos que intentan considerar más lenguas y culturas en programación, y al final generan el mayor sufrimiento para todos, incluidos los programadores que no hablan inglés.
No quiero escribir programas en mi lengua materna. Mucho menos si el costo de hacerlo es tener que considerar todos los idiomas principales al programar. Está bien que las discusiones de TI sean anglocéntricas. La diversidad es complejidad, y el inglés no es un idioma que posea alguien, sino una herramienta que la gente usa para comunicarse.
Gracias a esa lengua común puedo expresar mis ideas a mucha gente de India, China, Japón, Sudamérica, etc. En el momento en que ellos deciden hablar en inglés, también son dueños del inglés. No hace falta meter la política de la diversidad en TI; es mejor mantenerlo en el plano técnico.
En cierto sentido, esos dos silabarios se sienten como un alfabeto en mayúsculas/minúsculas.
Hay algunas cosas más
Las cargas multipart no se pueden hacer desde varias máquinas con credenciales de instancia. Como el principal es distinto, no pueden acceder a las cargas multipart de los demás. Para ensamblar una sola carga multipart desde varias máquinas, hace falta un usuario de IAM real
Las solicitudes LIST no solo son lentas; en grandes volúmenes también son muy caras. Hay alternativas como “bucket inventory”, pero no son cómodas ni baratas
La creación de buckets usa DNS internamente, por lo que no tiene consistencia de lectura tras escritura. Por eso a veces no puedes acceder a un bucket justo después de crearlo, o no puedes borrar un bucket recién creado antes de esperar lo suficiente a que se propaguen los cambios. Consulta https://github.com/julik/talks/blob/master/euruko-2019-no-su...
Puedes crear al mismo tiempo un objeto llamado “foo” y otro llamado “foo/bar”. Eso termina siendo una estructura donde un archivo sobrescribe un directorio, lo que impide portar los datos del bucket a una estructura de sistema de archivos
Como S3 distingue mayúsculas de minúsculas, puedes crear objetos que no se pueden portar a una estructura de sistema de archivos. El almacenamiento de archivos de Rails se rompió bastante en macOS porque asumía un almacenamiento sensible a mayúsculas y minúsculas, y se corrigió para usar identificadores siempre en minúsculas
La mayoría de las configuraciones de S3 permiten GET, pero no HEAD. Parece ser una forma de impedir la exploración de la existencia de objetos, aunque no estoy seguro. En cualquier caso, un flujo amigable con caché que verifica el tamaño de un objeto con una solicitud HEAD no funciona, especialmente con URL prefirmadas. En su lugar, hay que sortearlo con un GET usando un Range muy pequeño, por ejemplo trayendo solo el primer byte
Si generas muchas URL prefirmadas, es posible aumentar la velocidad de generación entre 10 y 40 veces: https://github.com/WeTransfer/wt_s3_signer
También sigues pagando el costo de almacenamiento de las cargas multipart incompletas. Hay que tener especial cuidado si tu arquitectura permite que los usuarios inicien este tipo de cargas. Existe una configuración para eliminar automáticamente las cargas multipart incompletas después de cierto tiempo; si no quieres sufrir, actívala
Paradójicamente, S3 fue revolucionario y sigue siendo un producto excelente en muchos niveles. Pero así como tiene muchas funciones, también tiene muchas trampas
En Elixir, usando Stream.transform(https://hexdocs.pm/elixir/Stream.html#transform/3), armé un pipeline de posprocesamiento de CSV en streaming para modificar e inyectar columnas. Los módulos de AWS y CSV de Elixir procesan datos entrantes en streaming, pero me dio tristeza que, si el total del stream saliente era menor que 5 MiB, S3 arrojaba un error porque el módulo de AWS usa cargas multipart
Hay otro problema interesante que diagnosticamos tras varios días de análisis con un colega. S3 descarta silenciosamente las solicitudes posteriores cuando una única conexión TCP ha enviado 100 solicitudes HTTP
https://github.com/aws/aws-sdk-go/issues/2825
Es un patrón común cuando quieres keep-alive por rendimiento, pero también quieres evitar que los clientes permanezcan conectados demasiado tiempo y creen hotspots en el balanceador de carga
También está el hecho de que S3, en la clase de almacenamiento estándar, tiene alta latencia y no es adecuado para servir contenido web
Mucha gente piensa que puede alojar directamente en S3 recursos de un sitio web como imágenes o fuentes, pero eso puede empeorar la experiencia de usuario
“applications can achieve consistent small object latencies (and first-byte-out latencies for larger objects) of roughly 100–200 milliseconds.”
Fuente: https://docs.aws.amazon.com/AmazonS3/latest/userguide/optimi...
Con CloudFront signed cookies, también puedes dar acceso por CDN a usuarios específicos solo al contenido de S3 que les pertenece. Es bastante genial
Puedes cachear assets a los que se accede con frecuencia para reducir la latencia y también bajar bastante los costos
Que el uploader decida las reglas es bastante fuerte. Si un sitio web está configurado de forma descuidada, ¿eso significa que un usuario con suficiente motivación podría hacer que el contenido de usuario se suba a Amazon Glacier y luego se sirva desde ahí?
https://docs.aws.amazon.com/service-authorization/latest/ref...
En particular, las claves de condición están aquí, donde puedes ver claves para controlar el acceso según la clase de almacenamiento, el etiquetado, etc.
https://docs.aws.amazon.com/service-authorization/latest/ref...