Español | English
rss facebook linkedin Twitter

Seguridad en Ruby on Rails (I)

Ruby on Rails (RoR o simplemente Rails) es un marco de desarrollo para aplicaciones Web de código abierto que está gozando de una gran popularidad en los últimos años. Rails sigue el paradigma de la arquitectura Modelo Vista Controlador (MVC) y se caracteriza por un elevado nivel de productividad en comparación a su curva de aprendizaje. Se basa en dos principios, "No te repitas", es decir, las definiciones deberían hacerse una sola vez y "Convención sobre configuración" , que se refiere al hecho de que el programador sólo necesita definir aquella configuración que no es convencional.

Desde su concepción a finales del año 2005, Rails ha ido evolucionando apoyado por un creciente número de desarrolladores hasta convertirse en un framework de programación Web sólido, escalable, bien documentado, preparado para el entorno empresarial y con una concienciación sobre la seguridad que ya apuntaba maneras desde sus orígenes.

Actualmente, aunque la última versión disponible de Rails es la 2.3.5 , el salto a la nueva generación (versión 3) tras la fusión con Merb -- otro framework de desarrollo Web sobre Ruby -- parece inminente, y dado el grado de avance del proyecto, está previsto que se produzca a lo largo de este año 2010.

Desde el punto de vista de la seguridad, la nueva generación de Rails ofrece interesantes posibilidades, como el uso de middleware a medida utilizando rack (http://guides.rails.info/rails_on_rack.html), además de incorporar de forma oficial soluciones que en las versiones anteriores de rails estaban implementadas en plugins.

Tanto si estamos desarrollando con Rails en la rama 2.3.x como si tenemos en mente acometer la migración o el desarrollo de una nueva aplicación empresarial basada en la versión 3 de Rails, existen una serie de consideraciones que deberemos tener en cuenta si queremos implementar un adecuado nivel de seguridad en nuestra aplicación Web.

Primeramente, veremos algunos de los errores típicos que se cometen durante el desarrollo y que tendrán implicaciones en mayor o menor medida en la seguridad de nuestra aplicación. Obvia decir que los ejemplos utilizados están muy simplificados para facilitar su comprensión.

En la segunda parte de este documento se ofrecen recomendaciones y consideraciones a tener en cuenta durante el desarrollo. Además se comentan algunas soluciones disponibles en forma de plugins que contribuirán a mejorar la seguridad de las aplicaciones desarrolladas con Rails.



ALGUNOS ERRORES TÍPICOS

Exposición de métodos en los controladores:

Rails asume todos los métodos públicos declarados en un controlador como acciones. Es importante no olvidarse de utilizar los atributos private y protected con el fin de proteger aquellos métodos que por algún extraño motivo estén en los controladores y cuyo uso no queramos permitir a los usuarios de la aplicación.

Asignación masiva en Active Record:

Siguiendo la arquitectura MVC, Rails hace uso del concepto "modelo" que es gestionado por una capa de mapeo objeto-relacional (ORM) conocida como Active Record, de manera que se crea una base de datos orientada a objetos virtual, sobre la base de datos relacional. Aunque existen otras alternativas, Active Record es el ORM estándar de Rails para efectuar conexiones con bases de datos, mapear tablas y manipular datos.

En Rails, es posible realizar una asignación masiva de los parámetros recibidos de un formulario. La asignación masiva evita tener que establecer cada valor de los atributos de un modelo de forma individual. Además, desde la versión 2.3 de Rails, es posible extender la asignación masiva a las asociaciones de los modelos (has_many, has_one, has_and_belongs_to_many) utilizando la declaración accepts_nested_attributes_for.

Pensemos en un ejemplo muy simplificado con un modelo usuario.

En el controlador tenemos definido un método "registro" encargado de realizar el alta de un nuevo usuario de nuestra aplicación Web. En este método registro se llama al método Create del modelo usuario, recuperando directamente todos los parámetros del formulario.

La estructura de la tabla asociada a este modelo podría ser similar a:


create_table :usuarios do |t| (
t.string :nombre
t.string :password
t.string :rol, :default => "user"
t.integer :aprobado, :default => 0
end


Lo interesante de esta tabla es la columna aprobado, en ella se almacena el valor 1 cuando el administrador acepta el alta de un nuevo usuario.

El formulario de registro correspondiente bien podría ser algo así:


<form method="post" action="http://dominio/usuario/registro">
<input type="text" name="usuario[nombre]" />
<input type="text" name="usuario[password]" />
</form>


Si hemos implementado una asignación masiva de los parámetros, en el controlador tendremos:


def registro
Usuario.create(params[:usuario])
end



En este caso, "params[:usuario]", contiene un hash con el nombre y la contraseña del usuario.



params[:usuario] #=> {:nombre => "s21sec", :password => "s21sec-password"}



Un usuario podría descargar una copia del formulario y manipularlo offline de forma que se envíen otros parámetros utilizando el método POST. ¿Qué sucedería?


<form method="post" action="http://dominio/usuario/registro">
<input type="text" name="usuario[nombre]" />
<input type="text" name="usuario[password]" />
<input type="text" name="usuario[rol]" value="admin" />
<input type="text" name="usuario[aprobado]" value="1" />
</form>


La acción "registro" de nuestro controlador estaría creando un nuevo usuario administrador, utilizando todos los parámetros que recibe del formulario. Además su registro figuraría como aprobado.

Para evitar esto, Active Record permite marcar los atributos como protegidos, de forma que su valor no pueda ser modificado mediante una asignación masiva.
Para ello, en el modelo Usuario se utiliza el método attr_protected:


class Usuario < ActiveRecord::Base
attr_protected :aprobado, :rol
# ... ...
end


De este modo, si es preciso modificar los parámetros aprobado y rol, se deberá hacer de forma explícita en el controlador:


usuario = Usuario.new(params[:usuario])
usuario.aprobado = params[:usuario][:aprobado]
usuario.rol = params[:usuario][:rol]


La otra opción es utilizar el método attr_accessible, que permite especificar los atributos que podrán ser modificados, quedando el resto protegidos de forma automática, evitando así dejar de proteger algún parámetro por descuido. Incluso es posible forzar el uso de listas blancas de atributos accesibles en los modelos inicializando ActiveRecord::Base.send(:attr_accessible, nil).

Almacenamiento de datos sensibles en los archivos de log:

Por defecto, Rails guarda en un archivo log todos los registros de las peticiones que recibe la aplicación Web. Si el servidor sufre algún tipo de ataque y es comprometido sería posible extraer de este archivo de registros, información sensible sin cifrar.

Para evitar esto, se puede hacer uso del método filter_parameter_logging en cualquier controlador, de modo que los parámetros aparecerán en el log marcados como filtrados. A modo de ejemplo, si se quiere evitar el registro del parámetro "password" para todas las acciones de todos los controladores de la aplicación, bastará con modificar el controlador ApplicationController.


ejemplo/app/controllers/application.rb

class ApplicationController < ActionController::Base
filter_parameter_logging :password
end




Filtros que utilizan expresiones regulares incorrectas:

Una de las características de las "aplicaciones Web 2.0" es la tendencia a la adopción de interfaces más interactivos y a menudo la personalización del entorno del usuario una vez se autentica en la aplicación. Tal es el caso de las hojas de estilo facilitadas por los usuarios (archivos .css), las cuales pueden utilizarse para inyectar código javascript que será interpretado por determinados navegadores. La implementación de estas funcionalidades debe realizarse prestando especial atención a los riesgos de seguridad que suponen. Será preciso filtrar la información que proporcionan los usuarios aplicando un enfoque de lista blanca e impidiendo que puedan manipular a su antojo la lógica de la aplicación.

Para ello en algún momento necesitaremos hacer uso de expresiones regulares.

Pensemos en un caso sencillo, la aplicación permite subir un archivo al servidor, siendo preciso validar el nombre del archivo que proporciona el usuario.

Supongamos que la aplicación tiene un modelo llamado "File" en el cual pretendemos validar el formato del atributo nombre con una expresión regular para que el nombre contenga sólo caracteres alfanuméricos, puntos, + y -. La expresión regular es la siguiente: /^[\w\.\-\+]+$/

El modelo "File" podría ser algo así:


class File < ActiveRecord::Base
validates_format_of :nombre, :with => /^[\w\.\-\ ] $/
end


En nuestro ejemplo, pretendíamos que la expresión regular coincidiese con el nombre del archivo conteniendo ciertos caracteres DESDE el principio HASTA el final de la cadena. Sin embargo, Ruby interpreta las expresiones regulares de forma diferente a otros lenguajes, ^ y $ se utilizan para el principio y el final de una cadena.

Una forma de evitar este filtro es utilizar un carácter de salto de línea en el nombre, codificado en URL encoding: %0A.

Supongamos que el nombre del archivo que ha subido el usuario es visualizado por el administrador del sitio Web, por ejemplo en un backend para validar una fotografía del perfil del usuario. Si el nombre del archivo subido es algo como "foto.jpg%0A<script>alert('hola')</script>" Rails lo convertirá a "foto.jpg\n<script>alert('hola')</script>". La expresión regular que implementa la validación coincide hasta el final de línea, el resto no importa.

La expresión correcta debería ser:

/\A[\w\.\-\+]+\z/


Al igual que en este ejemplo ficticio se provoca la ejecución de código javascript en el navegador del administrador para mostrar un simple mensaje, se podría efectuar otro tipo de acción explotando el XSS, como obtener las cookies o por ejemplo forzar al administrador a realizar acciones en la aplicación Web pasando inadvertidas.

Esta vulnerabilidad se conoce como Cross Site Request Forgery (CSRF), según la funcionalidad de la aplicación algunas acciones podrían ser: borrar otros usuarios, validar una compra como pagada, aumentar el "crédito" del usuario ...etc.

En general, a la hora de implementar un filtrado de caracteres en datos procedentes de fuentes externas, es más efectivo utilizar un enfoque de lista blanca, permitiendo sólo los caracteres válidos y no tratando de filtrar aquellos caracteres no permitidos, ya que en ocasiones este listado es desconocido y puede ser ampliamente superior. Tal es el caso de filtros anti XSS que no tienen en cuenta la posibilidad de codificar los caracteres en otros formatos.


Precauciones al implementar la funcionalidad de subir archivos

A la hora de implementar la funcionalidad de subir archivos al servidor, también hay que tener en cuenta otras consideraciones:

- Si es posible sería recomendable analizar los archivos subidos con un antivirus antes de permitir su acceso.
- Guardar los archivos con un nombre aleatorio y fuera del directorio DocumentRoot.
- Procesar de forma asíncrona la subida de archivos para evitar la realización de posibles ataques de denegación de servicio.
- Filtrar de forma correcta el nombre del archivo.
- Comprobar la presencia de html en los primeros 256 bytes del archivo, para evitar que el internet explorer muestre "inline" el contenido del archivo ya que usa este método cuando desconoce el tipo de archivo.
- Validar el tipo MIME del archivo que proporciona el cliente. Comprobando también el tipo mime del archivo en el lado del servidor. Para ello se puede utilizar la gema shared-mime-info. Esta gema permite averiguar el tipo MIME de un archivo en base a determinadas constantes presentes en los archivos binarios que se utilizan para determinar el formato de un archivo. Si la gema no es capaz de determinar el tipo de archivo porque no aparece en su base de datos (xml), entonces se basa en la extensión del archivo para determinar el tipo MIME, por esto será preciso modificar el método MIME.check(archivo) precisamente para que NO haga esto.


Acceso no autorizado a recursos

Otro error típico de cualquier aplicación Web es una implementación inapropiada de los controles de acceso a los recursos.
En el caso de las aplicaciones desarrolladas con Rails, este concepto se entiende de forma muy sencilla con el siguiente ejemplo básico:

Supongamos una aplicación Rails programada de forma insegura que permite a los usuarios gestionar y participar en diferentes proyectos, a través de un formulario en la url "http://dominio/proyecto/1". El parámetro 1 es el identificador del proyecto.

El código del controlador obtiene la información a mostrar de la siguiente forma:


@proyecto = Proyecto.find(params[:id])


¿Qué ocurre si el usuario cambia el identificador tratando de acceder a otro proyecto al que no debería tener acceso?

Si la aplicación no incorpora las medidas oportunas, probablemente el usuario logrará acceso a cualquier proyecto que solicite. Una forma mejor de implementarlo sería obtener el proyecto en cuestión a través de las relaciones entre las tablas establecidas mediante Active Record. De este modo se comprueba que la búsqueda se realiza sólo entre los proyectos a los que está asignado el usuario autenticado en la aplicación.

@proyecto = @usuario_autenticado.proyectos.find(params[:id])


Otra medida para la implementación correcta de los controles de acceso en una aplicación Rails, es el uso de filtros en los controladores que efectúen comprobaciones antes de la ejecución de determinadas acciones. En este caso puede ser más conveniente utilizar "before_filter :only => acciones" en lugar de usar ":except => acciones", aplicando un filtro de forma explícita sólo para las acciones especificadas.

Lo ideal sería disponer de un plugin específico desde el que centralizar todo el código relativo a la autorización de los usuarios. En función de los requerimientos de seguridad de la aplicación, el desarrollo a medida puede que no sea la mejor opción, ya que no es difícil cometer algún error en su diseño o implementación que permita evadir los controles de acceso. Por ello, lo recomendable es no partir de cero y al menos utilizar como base algún plugin o gema que esté en una fase estable de desarrollo, y sobre todo, conviene escoger el que más se adapte a nuestros requerimientos.

En cuanto a plugins en Rails para implementar procesos de autorización de usuarios, existen múltiples alternativas. Uno de los que están ganando más popularidad es DeclarativeAuthorization (http://github.com/stffn/declarative_authorization).


Ejecutables en la ruta raíz de publicación web:

Si la aplicación Web dispone de una funcionalidad que permita la subida de archivos al servidor, es aconsejable aplicar el conocido principio de "defensa en profundidad", forzando en la aplicación la creación de estos archivos en directorios que estén fuera de la ruta raíz de publicación de contenido.

En el caso del servidor Web Apache, el directorio DocumentRoot apunta al directorio base del sitio Web. Cualquier archivo de script que el servidor sea capaz de interpretar (ej: .asp, .php, .cgi ..etc) si figura dentro del directorio raíz será interpretado. Por este motivo, si el directorio DocumentRoot apunta al directorio "/public" de Rails, cualquier archivo que se le permita subir al usuario se deberá almacenar fuera de esa ruta.


Descarga no autorizada de archivos:

Rails dispone del método send_file() que permite enviar archivos al navegador del usuario para su descarga (por defecto en streaming, leyendo 4096 bytes cada vez). Si se utiliza este método para permitir la descarga de archivos desde la aplicación Web, será necesario asegurarse de que el archivo a descargar está dentro del directorio autorizado.

También es importante utilizar send_file :disposition => 'attachment' para forzar al navegador del usuario a presentar el diálogo de descarga de archivos en lugar de mostrar el archivo directamente. Esto es importante por ejemplo en el caso de que el usuario sea capaz de subir al servidor algún archivo malicioso (ej: pdf con un exploit).
Otra opción es almacenar los nombres de los archivos en la base de datos asociándolos con sus correspondientes ids.

Uso incorrecto de funciones de ejecución de comandos del sistema:

Ruby permite la ejecución de comandos del sistema operativo mediante diferentes métodos: exec(comando), system(comando), syscall(comando), `comando`.

Si la aplicación hace uso de alguno de estos métodos y permite la interacción del usuario, por ejemplo especificando el comando a ejecutar o alguno de sus parámetros, será preciso asegurarse de que el usuario no es capaz de realizar una acción no permitida.

Ejemplo de implementación incorrecta:


ejemplo/app/controllers/inyeccion-comandos_controller.rb
def ejecuta_comando
index
if params[:comando]
system("ls #{params[:comando]} > __tmp.file" )
archivo = File.open("__tmp.file" )
@texto = archivo.read
File.delete("__tmp.file" )
end
end
end


En este ejemplo se puede apreciar como se ejecuta el comando "ls" aceptando los parámetros suministrados directamente por el usuario.
Si además la aplicación no se ha desplegado sin tener en cuenta otras medidas preventivas, como la ejecución de la aplicación bajo una cuenta de usuario sin acceso a una shell de funcionalidad limitada, el problema se acentúa.

Para impedir el abuso de esta funcionalidad, se puede emplear el método "system(comando, parametros)", de manera que los parámetros se pasan de forma segura a la línea de comandos. De esta forma se invalida el uso de funcionalidades propias de la mayoría de las shells, como el uso de (|) ó (;) para concatenar comandos.

A modo de ejemplo:


system("/bin/echo","Cata-Crack; rm *.log")
# Muestra "Cata-Crack; rm *" no borra los archivos.


Modificación de cabeceras HTTP:

Si la aplicación está haciendo uso de versiones de Rails anteriores a la 2.1.2, otro problema derivado de una validación incorrecta de los datos proporcionados por los usuarios puede ser la posibilidad de alterar las cabeceras HTTP.
Estas versiones de Rails contenían un bug que afectaba al método redirect_to, permitiendo realizar una inyección en las cabeceras HTTP.
El uso de este método es muy habitual en Rails, y se utiliza para redirigir al usuario a una página específica enviando un código 302 (redirección) al navegador, colocando en la cabecera Location la url en cuestión.

Supongamos que la aplicación tiene una acción en un controlador implementada para realizar un seguimiento de la navegación del usuario. Este controlador recibe un parámetro referer con la url desde la cual accede el usuario.

http://dominio/controlador/accion?referer=otro_controlador/otra_accion

Contenido de: ejemplo/app/controllers/controlador_controller.rb


...
redirect_to params[:referer]
...


En este caso, insertando un CRLF (carriage-return line-feed), "\r\n", codificado en URL-encoded, "%0d%0a" en las versiones de Rails anteriores a la 2.1.2 era posible inyectar una segunda cabecera Location, modificando la redirección del usuario.

http://dominio/controlador/accion?referer=otro_controlador/otra_accion%0d%0aLocation:+http://www.dominio-fake.com

Esta situación permitiría a un usuario llevar a cabo un ataque de phishing, creando una copia de la aplicación, capturando las credenciales de los demás usuarios y redirigiendolos posteriormente a la aplicación auténtica. Otra posible explotación sería forzar a un usuario ya autenticado en la aplicación a realizar alguna acción en la misma sin su consentimiento. Incluso puede llegar a permitir la instalación de malware en los equipos de los usuarios si estos presentan alguna vulnerabilidad en sus navegadores Web.

Si la aplicación presenta este problema, posiblemente también permita separar la respuesta en dos mensajes HTTP diferentes insertando caracteres CRLF en las cabeceras de respuesta (HTTP Response Splitting). En este caso, las consecuencias podrían ser desde un robo de credenciales insertando javascript en la respuesta, hasta el envenenamiento de la cache Web de algún dispositivo o middleware intermedio (ej: un proxy inverso).

Por eso es importante filtrar cualquier dato proporcionado por los usuarios que se utilice en las cabeceras HTTP. A partir de la versión 2.1.2 de Rails, ya se realiza un filtrado por defecto en la cabecera Location cuando se utiliza el método redirect_to. Cualquier otra cabecera "a medida" que se utilice en la aplicación deberá tener en cuenta estas consideraciones.

Seguridad en Ruby on Rails (II)
-----
Daniel Peláez.
Dept. Auditoría S21sec

2 comentarios:

Anónimo dijo...

Muy buena entrada en el blog.

arnald dijo...

Muy bueno e ilustrativo el articulo.Arnal


(+34 902 222 521)


24 horas / 7 días a la semana



© Copyright S21sec 2013 - Todos los derechos reservados


login