Deserialización no segura

Categoría de OWASP: MASVS-CODE: Calidad de código

Descripción general

Cuando se almacenan o transfieren grandes cantidades de datos de objetos Java, a menudo es más eficiente serializar los datos primero. Luego, la aplicación, la actividad o el proveedor receptor que los controla realizará un proceso de deserialización. En circunstancias normales, los datos se serializan y, luego, se deserializan sin ninguna intervención del usuario. Sin embargo, un actor malicioso puede abusar de la relación de confianza entre el proceso de deserialización y su objeto previsto, por ejemplo, interceptar y alterar objetos serializados. Esto permitiría que la persona o entidad que actúa de mala fe realice ataques como la denegación del servicio (DoS), la elevación de privilegios y la ejecución de código remoto (RCE).

Si bien la clase Serializable es un método común para administrar la serialización, Android tiene su propia clase para controlar la serialización llamada Parcel. Con la clase Parcel, los datos del objeto se pueden serializar en datos de flujo de bytes y empaquetarse en un Parcel con la interfaz Parcelable. Esto permite que el Parcel se transporte o almacene de manera más eficiente.

Sin embargo, se debe tener cuidado cuando se usa la clase Parcel, ya que está diseñada para ser un mecanismo de transporte de IPC de alta eficiencia, pero no debe usarse para almacenar objetos serializados dentro del almacenamiento local persistente, ya que esto podría generar problemas de compatibilidad de datos o pérdida. Cuando se deben leer los datos, se puede usar la interfaz Parcelable para deserializar el Parcel y volver a convertirlo en datos de objetos.

Existen tres vectores principales para aprovechar la desserialización en Android:

  • Aprovecha la suposición incorrecta de un desarrollador de que la desserialización de objetos que provienen de un tipo de clase personalizada es segura. En realidad, cualquier objeto que proporcione cualquier clase se puede reemplazar potencialmente por contenido malicioso que, en el peor de los casos, puede interferir con el cargador de clases de la misma aplicación o de otras. Esta interferencia se produce mediante la inserción de valores peligrosos que, según el propósito de la clase, pueden provocar, por ejemplo, el robo de datos o la apropiación de cuentas.
  • Aprovechar métodos de deserialización que se consideran inseguros por diseño (por ejemplo, CVE-2023-35669, un error de escalamiento de privilegios local que permitía la inyección de código JavaScript arbitraria a través de un vector de deserialización de vínculos directos)
  • Aprovechar fallas en la lógica de la aplicación (por ejemplo, CVE-2023-20963, una falla de escalamiento de privilegios local que permitía que una app descargara y ejecutara código en un entorno con privilegios a través de una falla en la lógica de paquetes de WorkSource de Android)

Impacto

Cualquier aplicación que deserialice datos serializados no confiables o maliciosos podría ser vulnerable a ataques de ejecución remota de código o de denegación del servicio.

Riesgo: Deserialización de entradas no confiables

Un atacante puede aprovechar la falta de verificación de paquetes dentro de la lógica de la aplicación para inyectar objetos arbitrarios que, una vez deserializados, podrían obligar a la aplicación a ejecutar código malicioso que podría provocar la denegación del servicio (DoS), la elevación de privilegios y la ejecución remota de código (RCE).

Estos tipos de ataques pueden ser sutiles. Por ejemplo, una aplicación puede contener un intent que espera solo un parámetro que, después de validarse, se deserializará. Si un atacante envía un segundo parámetro adicional malicioso inesperado junto con el esperado, esto provocará que se deserialicen todos los objetos de datos insertados, ya que el intent trata los elementos adicionales como un Bundle. Un usuario malicioso puede usar este comportamiento para insertar datos de objetos que, una vez serializados, pueden provocar una RCE, la vulneración o pérdida de datos.

Mitigaciones

Como práctica recomendada, supone que todos los datos serializados no son de confianza y pueden ser maliciosos. Para garantizar la integridad de los datos serializados, realiza verificaciones de verificación en los datos para asegurarte de que sean la clase y el formato correctos que espera la aplicación.

Una solución viable podría ser implementar el patrón de búsqueda anticipada para la biblioteca java.io.ObjectInputStream. Si modificas el código responsable de la deserialización, puedes asegurarte de que solo se deserialice un conjunto de clases especificado de forma explícita dentro del intent.

A partir de Android 13 (nivel de API 33), se actualizaron varios métodos dentro de la clase Intent que se consideran alternativas más seguras a los métodos más antiguos y ahora obsoletos para controlar paquetes. Estos nuevos métodos más seguros, como getParcelableExtra(java.lang.String, java.lang.Class) y getParcelableArrayListExtra(java.lang.String, java.lang.Class), realizan verificaciones de tipo de datos para detectar debilidades de discrepancia que podrían hacer que las aplicaciones fallaran y, potencialmente, se aprovecharan para realizar ataques de elevación de privilegios, como CVE-2021-0928.

En el siguiente ejemplo, se muestra cómo se podría implementar una versión segura de la clase Parcel:

Supongamos que la clase UserParcelable implementa Parcelable y crea una instancia de datos del usuario que, luego, se escribe en un Parcel. Luego, se podría usar el siguiente método más seguro de tipo readParcelable para leer el paquete serializado:

Kotlin

val parcel = Parcel.obtain()
val userParcelable = parcel.readParcelable(UserParcelable::class.java.classLoader)

Java

Parcel parcel = Parcel.obtain();
UserParcelable userParcelable = parcel.readParcelable(UserParcelable.class, UserParcelable.CREATOR);

Observa en el ejemplo de Java anterior el uso de UserParcelable.CREATOR dentro del método. Este parámetro obligatorio le indica al método readParcelable qué tipo esperar y tiene un mejor rendimiento que la versión ahora obsoleta del método readParcelable.

Riesgos específicos

En esta sección, se recopilan los riesgos que requieren estrategias de mitigación no estándar o que se mitigaron en cierto nivel de SDK y están aquí para lograr una integridad.

Riesgo: Deserialización de objetos no deseada

Implementar la interfaz Serializable dentro de una clase hará que todos los subtipos de la clase determinada implementen automáticamente la interfaz. En esta situación, algunos objetos pueden heredar la interfaz mencionada anteriormente, lo que significa que los objetos específicos que no están destinados a deserializarse se seguirán procesando. Esto puede aumentar inadvertidamente la superficie de ataque.

Mitigaciones

Si una clase hereda la interfaz Serializable, según la guía de OWASP, se debe implementar el método readObject de la siguiente manera para evitar que se pueda deserializar un conjunto de objetos en la clase:

Kotlin

@Throws(IOException::class)
private final fun readObject(in: ObjectInputStream) {
    throw IOException("Cannot be deserialized")
}

Java

private final void readObject(ObjectInputStream in) throws java.io.IOException {
    throw new java.io.IOException("Cannot be deserialized");
}

Recursos