¿Cómo podemos escribir código Java seguro? ¿No te piratean?
En este artículo, analizamos técnicas para lidiar con 13 exposiciones estáticas diferentes. Para cada exposición, explicamos el impacto de no abordar estos problemas de seguridad. También recomendamos algunas pautas que debe seguir para desarrollar aplicaciones Java sólidas y seguras que no se vean amenazadas por estas exposiciones de seguridad estáticas. Siempre que surge la oportunidad, proporcionamos ejemplos de código (tanto código expuesto como no expuesto).
Consejos para lidiar con exposiciones de alta gravedad
Siga las siguientes recomendaciones para evitar exposiciones de seguridad estáticas de alta gravedad:
Restringir el acceso a las variables
Haga que todas las clases y métodos sean finales a menos que haya una buena razón para no hacerlo.
No confíe en el alcance del paquete
Haga que las clases no se puedan clonar
Hacer que las clases no sean serializables
Hacer que las clases sean serializables de manera no reversible
Evitar la codificación de datos confidenciales
Buscar código malicioso
Limitar el acceso a variables
Si una variable se declara como pública, el código externo puede operar sobre la variable. Esto puede provocar una exposición a la seguridad.
Impacto
Si la variable de instancia es pública, entonces se puede acceder a la variable de instancia y manipularla directamente en la instancia de clase. Declarar una variable de instancia como protegida no necesariamente resuelve este problema: aunque no es posible acceder a dicha variable directamente en una instancia de clase, aún se puede acceder a ella desde una clase derivada.
El Listado 1 muestra código con una variable pública, la cual está expuesta porque es pública.
Listado 1. Código con variables públicas
class Test {
public int id;
protected String name;
p>
Prueba(){
id = 1;
nombre = "hola mundo"
}
/ /code
}
clase pública MyClass extiende Test{
método público voidIllegalSet(String name){
this. nombre = nombre; // esto no debería permitirse
}
public static void main(String[] args){
Test obj = new Test( );
obj.id = 123; // esto no debería estar permitido
MyClass mc = new MyClass();
mc.methodIllegalSet("Ilegal Establecer valor" );
}
}
Recomendaciones
En términos generales, se deben utilizar métodos de valor en lugar de variables públicas. Caso por caso, al determinar qué variables son particularmente importantes y deben declararse privadas, compare la facilidad y el costo de la codificación con sus necesidades de seguridad.
El Listado 2 demuestra código que se puede hacer seguro de las siguientes maneras:
Listado 2. Código sin variables públicas
class Test {
private int id <; /p>
nombre de cadena privada;
Prueba(){
id = 1;
nombre = "hola mundo";
}
public void setId(int id){
this.id = id
}
public void setName (Nombre de cadena){
this.name = nombre;
}
public int getId(){
return id;
}
public String getName(){
devolver nombre;
}
}
Hacer que cada clase y método sea final
Las clases y métodos que no permiten extensión deben declararse finales. Hacerlo evita que el código externo al sistema extienda la clase y modifique su comportamiento.
Impacto
El simple hecho de declarar una clase como no pública no impide que un atacante extienda la clase, ya que aún se puede acceder a la clase desde su propio paquete.
Recomendación
Hacer que cada clase y método sea final a menos que haya una buena razón para no hacerlo. Con esta sugerencia te pedimos que abandones la extensibilidad, aunque es una de las principales ventajas de utilizar un lenguaje orientado a objetos como es el lenguaje Java. Cuando se intenta brindar seguridad, la escalabilidad se convierte en su enemigo; la escalabilidad solo brinda a los atacantes más formas de causarle problemas.
No confíe en el alcance del paquete
Las clases, métodos y variables que no están marcados explícitamente como públicos, privados o protegidos son accesibles dentro de sus propios paquetes.
Impacto
Si un paquete Java no está cerrado, un atacante puede introducir una nueva clase en el paquete y usar la nueva clase para acceder al contenido que desea proteger. Algunos paquetes como java.lang están cerrados de forma predeterminada y algunas JVM también le permiten cerrar sus propios paquetes. Sin embargo, será mejor que asumas que el paquete no está cerrado.
Recomendación
El alcance del paquete es importante desde la perspectiva de la ingeniería de software porque evita el acceso accidental e involuntario al contenido que desea ocultar. Pero no confíe en ello por seguridad. Las clases, métodos y variables deben marcarse explícitamente como públicas, privadas o protegidas, lo que se adapte a sus necesidades específicas.
Hacer que una clase no sea clonable
La clonación permite copiar fácilmente instancias de clases, sin pasar por el constructor.
Impacto
Incluso si no haces que una clase sea clonable intencionalmente, una fuente externa aún puede definir una subclase de tu clase y hacer que esa subclase implemente java.lang.Cloneable. Esto permite al atacante crear nuevas instancias de su clase. Copiar la imagen de memoria de un objeto existente crea una nueva instancia; aunque a veces esta es una forma aceptable de crear nuevos objetos, la mayoría de las veces no lo es.
El Listado 3 ilustra el código expuesto debido a la clonación:
Listado 3. Código clonable
class MyClass{
private int id;
nombre de cadena privada
public MyClass(){
id=1
nombre="HaryPorter"
}; p>
public MyClass(int id, nombre de cadena){
this.id=id
this.name=nombre;
}
visualización de vacío público(){
System.out.println("Id =" id "
" "Name=" nombre);
}
}
// código de hackers para clonar la clase de usuario
clase pública Hacker extiende MyClass implementa Cloneable {
public static void main(String[] args){
Hacker hack=new Hacker();
prueba{
MyClass o=(MyClass )hack. clone();
o.display();
}
catch(CloneNotSupportedException e){
printStackTrace() ;
}
}
}
Recomendación
Para evitar que las clases sean clonadas, puedes Agregar el método que se muestra en el Listado 4 para su clase:
Listado 4. Hacer que su código no sea clonable
public final Object clone()
lanza java.lang.CloneNotSupportedException {
throw new java.lang.CloneNotSupportedException();
}
Si desea que su clase sea clonable y haya considerado las consecuencias de esta elección , aún puedes proteger tu clase.
Para hacer esto, defina un método de clonación final en su clase y haga que dependa de un método de clonación no final en una de sus superclases, como se muestra en el Listado 5:
Listado 5. Haga que su código sea clonable en de forma segura
public final Object clone()
throws java.lang.CloneNotSupportedException {
super .clone();
}
La presencia del método clone() en una clase evita que los atacantes redefinan su método de clonación.
Hacer que una clase no sea serializable
La serialización permite guardar datos de una instancia de clase en un archivo externo. Romper el código puede clonar o copiar la instancia y luego serializarla.
Impacto
La serialización es preocupante porque permite que fuentes externas obtengan control del estado interno de su objeto. Esta fuente externa puede serializar uno de sus objetos en una matriz de bytes que luego el atacante puede leer. Esto le permite inspeccionar completamente el estado interno de su objeto, incluidos los campos que haya marcado como privados. También permite que un atacante acceda al estado interno de cualquier objeto al que haga referencia.
Recomendación
Para evitar que los objetos de una clase se serialicen, defina el método writeObject() en el Listado 6 de la clase:
Listado 6. Prevenir objeto serialización
private final void writeObject(ObjectOutputStream out)
throws java.io.NotSerializableException {
throw new java.io.NotSerializableException("Este objeto no puede
ser serializado");
}
Al declarar el método writeObject() como final, se evita que los atacantes anulen el método.
Hacer que una clase sea irreversiblemente serializable
Al utilizar la deserialización, un atacante puede crear una instancia de una clase con datos externos o un flujo de bytes.
Impacto
Ya sea que una clase sea serializable o no, se puede deserializar. Las fuentes externas pueden crear secuencias de bytes que se deserializan en instancias de clase. Esta posibilidad crea un gran riesgo para usted, ya que no tiene control sobre el estado del objeto deserializado. Piense en la deserialización como otro constructor público para su objeto, uno sobre el que no tiene control.
Recomendación
Para evitar la deserialización de objetos, debe definir el método readObject() en el Listado 7 de su clase:
Listado 7. Prevenir la deserialización de objetos
readObject final vacío privado (ObjectInputStream in)
lanza java.io.NotSerializableException {
lanza nueva java.io.NotSerializableException(" Este objeto no puede
be deserialized");
}
Al declarar el método como final, se evita que los atacantes anulen el método.
Evite codificar datos confidenciales
Es posible que tenga la tentación de almacenar secretos, como claves de cifrado, en el código de su aplicación o biblioteca. Para los desarrolladores, esto suele facilitar las cosas.
Impacto
Cualquier persona que ejecute su código tiene acceso completo a los secretos almacenados de esta manera. No hay nada que impida que programadores malintencionados o máquinas virtuales husmeen en su código y descubran sus secretos.
Recomendación
Es posible almacenar secretos en su código de una manera que solo usted pueda descifrarlos. En este caso, el secreto reside únicamente en el algoritmo que utiliza su código. No hay mucho daño al hacer esto, pero no se confíe y piense que proporciona una protección sólida. Puede enmascarar su código fuente o código de bytes, es decir, cifrarlo de tal manera que se deba conocer el formato de cifrado para poder descifrarlo, pero existe una alta probabilidad de que un atacante pueda deducir el formato de cifrado, lo que hará que oscurecimiento Realizar ingeniería inversa en el código para revelar sus secretos.
Una posible solución a este problema es guardar datos confidenciales en un archivo de propiedades que se pueda leer desde ese archivo siempre que se necesiten los datos. Si los datos son extremadamente confidenciales, su aplicación debe utilizar algunas técnicas de cifrado/descifrado al acceder a archivos de propiedades.
Buscar código malicioso
Un desarrollador malicioso que trabaja en un proyecto puede introducir intencionadamente código vulnerable con la intención de explotarlo más adelante. Dicho código puede iniciar un proceso en segundo plano cuando se inicializa, lo que puede abrir una puerta trasera para un intruso. También puede cambiar algunos datos confidenciales.
Existen tres tipos de código malicioso:
El método principal de la clase
Métodos que han sido definidos y no utilizados
En comentarios Dead Code
Impacto
Los programas de punto de entrada pueden ser peligrosos y maliciosos. Generalmente, los desarrolladores de Java tienden a escribir el método main() en sus clases, lo que ayuda a probar la funcionalidad de una sola clase. Cuando las clases pasan de prueba a producción, las clases con métodos main() se convierten en una amenaza potencial para la aplicación porque los intrusos las utilizan como puntos de entrada.
Compruebe si hay métodos no utilizados en el código. Estos métodos pasarán todos los controles de seguridad durante las pruebas porque no se llaman desde el código, pero pueden contener datos confidenciales codificados en su interior (aunque sean datos de prueba). Un atacante que introduzca un pequeño fragmento de código podría llamar a dicho método.
Evitar código muerto (código dentro de comentarios) en la aplicación final. Si un intruso elimina comentarios de dicho código, el código puede afectar la funcionalidad del sistema.
Se pueden ver ejemplos de los tres tipos de código malicioso en el Listado 8:
Listado 8. Código Java potencialmente malicioso
public void usedMethod( ){
// código escrito para dañar el sistema
}
public void usedMethod(){
//unusedMethod() ; código en el comentario puesto con malas intenciones,
//podría afectar el sistema si no se comenta
// int x = 100;
// x =x 10 ; //Código en el comentario, podría afectar la
//funcionalidad del sistema si no se comenta
}
Sugerencia
La principal () (que no sea el método main() que inicia la aplicación), los métodos no utilizados y el código inactivo deben eliminarse del código de la aplicación. Los desarrolladores principales deben realizar una revisión exhaustiva del código de las aplicaciones confidenciales antes de lanzar el software para su uso. Se debe utilizar una clase "Stub" o "ficticia" en lugar del método main() para probar la funcionalidad de la aplicación.
Consejos para lidiar con exposiciones de gravedad media
Siga las siguientes recomendaciones para evitar exposiciones de seguridad estáticas de gravedad media:
No confíe en la inicialización
No comparar clases por nombre
No usar clases internas
No confiar en la inicialización
Puedes asignar objetos sin ejecutar el constructor. Estos objetos no son seguros de usar porque no se inicializan mediante un constructor.
Impacto
La validación de objetos durante la inicialización garantiza la integridad de los datos.
Por ejemplo, imagine un objeto Cuenta que crea una nueva cuenta para un cliente. Solo se puede abrir una nueva cuenta si el saldo inicial de la cuenta es mayor que 0. Puede realizar dicha verificación en el constructor. Alguien que crea un objeto Cuenta sin ejecutar el constructor podría crear una nueva cuenta con algunos valores negativos, lo que haría que el sistema fuera inconsistente y vulnerable a futuras intervenciones.
Recomendación
Verificar el proceso de inicialización del objeto antes de usarlo. Para hacer esto, cada clase debe tener un indicador booleano privado configurado en el constructor, como se muestra en la clase del Listado 9. En cada método no estático, el código debe verificar el valor de este indicador antes de cualquier ejecución adicional. Si el valor de este indicador es verdadero, entonces el control debería continuar; de lo contrario, el control debería generar una excepción y detener la ejecución; Los métodos llamados desde el constructor no verificarán las variables inicializadas porque el indicador no está configurado cuando se llama al método. Debido a que estos métodos no verifican las banderas, deben declararse privados para evitar que los usuarios accedan a ellos directamente.
Listado 9. Uso de indicadores booleanos para verificar el proceso de inicialización
clase pública MiClase{
privado booleano inicializado = false;
/ / Otras variables
public MyClass (){
//inicialización de variable
método1();
inicializado = verdadero; p >
}
private void método1(){ //no es necesario verificar la variable de inicialización
//código
}
public void método2(){
try{
if(initialized==true){
//continuar con la lógica empresarial
}
else{
throw new Exception("Estado ilegal del objeto");
}
} captura (Excepción e){
e.printStackTrace();
}
}
}
Si el objeto se inicializa mediante deserialización, el mecanismo de validación discutido anteriormente no funcionará porque no se llama al constructor durante este proceso. En este caso, la clase debe implementar la interfaz ObjectInputValidation:
Listado 10. Implementando ObjectInputValidation
interfaz java.io.ObjectInputValidation {
public void validarObject() lanza InvalidObjectException;
}
Toda validación debe realizarse en el método validarObject(). El objeto también debe llamar al método ObjectInputStream.RegisterValidation() para registrarse para la validación después de deserializar el objeto. El primer parámetro de RegisterValidation() es un objeto que implementa validarObject(), normalmente una referencia al propio objeto. Nota: Cualquier objeto que implemente validarObject() puede actuar como un validador de objetos, pero los objetos generalmente validan sus propias referencias a otros objetos. El segundo parámetro de RegisterValidation() es una prioridad entera que determina el orden de las devoluciones de llamada. El que tiene un número de prioridad mayor es llamado primero que el que tiene un número de prioridad menor. El orden de las devoluciones de llamada dentro del mismo nivel de prioridad no está definido.
Cuando el objeto ha sido deserializado, ObjectInputStream llama a validarObject() en cada objeto registrado en orden de mayor a menor prioridad.
No compares clases por nombre
A veces es posible que necesites comparar las clases de dos objetos para determinar si son iguales o quizás quieras ver si es un objeto; Es una instancia de una clase específica. Debido a que la JVM puede incluir varias clases con el mismo nombre (clases con el mismo nombre pero en paquetes diferentes), no debe comparar clases según sus nombres.
Impacto
Si compara clases según sus nombres, puede otorgar sin darse cuenta derechos a la clase de un intruso que no pretende otorgar a otros, porque el intruso puede definir la clase con el mismo nombre.
Por ejemplo, supongamos que desea determinar si un objeto es una instancia de la clase com.bar.Foo. El Listado 11 demuestra la forma incorrecta de realizar esta tarea:
Listado 11. La forma incorrecta de comparar clases
if(obj.getClass().getName().equals("Foo ")) // ¡Incorrecto!
// la clase del objeto se llama Foo
}else{
// la clase del objeto tiene otro nombre
}
Recomendación
En aquellos casos en los que las clases deben compararse por nombre, debe tener mucho cuidado y asegurarse de que se utilice el espacio de nombres actual del ClassLoader de la clase actual. , como se muestra en el Listado 12:
Listado 12. Una mejor manera de comparar clases
if(obj.getClass() == this.getClassLoader().loadClass("com .bar .Foo")){
// la clase del objeto es igual a
//la clase que esta clase llama "com.bar.Foo"
} else{
// la clase del objeto no es igual a la clase que
// esta clase llama "com.bar.Foo"
}
Sin embargo, una mejor manera de comparar clases es comparar directamente los objetos de clase para ver si son iguales. Por ejemplo, si desea determinar si dos objetos a y b son de la misma clase, usaría el código del Listado 13:
Listado 13. Comparación directa de objetos para ver si son iguales p >
if(a.getClass() == b.getClass()){
// los objetos tienen la misma clase
}else{
// los objetos tienen diferentes clases
}
Utilice comparaciones directas de nombres lo menos posible.
No utilice clases internas
El código de bytes de Java no tiene el concepto de clases internas, porque el compilador convierte las clases internas en clases ordinarias y si la clase interna no se declara como privada Entonces, cualquier código en el mismo paquete puede acceder a esta clase ordinaria.
Impacto
Debido a esta característica, el código malicioso en el paquete puede acceder a estas clases internas. Las cosas empeoran si la clase interna tiene acceso a los campos de la clase externa circundante. Es posible que haya declarado estos campos como privados, de modo que la clase interna se convierta en una clase separada, pero cuando la clase interna accede a los campos de la clase externa, el compilador cambia estos campos de privados a válidos dentro del alcance. Ya es bastante malo que las clases internas queden expuestas, pero lo peor es que el compilador hace que sus esfuerzos por hacer que ciertos campos sean privados sean inútiles.
Recomendación: Si puede evitar el uso de clases internas, no utilice clases internas.
Consejos para lidiar con exposiciones de baja gravedad
Siga las siguientes recomendaciones para evitar exposiciones de seguridad estáticas de baja gravedad:
Evite devolver objetos mutables
Comprobar métodos nativos
Evitar devolver objetos mutables
Los métodos Java devuelven una copia de una referencia de objeto. Si el objeto real es mutable, llamar al programa con dicha referencia puede cambiar su contenido, lo que normalmente no es deseable.
Impacto
Considere este ejemplo: un método devuelve una referencia a una matriz interna de objetos sensibles, asumiendo que la persona que llama al método no muta estos objetos. Aunque el objeto de matriz en sí es inmutable, el contenido de la matriz se puede manipular fuera del objeto de matriz, y dichas manipulaciones se reflejarán en el objeto que devuelve la matriz. Las cosas empeoran aún más si el método devuelve un objeto mutable; las entidades externas pueden cambiar las variables públicas declaradas en esa clase, y dichos cambios se reflejarán en el objeto real.
El Listado 14 demuestra la vulnerabilidad.
El método getExposedObj() devuelve una copia de referencia del objeto expuesto, que es mutable:
Listado 14. Devuelve una copia de referencia del objeto mutable
clase Expuesta{
id int privado;
nombre de cadena privada;
público expuesto(){
}
público expuesto (id int , Nombre de cadena){
this.id = id
this.name = nombre
}
public int getId( ){
ID de retorno;
}
Cadena pública getName(){
Nombre de retorno;
}
public void setId(int id){
this.id=id;
}
public void setName(nombre de cadena) ){
this.name = nombre;
}
display vacío público(){
System.out.println(" Id = " id " Nombre = " nombre);
}
}
clase pública Exp12{
privado Expuestoobj expuesto = nuevo Exposed(1, "Harry Porter");
public Exposed getExposedObj(){
return expuestoObj; //devuelve una referencia al objeto.
}
public static void main(String[] args){
Exp12 exp12 = new Exp12()
exp12.getExposedObj().display();
Expuesto expuesto = exp12.getExposedObj();
expuesto.setId(10);
expuesto.setName("Hacker");
exp12.getExposedObj().display();
}
}
Recomendación
Si el método devuelve un valor mutable objeto, pero no desea que el programa que realiza la llamada cambie el objeto, modifique el método para que no devuelva el objeto real sino una copia o un clon del mismo.
Para corregir el código en el Listado 14, haga que devuelva una copia del objeto expuesto, como se muestra en el Listado 15:
Listado 15. Devolver una copia del objeto mutable
público expuesto getExposedObj (){
return new Exposed(exposedObj.getId(), expuestoObj.getName());
}
Alternativamente, su código puede devolver A clon del objeto expuesto.
Buscar métodos nativos
Un método nativo es un método Java cuya implementación está escrita en otro lenguaje de programación, como C o C. Algunos desarrolladores implementan métodos nativos porque el lenguaje Java es más lento que muchos lenguajes compilados, incluso con un compilador justo a tiempo. Otros necesitan usar código nativo para implementar funciones específicas de la plataforma fuera de la JVM.
Impacto
Tenga cuidado al utilizar código nativo, ya que la verificación de este código no es posible y el código nativo podría permitir que un subprograma eluda el control de acceso al dispositivo habitual del Administrador de seguridad y de Java. .
Recomendación
Si debe utilizar métodos nativos, examínelos para determinar:
Qué devuelven
Qué obtienen como parámetros
Si pasan por alto los controles de seguridad
Si son públicos, privados, etc.
Si contienen contenido que pasa por alto los límites de los paquetes y, por lo tanto, pasa por alto la protección de paquetes Llamadas a métodos
Conclusión
Escribir código Java seguro es difícil, pero este artículo describe algunas prácticas posibles para ayudarle a escribir código Java seguro. Estas recomendaciones no resolverán todos sus problemas de seguridad, pero reducirán la cantidad de exposiciones. Las mejores prácticas de seguridad de software pueden ayudar a garantizar que su software funcione correctamente. Los diseñadores de sistemas críticos para la seguridad y de alta confiabilidad siempre dedican mucho esfuerzo a analizar y rastrear el comportamiento del software. Sólo tratando la seguridad como una característica crítica del sistema (e incorporándola a las aplicaciones desde el principio) podremos evitar un enfoque fragmentado de la seguridad.
Referencias
Únase al foro para este artículo haciendo clic en la discusión en la parte superior o inferior del artículo.
Más información sobre la API de seguridad de Java.
El hilo de seguridad de DeveloperWorks a menudo contiene excelentes recursos sobre seguridad informática.
"La evolución de la seguridad de Java" (developerWorks, 1998), de Larry Koved, Anthony J. Nadalin, Don Neal y Tim Lawson, proporciona una mirada en profundidad al desarrollo inicial del lenguaje Java. modelo de seguridad.
En su serie de dos partes sobre seguridad de Java (developerWorks, febrero de 2001), Sing Li muestra a los desarrolladores: Aunque la comunidad puede tener que repensar la seguridad en Java 2, todavía hay un progreso constante en el diseño sexual que es sólo es útil para los desarrolladores y puede satisfacer sus necesidades:
Parte 1
Parte 2
John "Escaneo estático del código Java en busca de vulnerabilidades de seguridad" (IEEE Software, septiembre 2000) de Viega, Tom Mutdosch, Gary McGraw y Ed Felten describe una herramienta Java que puede utilizar para inspeccionar su código Java en busca de vulnerabilidades de seguridad.
Securing Java: Getting Down to Business with Mobile Code (John Wiley and Sons, 1998), de G. McGraw y E. Felten, cubre la seguridad de Java en profundidad. (El documento está en formato PDF.)
Consulte periódicamente la página de IBM Research Java Security para que IBM pueda realizar un seguimiento de importantes innovaciones en seguridad a medida que se desarrollan.
Si su código Java se ejecuta en un sistema S/390, querrá consultar la página de seguridad de S/390 Java para obtener información adicional.
Acerca del autor
Bijaya Nanda Sahu es una ingeniera de software que trabaja en IBM Global Services en India. Ha trabajado en diversas tecnologías y marcos de Internet (J2EE, WSBCC, JADE), tecnologías relacionadas con WebSphere, UML y OOAD. Actualmente trabaja en temas de seguridad de banca por Internet, enfocándose en WebSphere Application Server y Portal Server. Se le puede contactar en bijaya.sahu@in.ibm.com