Skip to main content

10 buenas prácticas de seguridad en Java

Cheat sheet header java

17 de septiembre de 2019

0 minutos de lectura

En esta edición de nuestra hoja de referencia, nos enfocaremos en diez buenas prácticas de seguridad en Java para quienes mantienen proyectos de código abierto y para desarrolladores. Esta hoja de referencia es una colaboración entre Brian Vermeer, Developer Advocate de Snyk, y Jim Manico, Java Champion y fundador de Manicode Security.

Te recomendamos imprimir la hoja de referencia y leer más sobre cada uno de los 10 consejos de seguridad en Java, que analizamos con más detalle a continuación.

¡Comencemos!

Problemas de seguridad en Java

Aunque todos nos esforzamos por escribir código de calidad, la seguridad en Java no siempre forma parte de la mentalidad de un desarrollador. Sin embargo, prevenir los problemas de seguridad en Java debería ser tan importante como hacer que tu aplicación Java sea eficiente, escalable y fácil de mantener.

También debes estar al tanto de las vulnerabilidades más recientes en Java, como Log4Shell (CVE-2021-44228), divulgada en diciembre de 2021 y que afectaba a las aplicaciones que ejecutaban versiones vulnerables de Log4j de Apache.

En esta hoja de referencia, analizamos 10 problemas comunes de seguridad en Java. Te daremos consejos prácticos y ejemplos sobre cómo prevenir estas vulnerabilidades comunes de seguridad en Java en las aplicaciones que desarrollas.

1. Usa parámetros en las consultas para prevenir la inyección

En la versión de 2017 de las 10 principales vulnerabilidades de OWASP, la inyección encabezó la lista como la vulnerabilidad número uno de ese año. En un caso típico de inyección SQL en Java, los parámetros de una consulta SQL se concatenan de forma ingenua con la parte estática de la consulta. A continuación se muestra una ejecución insegura de SQL en Java que un atacante puede usar para obtener más información de la prevista.

public void selectExample(String parameter) throws SQLException {
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);
   String query = "SELECT * FROM USERS WHERE lastname = " + parameter;
   Statement statement = connection.createStatement();
   ResultSet result = statement.executeQuery(query);

   printResult(result);
}

Si el parámetro de este ejemplo es algo como '' OR 1=1, el resultado contiene todos los elementos de la tabla. Esto podría ser aún más problemático si la base de datos admite varias consultas y el parámetro fuera ''; UPDATE USERS SET lastname=''.

Para prevenir este riesgo de seguridad en Java, debemos parametrizar las consultas mediante una instrucción preparada. Esta debería ser la única forma de crear consultas a la base de datos. Al definir todo el código SQL y pasar los parámetros a la consulta después, el código es más fácil de entender. Y, lo que es más importante, al distinguir entre el código SQL y los datos de los parámetros, se evita que una entrada maliciosa secuestre la consulta. 

public void prepStatmentExample(String parameter) throws SQLException {
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);
   String query = "SELECT * FROM USERS WHERE lastname = ?";
   PreparedStatement statement = connection.prepareStatement(query);
   statement.setString(1, parameter);
   System.out.println(statement);
   ResultSet result = statement.executeQuery();

   printResult(result);
}

En el ejemplo anterior, la entrada se vincula al tipo String y, por lo tanto, forma parte del código de la consulta. Esta técnica evita que la entrada del parámetro interfiera con el código SQL.

Para obtener más información sobre cómo prevenir la inyección SQL, consulta esta práctica guía: Hoja de referencia sobre inyección SQL: 8 buenas prácticas para prevenir ataques de inyección SQL

2. Usa OpenID Connect con 2FA

La gestión de identidades y el control de acceso son difíciles, y la autenticación deficiente suele ser la causa de las filtraciones de datos. De hecho, este es el puesto número 2 en la lista de las 10 principales vulnerabilidades de OWASP y, por lo tanto, un importante riesgo de seguridad en Java. Hay muchos aspectos que debes tener en cuenta al crear tu propio sistema de autenticación: almacenamiento seguro de contraseñas, cifrado sólido, recuperación de credenciales, etc. En muchos casos, es mucho más fácil y seguro usar soluciones existentes como OpenID Connect. OpenID Connect (OIDC) te permite autenticar usuarios en distintos sitios web y aplicaciones. Así, no tienes que administrar archivos de contraseñas. OpenID Connect es una extensión de OAuth 2.0 que proporciona información del usuario. Además de un token de acceso, agrega un token de ID y un endpoint /userinfo donde puedes obtener información adicional. También agrega una función de descubrimiento de endpoints y el registro dinámico de clientes.

Configurar OpenID Connect con bibliotecas como Spring Security es una tarea sencilla y habitual. Asegúrate de que tu aplicación exija 2FA (autenticación de dos factores) o MFA (autenticación multifactorial) para agregar una capa adicional de seguridad a tu sistema.

Al agregar las dependencias oauth2-client y Spring Security a tu aplicación Spring Boot, puedes aprovechar clientes de terceros como Google, GitHub y Okta para gestionar OIDC. Después de crear tu aplicación, solo tienes que conectarla al cliente específico que elijas, indicándolo en la configuración de la aplicación; puede ser el client-id y el client-secret de GitHub o Okta, como se muestra a continuación.

pom.xml

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

application.yaml

spring:
 security:
   oauth2:
     client:
         registration:
           github:
             client-id: 796b0e5403be4729ca01
             client-secret: f379318daa27502254a05e054361074180b840a9
           okta:
             client-id: 0oa1a4wascEpYu6yk358
             client-secret: hqxj7a9lVe_TudbS2boBW7AWwxTlZiHNrJxdc_Sk
             client-name: Okta
         provider:
           okta:
             issuer-uri: https://dev-844689.okta.com/oauth2/default

3. Analiza tus dependencias para detectar vulnerabilidades conocidas

Es muy probable que no sepas cuántas dependencias directas usa tu aplicación. También es muy probable que no sepas cuántas dependencias transitivas usa. Esto suele ocurrir aunque las dependencias representen la mayor parte de tu aplicación. Los atacantes apuntan cada vez más a las dependencias de código abierto, ya que su reutilización les permite llegar a muchas víctimas. Es importante asegurarse de que no haya vulnerabilidades de seguridad en Java conocidas en todo el árbol de dependencias de tu aplicación.

Snyk analiza los artefactos de compilación de tu aplicación y señala las dependencias que tienen vulnerabilidades conocidas. En un panel, te proporciona una lista de las vulnerabilidades de seguridad en Java presentes en los paquetes que usa tu aplicación.

Lista de proyectos de Snyk que muestra tres repositorios con cantidades altas, medias y bajas de problemas, totales de dependencias, horas de actualización y plataformas de origen

Además, sugiere versiones actualizadas o proporciona parches para corregir los problemas de seguridad mediante un pull request en tu repositorio de código fuente. Snyk también protege tu entorno al garantizar que todos los pull requests futuros que se creen en tu repositorio se analicen automáticamente (mediante webhooks) para comprobar que no introduzcan nuevas vulnerabilidades conocidas.

Snyk está disponible tanto en una interfaz web como en una CLI, para que puedas integrarlo en tu entorno de CI y configurarlo para que detenga la compilación cuando detecte vulnerabilidades con una gravedad superior al umbral que establezcas.

Usa Snyk gratis en proyectos de código abierto o en proyectos privados con una cantidad limitada de análisis mensuales.

4. Maneja con cuidado los datos confidenciales

Exponer datos confidenciales, como datos personales o números de tarjetas de crédito de tus clientes, puede ser perjudicial. Pero incluso un caso más sutil puede tener consecuencias igual de graves. Por ejemplo, exponer identificadores únicos en tu sistema supone una vulnerabilidad de seguridad en Java si ese identificador se puede usar en otra llamada para obtener datos adicionales.

En primer lugar, debes revisar detenidamente el diseño de tu aplicación y determinar si realmente necesitas esos datos. Además, asegúrate de no exponer datos confidenciales, por ejemplo, a través de registros, autocompletado o transmisiones de datos.

Una forma sencilla de evitar que los datos confidenciales terminen en tus registros es depurar los métodos toString() de las entidades de tu dominio. Así, no imprimirás accidentalmente los campos confidenciales. Si usas el proyecto Lombok para generar tu método toString(), prueba con @ToString.Exclude para evitar que un campo forme parte de la salida de toString().

También debes tener mucho cuidado al exponer datos al mundo exterior. Por ejemplo, si tenemos un endpoint en un sistema que muestra todos los nombres de usuario, no es necesario mostrar el identificador único interno. Ese identificador único podría usarse para vincular al usuario con otra información más confidencial mediante otros endpoints. Si usas Jackson para serializar y deserializar POJO en JSON, prueba con @JsonIgnore y @JsonIgnoreProperties para evitar que estas propiedades se serialicen o deserialicen.

Si necesitas enviar datos confidenciales a otros servicios, cífralos correctamente y asegúrate, por ejemplo, de que la conexión esté protegida con HTTPS.

5. Depura todas las entradas

El cross-site scripting (XSS) es un problema conocido y se usa principalmente en aplicaciones JavaScript. Sin embargo, Java tampoco es inmune. XSS no es más que una inyección de código JavaScript que se ejecuta de forma remota. Según OWASP, la regla número 0 para prevenir XSS es: “Nunca insertes datos que no sean de confianza, excepto en ubicaciones permitidas”. La solución básica a este riesgo de seguridad en Java es evitar los datos que no sean de confianza en la medida de lo posible y depurar todo lo demás antes de usarlo. Un buen punto de partida es la biblioteca de codificación de Java de OWASP, que ofrece muchos codificadores.

<dependency>
   <groupId>org.owasp.encoder</groupId>
   <artifactId>encoder</artifactId>
   <version>1.2.2</version>
</dependency>
String untrusted = "<script> alert(1); </script>";
System.out.println(Encode.forHtml(untrusted));

// output: <script> alert(1); </script>

Depurar el texto que ingresa el usuario es algo obvio. Pero ¿qué pasa con los datos que recuperas de una base de datos, incluso si es tu propia base de datos? ¿Y si alguien vulneró tu base de datos e insertó texto malicioso en un campo o documento?

También debes vigilar los archivos entrantes. La vulnerabilidad Zip-slip existe en muchas bibliotecas porque no se depuraba la ruta de los archivos comprimidos. Los archivos ZIP que contienen archivos con rutas como ../../../../foo.xy se podrían extraer y potencialmente sobrescribir archivos arbitrarios. Aunque no se trata de un ataque XSS, es un buen ejemplo de por qué debes depurar todas las entradas. Cualquier entrada podría ser maliciosa y debe depurarse según corresponda.

6. Configura tus analizadores XML para prevenir XXE

Con XML External Entity (XXE) habilitado, es posible crear un XML malicioso, como el que se muestra a continuación, y leer el contenido de cualquier archivo de la máquina. No sorprende que los ataques XXE formen parte de la lista de las 10 principales vulnerabilidades de OWASP y sean una vulnerabilidad de seguridad en Java que debemos prevenir. Las bibliotecas XML de Java son especialmente vulnerables a la inyección XXE porque, de forma predeterminada, la mayoría de los analizadores XML tienen habilitadas las entidades externas. 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE bar [
       <!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<song>
   <artist>&xxe;</artist>
   <title>Bohemian Rhapsody</title>
   <album>A Night at the Opera</album>
</song>

Una implementación ingenua de DefaultHandler y del analizador SAX de Java, como la que se muestra a continuación, analiza este archivo XML y revela el contenido del archivo passwd. Aquí se usa el caso del analizador SAX de Java como ejemplo principal, pero otros analizadores, como DocumentBuilder y DOM4J, tienen un comportamiento predeterminado similar.

SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();

DefaultHandler handler = new DefaultHandler() {

    public void startElement(String uri, String localName,String qName,Attributes attributes) throws SAXException {
        System.out.println(qName);
    }

    public void characters(char ch[], int start, int length) throws SAXException {
        System.out.println(new String(ch, start, length));
    }
};

Cambiar la configuración predeterminada para impedir las entidades externas y los doctypes en xerces1 o xerces2, respectivamente, previene este tipo de ataques.

...
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();

factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
saxParser.getXMLReader().setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 
... 

Para obtener información práctica sobre cómo prevenir la inyección XXE maliciosa, consulta la hoja de referencia de OWASP sobre XXE. También puedes ver mi video sobre cómo prevenir ataques de inyección de entidades externas (XXE).

Java XML Security: How to prevent External Entity (XXE) Injection attacks

7. Evita la serialización de Java

La serialización en Java nos permite transformar un objeto en un flujo de bytes. Este flujo de bytes se guarda en el disco o se transporta a otro sistema.  En sentido inverso, se puede deserializar un flujo de bytes para recrear el objeto original.

El mayor problema está en la deserialización. Por lo general, se ve así:

ObjectInputStream in = new ObjectInputStream( inputStream );
return (Data)in.readObject();

No hay forma de saber qué estás deserializando antes de decodificarlo.  Es posible que un atacante serialice un objeto malicioso y lo envíe a tu aplicación.  Cuando llamas a readObject(), los objetos maliciosos ya se instanciaron. Podrías pensar que este tipo de ataques es imposible porque necesitas tener una clase vulnerable en tu classpath. Sin embargo, si consideras la cantidad de clases en tu classpath —que incluye tu propio código, bibliotecas de Java, bibliotecas de terceros y frameworks—, es muy probable que haya una clase vulnerable disponible. 

A la serialización de Java también se la conoce como “el regalo que no deja de dar problemas”, debido a los numerosos problemas que ha causado a lo largo de los años. Oracle planea eliminar la serialización de Java con el tiempo como parte de Project Amber. Sin embargo, esto podría tardar y es poco probable que se corrija en versiones anteriores. Por lo tanto, es recomendable evitar la serialización de Java tanto como sea posible. Si necesitas implementar Serializable en tus entidades de dominio, lo mejor es implementar tu propio readObject(), como se muestra a continuación. Esto previene la deserialización.

private final void readObject(ObjectInputStream in) throws java.io.IOException {
   throw new java.io.IOException("Deserialized not allowed");
}

Si necesitas deserializar un InputStream por tu cuenta, debes usar un ObjectsInputStream con restricciones. Un buen ejemplo es ValidatingObjectInputStream de Apache Commons IO. Este ObjectInputStream comprueba si el objeto que se deserializa está permitido.

FileInputStream fileInput = new FileInputStream(fileName);
ValidatingObjectInputStream in = new ValidatingObjectInputStream(fileInput);
in.accept(Foo.class);

Foo foo_ = (Foo) in.readObject();

Los problemas de deserialización de objetos no se limitan a la serialización de Java. La deserialización de JSON a objetos Java puede presentar problemas similares. En la publicación del blog «Vulnerabilidad de deserialización de Jackson» encontrarás un ejemplo de este tipo de problema con la biblioteca Jackson.

Para aprender aún más, consulta nuestro blog Serialización y deserialización en Java: explicación de la vulnerabilidad de deserialización de Java.

8. Usa algoritmos sólidos de cifrado y hash.

Si necesitas almacenar datos confidenciales en tu sistema, debes asegurarte de contar con un cifrado adecuado. Primero, debes decidir qué tipo de cifrado necesitas; por ejemplo, simétrico o asimétrico. También debes elegir el nivel de seguridad que necesitas. Un cifrado más sólido requiere más tiempo y consume más CPU. Lo más importante es que no necesitas implementar los algoritmos de cifrado por tu cuenta. El cifrado es complejo, y una biblioteca confiable se encarga de resolverlo.

Por ejemplo, si queremos cifrar datos de tarjetas de crédito, probablemente necesitemos un algoritmo simétrico, porque tenemos que poder recuperar el número original. Supongamos que usamos el Estándar de Cifrado Avanzado (AES), que actualmente es el algoritmo de cifrado simétrico estándar para las organizaciones federales de EE. UU. Para cifrar y descifrar, no hace falta profundizar en la criptografía de bajo nivel de Java. Te recomendamos usar una biblioteca que se encargue del trabajo pesado. Por ejemplo, Google Tink.

<dependency>
   <groupId>com.google.crypto.tink</groupId>
   <artifactId>tink</artifactId>
   <version>1.3.0-rc1</version>
</dependency>

A continuación, encontrarás un breve ejemplo de cómo usar el cifrado autenticado con datos asociados (AEAD) con AES. Esto nos permite cifrar texto sin formato y proporcionar datos asociados que deben autenticarse, pero no cifrarse.

private void run() throws GeneralSecurityException {
   AeadConfig.register();
   KeysetHandle keysetHandle = KeysetHandle.generateNew(AeadKeyTemplates.AES256_GCM);

   String plaintext = "I want to break free!";
   String aad = "Queen";

   Aead aead = keysetHandle.getPrimitive(Aead.class);
   byte[] ciphertext = aead.encrypt(plaintext.getBytes(), aad.getBytes());
   String encr = Base64.getEncoder().encodeToString(ciphertext);
   System.out.println(encr);

   byte[] decrypted = aead.decrypt(Base64.getDecoder().decode(encr), aad.getBytes());
   String decr = new String(decrypted);
   System.out.println(decr);
}

Para las contraseñas, es más seguro usar un algoritmo sólido de hash criptográfico, ya que no necesitamos recuperar las contraseñas originales, sino solo comparar los hashes. Según la Hoja de referencia sobre contraseñas de OWASP, los mejores algoritmos de hash para contraseñas actualmente son Argon2 y BCrypt. En sistemas heredados, Scrypt puede usarse hasta cierto punto. Los tres son hashes criptográficos (funciones unidireccionales) y algoritmos difíciles de calcular que consumen mucho tiempo. Eso es precisamente lo que necesitas, porque así los ataques de fuerza bruta tardan muchísimo.

Spring Security ofrece una excelente compatibilidad con una amplia variedad de algoritmos. Prueba los codificadores Argon2PasswordEncoder y BCryptPasswordEncoder que ofrece Spring Security 5.0 para aplicar hash a las contraseñas.

Un algoritmo de cifrado que hoy es sólido podría ser débil dentro de un año. Por eso, debes revisar el cifrado con regularidad para asegurarte de usar el algoritmo adecuado para cada tarea. Usa bibliotecas de seguridad verificadas para estas tareas y mantenlas actualizadas.

9. Habilita el Administrador de seguridad de Java

De forma predeterminada, un proceso de Java no tiene restricciones. Puede acceder a todo tipo de recursos, como el sistema de archivos, la red, procesos externos y más. Sin embargo, existe un mecanismo que controla todos estos permisos: el Administrador de seguridad de Java. De forma predeterminada, el Administrador de seguridad de Java no está activo y la JVM tiene poder ilimitado sobre la máquina. Aunque probablemente no queramos que la JVM acceda a ciertas partes del sistema, sí puede hacerlo. Y, lo que es más importante, Java proporciona API capaces de hacer cosas peligrosas e inesperadas.

Creo que la API más aterradora es Attach API. Con esta API, puedes conectarte a otras JVM en ejecución y controlarlas. Por ejemplo, es bastante fácil cambiar el bytecode de una JVM en ejecución si tienes acceso a la máquina. Esta publicación del blog de Nicolas Frankel muestra un ejemplo de cómo hacerlo.

Activar el Administrador de seguridad de Java es fácil. Al iniciar Java con un parámetro adicional, activas el administrador de seguridad con la política predeterminada: java -Djava.security.manager.

Sin embargo, es probable que la política predeterminada no se ajuste del todo a las necesidades de tu sistema. Quizá tengas que crear una política personalizada y proporcionársela a la JVM. java -Djava.security.manager -Djava.security.policy==/foo/bar/custom.policy

Observa los dos signos de igual: esto reemplaza la política predeterminada. Si usas un solo signo de igual, la política personalizada se agrega a la política predeterminada.

Para obtener más información sobre los permisos en el JDK y cómo escribir archivos de políticas, consulta la documentación oficial de Java.

Nota:Desde el lanzamiento de Java 17, el administrador de seguridad está marcado como «obsoleto» debido a la implementación de JEP 415. Aun así, sigue funcionando por completo en Java 17. Actualmente, la mayoría de los desarrolladores de Java usan Java 8 o Java 11 en producción. Esto significa que, en la práctica, aunque el administrador de seguridad se eliminará en el futuro, sigue siendo un buen mecanismo que puedes aprovechar.

10. Centraliza el registro y la supervisión

La seguridad no consiste solo en prevenir. También debes poder detectar cuándo algo sale mal para actuar en consecuencia. En realidad, no importa qué biblioteca de registro uses. Lo importante es registrar muchos eventos, porque la falta de registros sigue siendo un gran problema, según OWASP Top 10. En general, se debe registrar todo lo que pueda ser un evento auditable. Las excepciones, los inicios de sesión y los intentos fallidos pueden parecer obvios, pero probablemente quieras registrar todas las solicitudes entrantes, incluido su origen. Así, al menos sabrás qué ocurrió, cuándo y cómo, en caso de que sufran un ataque.

Se recomienda usar un mecanismo para centralizar los registros. Si usas Log4j o logback, conectarlos a una solución centralizada como Elastic Stack es bastante sencillo. Con herramientas como Kibana, puedes acceder a los registros de todos los servidores o sistemas y buscarlos para investigar.

Además de registrar eventos, debes supervisar activamente tus sistemas y almacenar estos valores en una ubicación centralizada y de fácil acceso. Los aumentos repentinos de uso de CPU o una carga enorme desde una sola dirección IP podrían indicar un problema o un ataque. Combina el registro centralizado y la supervisión en vivo con alertas para que recibas una notificación cuando ocurra algo extraño.

Acciones como restablecer la contraseña de administrador, que una IP externa acceda a un servidor interno o que aparezca un parámetro de URL como ‘UNION’ son solo algunos indicadores de que algo no está bien. Si recibes las alertas adecuadas sobre este tipo de problemas y rastreas lo que realmente ocurrió, hay muchas probabilidades de que puedas evitar daños mayores y corregir la filtración a tiempo.

Preguntas frecuentes

¿Qué es la seguridad de Java?

La seguridad de Java se refiere a las medidas que toma un desarrollador de Java para evitar que un usuario malicioso vulnere una aplicación. Al escribir código Java sólido y seguro, el desarrollador evita que se pongan en riesgo la confidencialidad, la integridad y la disponibilidad de la aplicación y de los datos.

¿Java representa un riesgo de seguridad?

Java no tiene por qué representar un riesgo de seguridad. Si actualizas Java correctamente, usas solo los módulos necesarios y te aseguras de que tus aplicaciones se desarrollen con la seguridad como prioridad, puedes minimizar el riesgo. Esta guía te ayuda a evitar vulnerabilidades de seguridad de Java en las aplicaciones que desarrollas.

¿Dónde se encuentra el archivo de seguridad de Java?

El archivo java.security forma parte del entorno de ejecución de Java (JRE) y contiene las propiedades de seguridad predeterminadas. Si usas Java 8 o una versión anterior, puedes encontrarlo en $JAVA_HOME/jre/lib/security. En las versiones más recientes de Java, el archivo se encuentra en $JAVA_HOME/conf/security.

Comienza con Capture the Flag

Aprende a resolver desafíos de Capture the Flag con nuestro taller virtual 101 a pedido.

Leer más

blog feature snyk container custom base recommendations
Blog

Presentamos Evo Agentic AppSec: seguridad al ritmo del desarrollo con IA

El desarrollo impulsado por IA supera el ritmo de los flujos de trabajo de seguridad tradicionales. Agentic AppSec de Snyk usa agentes de seguridad para encontrar, corregir y verificar vulnerabilidades de forma continua, mientras las personas definen el estándar.

blog feature ai violet
Blog

VulnBump: ¿Pueden los agentes de IA corregir dependencias vulnerables sin romper la compilación?

VulnBump prueba agentes de IA en 307 actualizaciones reales de dependencias. Una corrección cuenta solo si elimina la vulnerabilidad y mantiene en verde la compilación y las pruebas.

Blog

Los modelos de frontera encontraron las vulnerabilidades. Solo el atacante encontró las cadenas.

El análisis estático encontró las fallas, pero solo las pruebas de ataque en vivo demostraron cómo podían encadenarse para provocar brechas. Una comparación de Evo COS, Claude Security y Claude Code Security.