- En una realidad donde incluso funciones simples movilizan miles de dependencias y decenas de millones de líneas de código, la obesidad del software en sí misma se vuelve una causa importante de vulnerabilidades de seguridad
- La seguridad no depende solo de la densidad de bugs, sino también del volumen total de código al que un atacante puede llegar, y una superficie de ataque innecesariamente amplia puede derivar en compromisos reales
- Los ecosistemas de dependencias de Electron JS, Node.js, imágenes de Docker y npm·PyPI vuelven difusa la cantidad y procedencia del código desplegado, y hasta una app para abrir la puerta del garaje puede involucrar más de 50 millones de líneas de código activo
- Rust, los sanitizer y los fuzzer mejoran la calidad del código, pero los fallos lógicos de diseño como ejecutar automáticamente código dentro de documentos son difíciles de prevenir solo eliminando bugs
- Trifecta ofrece una función de compartir imágenes con 1,600 líneas de código nuevo, unas 5 dependencias clave y un tamaño total de 3 MB, mostrando que también es posible crear software moderno con código y dependencias limitados
El estado peligroso de la seguridad del software
- El estado reciente de la seguridad del software es muy malo
- En el último año hubo incidentes graves de compromiso en software estándar de la industria como Ivanti, MOVEit, Outlook, Confluence, Barracuda Email Security Gateway y Citrix NetScaler ADC·NetScaler Gateway
- Incluso empresas con muchos recursos como Apple y Google han cometido errores de seguridad que pusieron en riesgo a sus clientes
- Se ha generalizado el consejo de no ejecutar software directamente y dejarlo en manos de X as a service o de la nube, por la percepción de que el software se ha vuelto demasiado riesgoso
- También se está debilitando la premisa de que la nube vuelve confiable al software vulnerable
- La plataforma de correo de Microsoft fue hackeada, incluidos correos gubernamentales confidenciales
- Siguen existiendo preocupaciones sobre la seguridad de la nube de Azure
- Okta sufrió su segunda intrusión en menos de 2 años, y después continuaron de forma sospechosa los casos de hackeo a usuarios de Okta
- La UE está impulsando tres legislaciones para abordar la seguridad del software
- NIS2: dirigida a servicios importantes
- Cyber Resilience Act: dirigida a casi todo el software comercial y los dispositivos electrónicos
- Product Liability Directive: amplía el alcance de la responsabilidad para incluir software
Las vulnerabilidades surgen tanto de la calidad como de la cantidad de código
- La seguridad del software depende de dos ejes
- La densidad de problemas de seguridad en el código fuente
- La cantidad de código a la que los hackers pueden acceder
- Cuanto más código hay, mayor es el riesgo
- Incluso con baja densidad de bugs, en millones de líneas de código se pueden encontrar huecos explotables
- El caso de iMessage muestra qué problemas genera ampliar la superficie de ataque
- Incluso los iMessage no deseados se procesan de inmediato en el iPhone para generar una vista previa
- Apple admitía una variedad de formatos de imagen, e incluso entraban en el procesamiento PDF con fuentes comprimidas extrañas
- Ese formato antiguo en la práctica incluía un lenguaje de programación, y los atacantes podían usarlo para explorar otras debilidades del teléfono
- Apple podría haber reducido la superficie de ataque limitando las vistas previas a muchos menos formatos de imagen o a un solo formato “conocido y seguro”
- El EU Cyber Resilience Act también especifica que los proveedores deben minimizar la superficie de ataque
Un mejor código por sí solo no basta
- Ya existen movimientos para mejorar la calidad del código
- Lenguajes seguros en memoria como Rust
- Herramientas de refuerzo de seguridad como AddressSanitizer
- Fuzzer que alteran entradas automáticamente para encontrar vulnerabilidades y bugs
- Pero muchos problemas de seguridad surgen de la lógica subyacente más que de bugs en el código mismo
- La vulnerabilidad de correo de Barracuda se originó en que una biblioteca de terceros que escaneaba hojas de cálculo de Excel para detectar virus en realidad ejecutaba código
- La decisión de incluir una función que ejecuta automáticamente código dentro de documentos no se resuelve aunque se eliminen todos los bugs del código
No saber qué se está desplegando
- El software moderno se ha vuelto tan grande que resulta difícil saber qué se está desplegando realmente
- Niklaus Wirth criticó en 1995 en “A Plea for Lean Software” que el software ya había crecido a escala de megabytes
- Su sistema operativo Oberon pesaba 200 KB incluyendo editor y compilador
- Hoy hay proyectos cuyo archivo de configuración por sí solo supera los 200 KB
- Hoy en día, una app típica puede estar hecha sobre Electron JS
- Electron JS incluye Chromium y Node.js
- Node.js permite acceder a decenas de miles de paquetes de JavaScript
- Se estima que solo usar Electron JS ya puede implicar al menos 50 millones de líneas de código, incluidas las dependencias
- Las apps traen cientos o miles de paquetes auxiliares, y las dependencias a su vez arrastran más dependencias
- Qué entra exactamente en una compilación puede cambiar cada día
- Algunos paquetes pueden exponer por defecto a los usuarios ante anunciantes o brokers de datos
- Una app que controla dispositivos del hogar también puede conectarse a la pila de software de Amazon, y esa pila a su vez puede usar Node.js y múltiples dependencias
- Como resultado, incluso para abrir la puerta del garaje pueden estar funcionando más de 50 millones de líneas de código activo sobre varias imágenes de sistemas operativos y múltiples servidores
La carga de los contenedores y la cadena de suministro de dependencias
- Antes se distribuían salidas de compilador o conjuntos de archivos interpretables, y durante la instalación y configuración había que pensar qué venía dentro del paquete
- Hoy muchas veces se distribuye con contenedores no solo el software, sino también archivos del sistema operativo para igualar el entorno de ejecución
- En la práctica, con frecuencia se termina distribuyendo una imagen de disco de una computadora completa
- En Docker Hub abundan imágenes que superan los 350 MB
- Los contenedores pueden usarse para buenos fines, pero según cómo se usen pueden aumentar mucho la cantidad de código desplegado
- Tampoco está claro si las actualizaciones de seguridad de las dependencias llegan hasta la app final
- Es difícil saber si bugs de procesamiento de imágenes que Google y Apple corrigieron con urgencia siguen presentes en apps de Electron
- El ecosistema npm tiene antecedentes de toma de control de repositorios de paquetes, secuestro y reaparición de paquetes con el mismo nombre
- PyPI ha sufrido problemas parecidos
- Las dependencias necesitan revisión, pero es difícil esperar que alguien verifique miles de ellas con frecuencia
- Reimplementar todo por cuenta propia tampoco es la respuesta, y hay buenos módulos como SQLite que probablemente sean más seguros que una implementación propia
Trifecta: una herramienta para compartir imágenes hecha con poco código
- Trifecta es un software independiente para compartir imágenes minimalista pero realmente utilizable
- Permite compartir imágenes fácilmente arrastrándolas y soltándolas en el navegador
- Si se usa imgur, se instalan muchas cookies y rastreadores en el navegador, y quienes ven las imágenes compartidas también pueden quedar forzados a aceptar rastreadores
- También se consideró que muchas herramientas de self-hosting para compartir imágenes se basan en frameworks grandes y son difíciles de confiar
- Trifecta fue hecho pequeño para que todo el código pudiera revisarse en unas pocas horas
- 1,600 líneas de código fuente nuevo
- Unas 5 dependencias importantes
- 3 MB de tamaño total de código
- Otra solución para compartir imágenes que se usó como comparación se distribuía como una imagen Docker de 288 MB
- Otra solución para compartir fotos basada en Node tenía 1,600 dependencias y más de 4 millones de líneas de JavaScript
- Trifecta no está pensado para un sitio público donde cualquiera suba imágenes, sino para uso empresarial o personal
El problema de confundir complejidad con poder
- Una reacción común ante Trifecta fue recomendar desplegarlo usando un paquete de Amazon Web Services
- Esa reacción no encaja con el objetivo de un software independiente que no dependa de servicios externos
- También hubo respuestas diciendo que se estaba tratando a Docker de manera injusta, aunque se reconoce que los contenedores pueden usarse con buenos fines
- Niklaus Wirth señaló en su artículo de 1995 que la gente tiende a confundir complejidad con sofisticación
- Como dijo Tony Hoare, hay dos formas de diseñar software
- Hacer un programa tan simple que sea evidente que no tiene errores
- Hacerlo tan complejo que no parezca evidente que tiene errores
- Wirth veía la presión de tiempo como una causa principal del software sobredimensionado
- La presión de tiempo obstaculiza la planificación cuidadosa
- Lleva a agregar y modificar rápido en vez de mejorar una solución aceptable
- Va deteriorando poco a poco el estándar de calidad y el sentido de acabado de los ingenieros
- La explosión del software no es una ley natural, sino algo que la ingeniería de software debería reducir
Reducir la cantidad de código como medida de seguridad
- El mundo actual está desplegando demasiado código
- La mayor parte es código de terceros
- Parte se incluye sin intención
- La mayor parte no se revisa lo suficiente
- El resultado es una enorme superficie de ataque, dentro de la cual queda expuesta una gran cantidad de código de calidad apenas promedio
- Los esfuerzos por mejorar la calidad del código continúan, pero muchos exploits provienen de fallos lógicos, y el progreso para detectarlos sigue siendo relativamente limitado
- Solo con reducir la cantidad de código expuesta al mundo ya se puede lograr una gran mejora
- El tiempo de salida al mercado puede aumentar, pero la legislación que viene podría obligar a los proveedores a tomar la seguridad más en serio
- Trifecta y Oberon muestran que incluso con código y dependencias limitados se puede ofrecer mucha funcionalidad
1 comentarios
Comentarios de Hacker News
En A Deepness in the Sky, de Vernor Vinge, la humanidad se ha extendido entre las estrellas usando solo tecnología sublumínica, y las naves interestelares aparecen como una mezcla de tecnologías antiguas de varios sistemas estelares y civilizaciones.
Los sistemas informáticos también han evolucionado durante tanto tiempo que ya casi nadie entiende la mayor parte del código; simplemente lo usan y vuelven a construir encima.
En particular, uno de los personajes es un antiguo ingeniero de sistemas que, tras repetir durante muchísimo tiempo ciclos de suspensión y viajes, está entre los humanos vivos más viejos; y en un futuro donde todos han construido múltiples capas encima de aquello, conocer cómo funcionaban las cosas en su época y sus vulnerabilidades se vuelve una gran ventaja.
Creo que Vinge dio justo en el clavo.
Lo primero es un verdadero misterio que deben resolver la ciencia moderna y personas muy inteligentes, pero lo segundo se acerca más a una falta de interés.
Si pagas lo suficiente, un ingeniero competente desmontará la lavadora y encontrará la falla exacta, pero nadie pagará ese costo: simplemente la tirará y comprará una nueva.
El conocimiento sobre software del pasado claramente cae en lo segundo. Si uno se mete a fondo en cualquier parte, al final puede entenderla por completo, pero para la mayoría resulta mucho más barato y práctico ignorarla o agregar otra capa encima.
Trata de un futuro en el que la humanidad olvidó la aritmética básica y, cuando alguien la redescubre, los poderosos intentan usarla para la guerra. Entiendo el mensaje que quiere transmitir, pero creo que la premisa es tan irrealmente ridícula que le quita fuerza.
Para más discusión, consulta http://lambda-the-ultimate.org/node/4424
Ya estamos viviendo este problema. Mi tío, que está en sus 60, mantiene software antiguo de transporte de carga hecho en COBOL, y también existen trabajos con tecnologías obsoletas como esa. Si te interesa, te puedo presentar.
El problema de fondo es el mismo que el del caso de left-pad. Nos burlamos de un ingeniero junior sin supervisión que instala dependencias de cualquier manera, pero después de décadas y varias generaciones de desarrolladores, al final la mayor parte del software termina apoyándose, en cierta medida, en dependencias desconocidas.
Imagina que en 2100 tienes que desplegar una actualización: la vas a empujar a través del sistema de gestión de dependencias de npm de esa época. Al mismo tiempo, podría haber dispositivos dependientes que necesitan actualizaciones de seguridad a escala del sistema solar, billones de dispositivos y cachés intermedias de las que no se sabe si están al día. Ni siquiera puedo imaginar cómo se vería un árbol de dependencias así.
También es frustrante trabajar en un entorno donde hay que escarbar en el código del framework para llegar al núcleo. Se siente como una pérdida de tiempo.
La hinchazón se ve en la mayoría de las bibliotecas de npm. Los autores no conocen el buen diseño y quieren que todas las bibliotecas hagan de todo.
Es como decir que una biblioteca convierte codificaciones de strings, pero meter en el mismo repositorio carga y guardado de archivos, descargas de Internet y hasta una herramienta de línea de comandos. Una biblioteca debería hacer una sola cosa y dejar el resto al usuario.
El lado de Rust no parece estar mejor. Si intentas arreglar la documentación de Rust, verás que se instalan como 1000 crates.
El problema no es el lenguaje, sino que cualquiera puede subir una biblioteca y, en la práctica, cualquiera lo hace. La gente que “solo quiere terminar el trabajo” elige la biblioteca con más funcionalidades y pide todavía más para no escribir código que serían 3 líneas fuera de la biblioteca. Algo como: “¿Podrían agregar también renderizado de PDF?”.
No sé bien cuál es la solución, pero se me ocurre crear un grupo de defensa de Low Dependency y una insignia, para que los autores de bibliotecas quieran tenerla y los usuarios también la busquen al elegir una biblioteca.
En cambio, si quieres una biblioteca con pocas dependencias, traerás unas pocas que hacen muchas cosas.
Desde mi punto de vista, prefiero una biblioteca algo más gruesa, con pocas dependencias, hecha por un autor en quien confío. Lodash es grande, pero su versión de módulos ES6 soporta tree shaking y, en la práctica, funciona como la biblioteca estándar que JavaScript no tenía. date-fns cumple un papel parecido para Date. Para cubrir los huecos de las bibliotecas centrales de JavaScript, casi siempre incluyo estas dos por defecto en cualquier proyecto.
Hace tiempo hice trabajo por contrato con Ruby on Rails, y los problemas de rendimiento eran tan graves que estábamos desarrollando en modo release. El servidor ni siquiera podía detectar cambios de archivos y recargar automáticamente.
Un día me harté y empecé a investigar; había tantas gemas incluidas que ni recuerdo cuántas eran. Una de ellas existía literalmente para ahorrarse 3 líneas de código.
Después de eso me alejé de la comunidad de RoR. Hace poco, después de varios años, volví a tomar un contrato con RoR; no está tan mal como antes, pero sigue sin estar bien.
Algunas comunidades no respetan en absoluto el riesgo que traen las dependencias.
Por otro lado, es realmente cómodo que cualquier repositorio Git pueda hospedar una biblioteca de Go y que cualquiera pueda usarla desde esa URL.
Primero, los creadores de paquetes quieren convertir esto en una carrera, y la única forma de llamar la atención es crear montones de paquetes. Entonces siguen creando paquetes que dependen de otros paquetes suyos e intentan meter una o dos cosas útiles en el código de otras personas.
Segundo, están quienes creen que resolver un problema siempre implica incluir un paquete nuevo. No les importa cuántas dependencias tenga ni si el problema real es difícil. Así que, en vez de aprender a resolver el problema, aprenden la API de un wrapper con 4 estrellas en GitHub.
Lo ideal sería que una herramienta así dedicara el tiempo tedioso a podar paquetes innecesarios, minimizar la responsabilidad de cada paquete, ordenar el entorno de forma razonable y luego cachearlo de manera independiente, eliminando la dependencia del LLM. Lo mejor sería llamar a un verificador de actualizaciones o a un curador solo cuando surja un problema.
Sinceramente, creo que es uno de los peores problemas del software moderno y que vuelve inutilizables más del 50% de los proyectos. Es un problema aburrido pero resoluble, justo ideal para un agente LLM bien hecho, y sería de gran ayuda tenerlo.
“¿Has visto un avión moderno? ¿Has seguido cómo evolucionan sus líneas año tras año? ¿Has pensado que, no solo en los aviones sino en todo lo que fabrica el ser humano, el esfuerzo industrial humano, los cálculos y las noches pasadas sobre planos acaban desembocando en un único principio dominante: la simplicidad definitiva?
Es como si hubiera una ley natural que ordenara que, para pulir la curva de un mueble, la quilla de un barco o el fuselaje de un avión hasta acercarlos a la pureza primordial de las curvas del pecho o los hombros humanos, hicieran falta experimentos de varias generaciones de artesanos. La perfección parece alcanzarse no cuando ya no queda nada por añadir, sino cuando ya no queda nada por quitar”.
— Antoine de Saint Exupéry, Terre des Hommes
Cuando lo escribes como “abrir la puerta de un garaje puede requerir más de 50 millones de líneas de código activo e imágenes de sistema operativo en varios servidores”, de verdad suena a locura.
Me marea pensar cuánto código está ejecutándose en la máquina en la que escribo esto ahora. Código que nunca he revisado y que probablemente casi no ha pasado por una revisión estricta.
Bueno, me voy a instalar dependencias de npm otra vez.
La analogía de que “el software ahora se considera tan peligroso que se le dice a la gente que no lo ejecute directamente. En cambio, se les dice que se lo dejen a un proveedor de ‘X as a service’ o simplemente a la ‘nube’. Compárese con una situación hipotética en la que los autos se incendian tan seguido que el consejo es no manejar uno mismo, sino dejarle el manejo a un experto al que siempre sigue un bombero profesional” es tan buena que dan ganas de usarla
Una exnovia desconfiaba de la “nube” por razones racionales, al haber crecido en el antiguo bloque del Este. Pero la alternativa era simplemente esperar no perder su laptop HP comprada al precio más bajo. Después de un poco de educación, al menos en esa parte se quedó tranquila
El problema es la falta general de educación y la falta de consideración sobre sus consecuencias. Al final hay que aceptar el riesgo, aprender por cuenta propia o apoyarse en empresas SaaS y de nube. He visto muchas lágrimas y no he visto mucha gente que aprenda por sí misma
Es una cuestión de responsabilidad personal, pero como nadie quiere asumir esa responsabilidad, dejárselo a expertos puede ser una solución menos mala que confiar en uno mismo. La respuesta correcta es la educación, pero es desesperadamente difícil
El software no puede volverse más esbelto. Para eso se necesita tiempo, habilidad y personal caro, no solo gente que arme un traje de Frankenstein pegando ejemplos de 12 stacks tecnológicos distintos
Soy desarrollador independiente, y alguien que aprendió node.js el año pasado y puede ensamblar en un día node.js, contenedores, algún servicio de base de datos alojada de AWS, Lambda, almacenamiento de objetos, Cloudflare, YAML, React, Vite y otras dependencias para crear una webapp cliché pero todavía vulnerable siempre cotiza más barato que yo
Aunque el software esbelto, rápido, barato de ejecutar y también más barato de mantener sea más económico a largo plazo, es difícil escribirlo de forma rentable
Antes existía el sueño de que el sistema ofreciera hooks y rutinas estándar que todos usarían para interfaces y cosas similares. Piensen en Macintosh Toolbox o QuickDraw
Se decía que el trabajo principal del desarrollador era escribir la lógica del programa, y que los cambios o agregados debían ser transparentes. Las llamadas al sistema realizarían sin fricción la misma tarea aunque el código interno cambiara, y las nuevas funciones serían un superconjunto de las existentes, de modo que el código viejo compilaría o se ejecutaría sin problemas mientras el software nuevo obtendría más capacidades
Se creía que esto facilitaría el mantenimiento, normalizaría las interfaces y, al depender mucho de las llamadas al sistema, también haría el código más esbelto. Había un ambiente de que debían evitarse las bibliotecas externas
Ese sueño se derrumbó rápido; basta pensar en las DLL. Mucha de la gestión de paquetes y del empaquetado actuales parece tratarse de garantizar que existan las bibliotecas correctas
En ese momento, el desarrollo de software a gran escala todavía estaba casi en su infancia, así que se entiende que no haya salido como se esperaba. Ahora se ha acumulado mucha experiencia colectiva sobre estos problemas, y me pregunto si se llegó a la conclusión de que ese sueño es irrealizable de forma sensata, o si haber sufrido lo suficiente el estado desordenado actual nos está llevando a un reintento moderno
Si queremos software rápido, esbelto, estable y seguro, aunque sea difícil obtener las cuatro cosas a la vez, no tengo muy claro que la situación actual esté yendo en esa dirección
El Lisp temprano ponía muy poco en el lenguaje, pero Raku empuja a la especificación del lenguaje incluso cosas menores que podrían ir como bibliotecas de npm
C dejaba que uno decidiera cómo compilar el código, pero la mayoría de los lenguajes compilados nuevos vienen con alguna forma de herramienta de build
Hay cosas bastante efectivas en este panorama, pero ocurren fuera del contexto “tú, la máquina, un proyecto nuevo” que guiaba el trabajo de Wirth. El problema es que tienden a traer consigo el umbral de dependencias enormes como bases de datos o motores de navegador, y si no te gusta cómo se crearon esas dependencias, al final terminas siendo infeliz
Esto es lo que digo todo el tiempo sobre Rust
Si el 70% de las vulnerabilidades antiguas de C++ realmente están relacionadas con la memoria, entonces quizá pueda tener 70% menos vulnerabilidades por línea de código que C++
Pero si en Rust traes cientos de paquetes y la cantidad de líneas de código se multiplica por 10, la historia cambia
El 30% de 100 mil líneas es más en total que el 100% de 10 mil líneas
Si algo como QT estuviera escrito en Rust, por sí solo tendría cientos de crates, pero la cantidad de código y el nivel de riesgo asumido serían exactamente los mismos
Pero el gran problema son las vulnerabilidades. ¿Es mejor corregir un bug en una biblioteca compartida y así arreglar cientos de bibliotecas, o corregir cientos de bibliotecas una por una?
El hecho de que Rust facilite traer muchas dependencias pequeñas en lugar de unas pocas dependencias enormes es irrelevante. No significa que se escriba más código
Por ejemplo, ¿se cuenta el crate
regexde Rust como dependencia? En C++ eso está en la biblioteca estándar¿Se cuenta Boost como una sola dependencia en C++? En Rust eso equivaldría a unas 30 crates separadas
¿Cuánta ejecución remota de código ha habido en programas Rust en comparación con C++? Sospecho que la frecuencia de ejecución remota de código en C++ supera por mucho más del 70% a la de Rust
Se dice que hoy en día las apps suelen hacerse con Electron JS, pero parece que no se sabe lo suficiente que también se pueden usar los controles web nativos de cada plataforma sin empaquetar Electron
Al hacerlo, la app distribuida puede quedar en el orden de kilobytes. Este enfoque da libertad para usar cualquier lenguaje de backend o stack tecnológico, siempre que pueda comunicarse con la vista web
¿No deberían ser posibles las PWA para una proporción bastante grande de aplicaciones hoy en día?
No estoy muy seguro, pero ¿Discord no podría ser una PWA en vez de una app de Electron?
La mayor brecha probablemente sea la diferencia entre algo potente equivalente a SQLite e IndexedDB, pero aun así creo que la mayoría de las apps no necesitan necesariamente un lenguaje de consultas de más alto nivel que el modelo B-tree de IndexedDB
Otro aplauso para la filosofía suckless. Viva
[0 ]https://suckless.org/