Lo que he visto desde mi experiencia es que cuando programamos el aspecto de seguridad se descuida bastante. Tal vez no sea adrede sino por circunstancias que se nos escapan como falta de conocimiento, falta de revisión de código o apuros del día a día.
Justo por eso es ventajoso usar frameworks en lugar de hacer mezclas de distintas librerías. Idealmente, un framework de desarrollo brinda características de diferente tipo para gestionar diferentes partes de una aplicación web. El aspecto de seguridad hace parte de esas características. Y por eso es bueno usar Ruby on Rails.
En el apartado de seguridad de las guías de Rails tenemos una completa lista y descripción de las cosas que tenemos a nuestro favor para hacer una web segura: gestión de sesiones, protección contra CSRF, resguardos contra XSS o ataques de inyección, e incluso un lenguaje específico de dominio (DSL) para CSP (Content Security Policy).
A continuación, trataré de explicar algunos aspectos de esta oferta.
Cross-Site Request Forgery o Falsificación de Petición
Este tipo de ataque parte de la confianza que tienen los sitios web en sus usuarios, o más bien, las cookies de los usuarios.
Un ataque tipo CSRF es exitoso cuando el navegador web de una víctima, envía comandos no intencionados a un sitio web vulnerable. Esto se logra mediante el robo de las cookies de sesión. Mediante estas, el sitio expuesto creerá que es una petición regular porque proviene de un usuario fiable y autenticado.
Para protegernos de esto, primero que nada hay que usar las peticiones HTTP como corresponde:
- POST, PUT, DELETE para modificar datos
- GET a modo solo lectura
Con esto en orden, ya podemos usar de manera tranquila la macro protect_from_forgery
en ApplicationController
. Así se usa normalmente y por defecto:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
Esta macro funciona de dos formas.
En formularios generados con helpers de Rails, se agrega un campo oculto en cada formulario que tiene un token para idenfiticar que la petición proviene de un usuario fiable:
<input name="authenticity_token" type="hidden" value="xxxxxxxxxxx" />
En peticiones tipo Ajax generadas por Rails agrega la cabecera X-CSRF-TOKEN
.
Cuando se hacen peticiones Ajax que no son generadas por Rails, hay que configurar la petición para enviar dicho token en la cabecera. Esto se logra leyendo el token desde la etiqueta meta que se imprime por el helper <%= csrf_meta_tags %>
que debe estar en application.html.erb
.
<meta name='csrf-token' content='THE-TOKEN'>
Por ejemplo, así se obtiene el token:
const token = document.getElementsByName('csrf-token')[0].content
Y así se enviaría en una petición Ajax usando fetch nativo:
fetch('/api/v1/posts', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token
},
credentials: 'same-origin'
})
El token de autenticación es un token de sincronización (generado criptograficamente en el servidor) el cual está atado a cada usuario y se envía en cada petición POST en formularios. Si este es diferente al que el servidor espera, la petición se considera fallida.
En la hoja de trampa (o chuleta) de OWASP nos dicen esto sobre los token CSRF:
- Deben ser únicos por sesión,
- secretos,
- impredecibles (valor grande generado aleatoriamente),
- y no deben ser transmitidos por cookies
Todo esto para impedir que un atacante abuse de la confianza y la exposición de los usuarios a sitios (vulnerables o no).
Ataques de Inyección
Este es uno de los ataques que más suele mencionarse en tutoriales o cuando se ejemplifica como usar algunas características de Rails.
Un ataque de inyección es aquel intenta insertar código o parámetros maliciosos en una aplicación web para que se ejecute dentro de un contexto de seguridad.
Un contexto de seguridad viene siendo diferentes partes donde se puede ejecutar o enviar/recibir parámetros en una web. Ejemplos de contexto pueden ser un script, una consulta a la base de datos, el mismo lenguaje de programación, la línea de comandos, una función de Ruby o de Rails.
Los ataques de inyección más comunes son los XSS (cross-site scripting) y las inyecciones de SQL (SQL Injection) pero también hay inyecciones por CSS, por cabeceras de petición HTTP, por línea de comando e incluso HTML.
Las guías de Rails hacen mucho énfasis en que una forma de protegerse de esto es manteniendo siempre listas de cosas permitidas en lugar de listas de restricción.
Cuando se limpie, verifique o proteja algo, prefiere listas permitidas que listas restringidas.
Guías de Rails
Pensar en listas de elementos permitidos es más sencillo porque las listas de restricciones tienden a ser olvidadas cuando haya que agregar algo nuevo. En cambio, se puede pensar con más amplitud en las cosas a permitir y obviar todo lo que se deba restringir.
Muy similar a cómo en Rails 4 se introdujo Strong Parameters: indica la lista de atributos que un formulario puede recibir. Todo lo que no se incluya se descarta.
Inyecciones SQL
Al usar Rails, tenemos una ventaja con Active Record ya que implementa filtros y ajustes por defecto para prevenir este tipo de ataques. Para casos puntuales, las guías tienen recomendaciones de cómo lidiarlos.
Limpiar parámetros en métodos que reciban un string SQL:
Project.where("name = '#{params[:name]}'")
connection.execute()
Model.find_by_sql()
En esos casos hay que haber limpiado la cadena de la consulta para detectar posibles intentos de ataque.
Para casos de consultas WHERE, Rails nos insta a usar mejor parámetros posicionales con y sin nombre:
Model.where("zip_code = ? AND quantity >= ?", entered_zip_code, entered_quantity).first
En todo caso, se pueden limpiar cadenas de consulta con el método sanitize_sql
el cual si revisamos veremos que va haciendo limpieza mediante listas permitidas.
Cross-Site Scripting o Secuencia de Comandos
Este es el ataque más común y también más peligroso ya que su intención es inyectar código ejecutable de lado del cliente con la finalidad de saltar controles de seguridad y hacerse pasar por el usuario vulnerado.
En MDN, se menciona que este tipo de ataques son exitosos si el sitio web no aplica los suficientes controles de validación o codificación de caracteres. En esos casos, el navegador del usuario no puede detectar que el script malicioso no es fiable y por ende le permite acceso a cookies, sesiones o cualquier tipo de información sensible, incluso, permitir reescribir el HTML.
Buena cosa es que Rails provee de diferentes formas de protegernos contra esto. Antes de mirar dichas formas tenemos que conocer el término «Entry Point».
Un «Entry Point» es una URL vulnerable y sus parámetros con los cuales un atacante empieza su ataque. Ejemplos de Entry Points son:
- Cualquier formulario o parte donde ingrese datos
- Tableros de mensajes
- Comentarios de usuarios en publicaciones
- Títulos de proyectos/publicaciones, nombres de documentos e incluso resultados de búsquedas
El más común de los ataques de tipo Cross-Site es el de inyección de JavaScript dada la popularidad de este lenguaje en navegadores web. Algunos de sus usos son robo de cookies y desfiguración.
Robo de Cookies lo logran al explotar la función document.cookie
. Se soluciona usando la bandera httpOnly
en las cookies que configuremos.
Desfiguración (defacement) es un ataque en el cual se presenta información falsa para atraer el usuario y robarle cookies, sesiones, credenciales u otra información delicada. La forma más popular de hacer este ataque es mediante iframes.
¿Cómo Prevenir Cross-Site Scripting?
Muy importante filtrar y limpiar datos de ingreso que podrían prestarse para fines maliciosos pero también es importante escapar y limpiar los datos de salida.
Recordemos lo importante de trabajar con listas permitidas en lugar de listas restringidas. Las listas permitidas indican explícitamente todo lo que puede ingresar en captura de datos. Es más fácil mirarlo de esa forma que pensar en aquello que no se permite.
Un ejemplo de esto es si se usa el helper strip_tags()
. Si nos centramos en restringir, en el siguiente ejemplo permitiremos un ataque:
strip_tags("some<<b>script>alert('hello')<</b>/script>")
# retornará:
# "some<script>alert('hello')</script>"
lo cual no tiene ninguna finalidad porque un atacante podría llegar a lograr su cometido.
En cambio, si usamos listas permitidas:
tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p)
s = sanitize(user_input, tags: tags, attributes: %w(href title))
Ya cambia la película porque solo vamos a permitir aquello en la lista de permitidos. No hay que validar más nada (excepción de casos particulares pero con esto se tiene un buen punto de partida).
Hay otros tipos de ataque de inyección como lo son CSS, Textije, Ajax, Command Line y Header injection los cuales se dan en otros contextos de seguridad, tal vez un poco menos comunes (igual vulnerables si no se protegen) y que requieren más o menos las mismas protecciones: limpiar datos de ingreso, usar listas permitidas, limpiar datos de salida.
Content Security Policy (CSP)
Esto no es un tipo de ataque sino una cabecera que envían los servidores para controlar el acceso a recursos que una página puede cargar y de esta forma ayuda a prevenir ataques de los tipos mencionados anteriormente.
CSP se diseñó para ser retrocompatible. Navegadores que no lo soporten podrían trabajar con servidores que lo implementan y vice versa: ante la ausencia de una política de contenido, se usaría la política de mismo origen (CORS).
Ruby on Rails provee un lenguaje específico de dominio para configurar estas políticas. Normalmente, se puede configurar en config/initializers/content_security_policy.rb
.
Este es un ejemplo en una app Rails donde la política no está configurada.
# Be sure to restart your server when you modify this file.
# Define an application-wide content security policy
# For further information see the following documentation
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
# Rails.application.config.content_security_policy do |policy|
# policy.default_src :self, :https
# policy.font_src :self, :https, :data
# policy.img_src :self, :https, :data
# policy.object_src :none
# policy.script_src :self, :https
# policy.style_src :self, :https
# # If you are using webpack-dev-server then specify webpack-dev-server host
# policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development?
# # Specify URI for violation reports
# # policy.report_uri "/csp-violation-report-endpoint"
# end
# If you are using UJS then enable automatic nonce generation
# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
# Set the nonce only to specific directives
# Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
# Report CSP violations to a specified URI
# For further information see the following documentation:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
# Rails.application.config.content_security_policy_report_only = true
Como muchas cosas en Rails, lo que se configure en ese archivo podrá ser reescrito a nivel de controlador.
class PostsController < ApplicationController
content_security_policy do |policy|
policy.upgrade_insecure_requests true
policy.base_uri "https://www.example.com"
end
end
¿Cómo nos ayuda CSP?
Lo hace de dos formas claras.
Mitigar XSS: la meta principal de CSP es mitigar y reportar este tipo de ataques. CSP hace posible que los administradores reduzcan los vectores de ataque al especificar los dominios que el navegador debe considerar como fuentes fiables.
Mitigrar sniffing de paquetes: se puede especificar que protocolos son permitidos. Idealmente, solo se debería permitir HTTPS, cookies con el atributo secure y haciendo redirecciones automáticas de HTTP a HTTPS.
Así luce una cookie configurada con el atributo secure:
Set-Cookie: id=a3fWa; Expires=Thu, 21 Oct 2021 07:28:00 GMT; Secure; HttpOnly
Conclusión y Fuentes
Hay mucha tela por cortar y muchas cosas por enterarnos en cuanto de seguridad se trata pero no se puede tragar todo en un solo bocado. La verdad es que mientras escribía esto aprendí de seguridad en la web y me doy cuenta del poco de cosas que hago a un lado cuando escribo código en Ruby on Rails.
¿Qué puedo hacer para mejorar? Lo primero fue darme cuenta que no tenía en cuenta la guía de seguridad ni las recomendaciones de OWASP. Lo que sigue es tratar de ser consciente de todo esto en futuras ocasiones y pensar en el aspecto de la seguridad cuando esté programando.
Los atacantes están rastreando y monitoreando los sitios que menos uno espera en busca de una oportunidad. Claramente, entre más valor halla en el sitio más lo harán pero no hay que descuidarse por eso.
Mientras tanto dejo fuentes y enlaces para consulta a futuro.
Enlaces: