- El EEF CNA reveló que el 35.8% de los CVE publicados son de consumo no controlado de recursos, y en el ecosistema BEAM una gran parte corresponde al agotamiento repetido de atoms
- El agotamiento de atoms es una vulnerabilidad de denegación de servicio: los atoms no pasan por recolección de basura, se acumulan en una tabla global y, cuando la tabla se llena, la VM se cae
- Crear atoms a partir de datos cuyo conjunto posible de valores no está garantizado como finito, como la entrada del usuario, genera riesgo de DoS; el esquema de URI tampoco es una excepción
- El riesgo existe no solo en llamadas explícitas como
binary_to_atom/1yString.to_atom/1, sino también en la decodificación de claves JSON a atoms y en la generación dinámica basada en interpolación de cadenas - El manejo seguro consiste en evitar crear atoms nuevos en tiempo de ejecución y, para valores conocidos, limitarse a tablas de búsqueda explícitas o a la familia
to_existing_atom, además de revisarlo con un linter
Vulnerabilidades de denegación de servicio causadas por el agotamiento de atoms
- Entre los CVE publicados por EEF CNA, 35.8% corresponden a consumo no controlado de recursos, y en el ecosistema BEAM una gran parte proviene de problemas repetidos de agotamiento de atoms {p:36}
- La distribución actual puede verse en la página de Common Weaknesses de EEF CNA
- El agotamiento de atoms es una vulnerabilidad de denegación de servicio (DoS)
- Los atoms no pasan por recolección de basura
- Se almacenan en una tabla global de atoms
- Cuando la tabla se llena, la VM se cae
- Crear atoms a partir de valores no finitos, especialmente de la entrada del usuario, se convierte en un DoS potencial
- El riesgo no se limita solo a llamadas evidentes
- En Erlang:
binary_to_atom/1,list_to_atom/1 - En Elixir:
String.to_atom/1,List.to_atom/1
- En Erlang:
- También existen patrones de riesgo menos visibles
- Generación dinámica de atoms mediante interpolación en Erlang:
% Erlang: 보간을 통한 동적 atom 생성 list_to_atom("field_" ++ UserInput) - Decodificar claves JSON como atoms en Elixir:
# Elixir: JSON을 atom 키로 디코딩 Jason.decode(json, keys: :atoms) - Generación dinámica de atoms mediante interpolación en Elixir:
# Elixir: 보간을 통한 동적 atom 생성 :"field_#{user_input}"
- Generación dinámica de atoms mediante interpolación en Erlang:
Formas seguras de manejo y qué revisar
- Las vulnerabilidades por agotamiento de atoms no son simple descuido; aparecen con frecuencia en código que asume que la entrada está controlada o es finita
- El esquema de URI es un ejemplo representativo
- Puede parecer que solo hay unos pocos esquemas por manejar
- Si el valor proviene de una entrada externa, ya no se puede garantizar que el conjunto posible siga siendo finito
- El código que crea atoms a partir de entrada no es seguro, salvo cuando el conjunto de valores posibles es finito, conocido y forzado
- El enfoque más seguro es no crear atoms nuevos en tiempo de ejecución
- Si se conocen los valores permitidos, es más seguro usar una tabla de búsqueda explícita
% Erlang case Scheme of <<"http">> -> http; <<"https">> -> https; _ -> error end - Cuando una tabla de búsqueda no es práctica, se deben usar variantes que solo empleen atoms existentes, sin crear nuevos
- Estas funciones no crean atoms nuevos y lanzan un error
% Erlang binary_to_existing_atom(Value) list_to_existing_atom(Value)# Elixir String.to_existing_atom(value) List.to_existing_atom(value) - Los linters ayudan a detectar patrones de riesgo antes de que se conviertan en vulnerabilidades
- En proyectos de Elixir, se puede considerar activar Credo.Check.Warning.UnsafeToAtom de Credo
- Esta revisión marca llamadas inseguras a
String.to_atom/1,List.to_atom/1,Module.concat/1,2yJason.decode/2conkeys: :atoms - Esa revisión está desactivada de forma predeterminada
- Los mantenedores de proyectos Erlang o Elixir deberían buscar código que genere atoms a partir de binarios, cadenas, claves JSON, componentes de URI, encabezados y valores de configuración
- Esta categoría de vulnerabilidad es una de las más fáciles de corregir antes de que termine convertida en un CVE
- Hay más lineamientos en la guía para prevenir el agotamiento de atoms del EEF Security Working Group
1 comentarios
Opiniones en Lobste.rs
Suena similar a cómo era la situación en Ruby antes de que
Symbolfuera recolectado por basuraNo entiendo el título. Esto definitivamente parece un footgun
Si piensas “¿Ruby no tiene symbols parecidos a los atoms de Erlang?”, sí, pero Ruby sí recolecta los symbols con garbage collection
Además, por defecto la tabla de consulta donde se almacenan los atoms de Erlang solo permite un máximo de 1,048,576
Si generas atoms dinámicamente a partir de entrada de usuario como la de un formulario, es muy peligroso y el software queda expuesto a ataques de denegación de servicio
Aun así, por mi experiencia, “footgun” ya es una expresión bastante amplia, así que de cualquier forma la redacción del título suena rara
Me sorprende porque suena a que alguna parte fundamental del diseño o de la implementación es mala. Más aún porque es un lenguaje que siempre veía muy elogiado en internet
Agregar conteo de referencias a esa tabla sería costoso y cambiaría las características de escalabilidad de código que ha existido durante décadas
El número máximo de atoms es por defecto de 1 millón y se define al iniciar la VM
Es una trampa, pero no es difícil de evitar. Desde hace mucho la recomendación es “no crees atoms a partir de entrada de usuario”
Por ejemplo, si parseas JSON, normalmente no conviertes las claves a atoms, o solo las conviertes si ya existe ese atom. Así puedes hacer pattern matching con claves atom, esos atoms ya fueron creados al cargar el código y las cláusulas generales pueden recibir cadenas en vez de atoms
Elixir es mucho más popular, así que es probable que un desarrollador de Erlang lo sepa, pero uno de Elixir quizás no
Personalmente, usar atoms de esa manera ya me parece raro. Porque entiendo los atoms de Erlang más o menos como el tipo enum de C
Lo veo como una función de conveniencia donde, si escribes una palabra de cierta manera, internamente se vuelve un enum
El artículo menciona entrada de usuario, pero no entiendo por qué habría un caso de uso donde quisieras crear nuevos tipos enum a partir de entrada de usuario. Parece tener una utilidad muy limitada
Los comentarios de al lado hablan de parsing, pero idealmente ¿no estarías parseando una estructura de datos ya conocida? Siento que me falta algo
En ese lenguaje, hay al menos dos ventajas de tener symbols como un tipo separado más allá de comparaciones rápidas de igualdad. Un symbol es un atom, o sea una unidad atómica, no una secuencia tipo lista de caracteres, así que varios operadores lo tratan de forma distinta, y los symbols están vectorizados y pueden almacenarse de forma compacta en listas de un solo tipo
En K y Q, es muy deseable representar las columnas de tablas de base de datos como tipos vectorizados. Tienen buena localidad, usan la memoria con más eficiencia y muchos operadores tienen rutas rápidas para ellos. Pero por las restricciones de la tabla de symbols, hay que tener cuidado al usar symbols en columnas de alta cardinalidad
Si parseas JSON con un esquema conocido, los symbols son excelentes como claves de diccionario y en k2/k3 son prácticamente obligatorios. Pero si es JSON de procedencia desconocida, no deberían venir de entrada de usuario
En algunos dialectos de K, la longitud de los symbols se limita a propósito para que puedan empaquetarse y moverse como valores de 64 bits. Se sacrifica generalidad, pero así ya no hace falta la tabla de symbols
La distinción entre “entrada controlada” y “entrada no controlada” en seguridad me recuerda un poco a cosas como si algo es
nullo noCuando ves entradas del tipo “
webpack-plugin-less-cssprovoca denegación de servicio si recibe un archivo CSS no confiable”, sí da bastante fatiga de CVEAun así, aquí estaría bien tener un mejor marcado de límites. Por ejemplo, si también pudiera manejar bien reglas de composición sobre qué propiedades de seguridad se conservan después de concatenar cadenas
Y si tomaste un montón de cosas recibidas por HTTP POST y las trataste todas como
SafeString, entonces eso ya es, hasta cierto punto, responsabilidad tuya