WIP: colaboraciones anonimas
This commit is contained in:
parent
e6dce0e055
commit
164cb5dfa2
6 changed files with 309 additions and 1 deletions
45
app/controllers/api/v1/invitades_controller.rb
Normal file
45
app/controllers/api/v1/invitades_controller.rb
Normal file
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class InvitadesController < BaseController
|
||||
# Obtiene una cookie válida por el tiempo especificado por el
|
||||
# sitio.
|
||||
#
|
||||
# Aunque visitemos el sitio varias veces enviando la cookie
|
||||
# anterior, la cookie se renueva.
|
||||
def cookie
|
||||
# XXX: Prestar atención a que estas acciones sean lo más rápidas
|
||||
# y utilicen la menor cantidad posible de recursos, porque son
|
||||
# un vector de DDOS.
|
||||
site, anon = Site.where(name: params[:site_id], colaboracion_anonima: true)
|
||||
.pluck(:name, :colaboracion_anonima)
|
||||
.first
|
||||
|
||||
# La cookie no es accesible a través de JS y todo su contenido
|
||||
# está cifrado para que no lo modifiquen les visitantes
|
||||
#
|
||||
# Enviamos un token de protección CSRF
|
||||
if anon
|
||||
headers['Access-Control-Allow-Credentials'] = true
|
||||
headers['Access-Control-Allow-Origin'] = "https://#{site}"
|
||||
headers['Vary'] = 'Origin'
|
||||
|
||||
expires = 30.minutes
|
||||
cookies.encrypted[site] = {
|
||||
httponly: true,
|
||||
secure: true,
|
||||
expires: expires,
|
||||
same_site: :none,
|
||||
value: {
|
||||
csrf: form_authenticity_token,
|
||||
expires: (Time.now + expires).to_i
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
render html: nil, status: :no_content
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
99
app/controllers/api/v1/posts_controller.rb
Normal file
99
app/controllers/api/v1/posts_controller.rb
Normal file
|
@ -0,0 +1,99 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class PostsController < BaseController
|
||||
# Ver doc/anonymous.md
|
||||
skip_forgery_protection
|
||||
# Protecciones antes de procesar los datos
|
||||
before_action :cookie_is_valid?, unless: :performed?
|
||||
before_action :valid_authenticity_token_in_cookie?, unless: :performed?
|
||||
before_action :site_exists_and_is_anonymous?, unless: :performed?
|
||||
before_action :site_is_origin?, unless: :performed?
|
||||
|
||||
# Crea un artículo solo si el sitio es invitado, pero antes
|
||||
# tenemos que averiguar varias cosas:
|
||||
#
|
||||
# * la cookie sea válida
|
||||
# * el token anti CSRF es válido
|
||||
# * el sitio existe
|
||||
# * el sitio admite invitades
|
||||
# * el origen de la petición no es el sitio
|
||||
#
|
||||
# TODO: Definir cuáles van a ser las respuestas para cada error
|
||||
# o si simplemente vamos a aceptarlas sin dar feedback.
|
||||
def create
|
||||
# No procesar nada más si ya se aplicaron todos los filtros
|
||||
return if performed?
|
||||
|
||||
# Redirigir a la URL de agradecimiento
|
||||
redirect_to params[:redirect_to]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Comprueba que no se haya reutilizado una cookie vencida
|
||||
#
|
||||
# XXX: Si el navegador envió una cookie vencida es porque la está
|
||||
# reutilizando, probablemente de forma maliciosa?
|
||||
def cookie_is_valid?
|
||||
unless cookies.encrypted[site_id] &&
|
||||
cookies.encrypted[site_id]['expires'] > Time.now.to_i
|
||||
render html: nil, status: :no_content
|
||||
end
|
||||
end
|
||||
|
||||
# Queremos comprobar que la cookie corresponda con la sesión. La
|
||||
# cookie puede haber vencido, así que es uno de los chequeos más
|
||||
# simples que hacemos.
|
||||
#
|
||||
# TODO: Pensar una forma de redirigir al origen sin vaciar el
|
||||
# formulario para que le usuarie recargue la cookie.
|
||||
def valid_authenticity_token_in_cookie?
|
||||
unless valid_authenticity_token? session, cookies.encrypted[site_id]
|
||||
render html: nil, status: :no_content
|
||||
end
|
||||
end
|
||||
|
||||
# El sitio existe y soporta colaboracion anónima
|
||||
#
|
||||
# Pedimos el sitio aunque no lo necesitemos para que la consulta
|
||||
# entre en la caché
|
||||
def site_exists_and_is_anonymous?
|
||||
_, anon = site_anon_pair
|
||||
|
||||
render html: nil, status: :no_content
|
||||
end
|
||||
|
||||
# El navegador envía la URL del sitio en el encabezado Origin,
|
||||
# queremos comprobar que los datos son enviados desde ahí.
|
||||
def site_is_origin?
|
||||
site, = site_anon_pair
|
||||
|
||||
unless "https://#{site}" === request.headers['Origin']
|
||||
render html: nil, status: :no_content
|
||||
end
|
||||
end
|
||||
|
||||
# Solo soy un atajo
|
||||
def site_id
|
||||
@site_id ||= params[:site_id]
|
||||
end
|
||||
|
||||
# La consulta más barata que podemos hacer y la reutilizamos para
|
||||
# que esté en la caché
|
||||
def site_anon_pair
|
||||
Site.where(name: site_id, colaboracion_anonima: true)
|
||||
.pluck(:name, :colaboracion_anonima)
|
||||
.first
|
||||
end
|
||||
|
||||
# Instancia el sitio completo
|
||||
#
|
||||
# XXX: Solo usar después de comprobar que el sitio existe!
|
||||
def site
|
||||
@site ||= Site.find_by(name: site_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -34,4 +34,5 @@
|
|||
- next if @post.send(attr).front_matter?
|
||||
|
||||
%section{ id: attr }
|
||||
-# TODO: Esto es un agujero de XSS!!!!
|
||||
= raw @post.send(attr).value
|
||||
|
|
|
@ -21,7 +21,10 @@ Rails.application.routes.draw do
|
|||
namespace :v1 do
|
||||
resources :csp_reports, only: %i[create]
|
||||
get 'sites/allowed', to: 'sites#allowed'
|
||||
resources :sites, only: %i[index]
|
||||
resources :sites, only: %i[index], constraints: { site_id: /[a-z0-9\-\.]+/, id: /[a-z0-9\-\.]+/ } do
|
||||
get 'invitades/cookie', to: 'invitades#cookie'
|
||||
resources :posts, only: %i[create]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
7
db/migrate/20200130193655_add_anonymous_to_site.rb
Normal file
7
db/migrate/20200130193655_add_anonymous_to_site.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddAnonymousToSite < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
add_column :sites, :colaboracion_anonima, :boolean, default: false
|
||||
end
|
||||
end
|
153
doc/anonymous.md
Normal file
153
doc/anonymous.md
Normal file
|
@ -0,0 +1,153 @@
|
|||
# Colaboraciones anónimas
|
||||
|
||||
> Estamos escribiendo hipótesis para aclararnos las ideas.
|
||||
|
||||
|
||||
[Ver discusión](https://0xacab.org/sutty/sutty/issues/75)
|
||||
|
||||
## Configuración
|
||||
|
||||
```yaml
|
||||
# Actual
|
||||
invitades: true
|
||||
|
||||
# Nueva
|
||||
invitades:
|
||||
allowed:
|
||||
- users
|
||||
- guests
|
||||
- anonymous
|
||||
```
|
||||
|
||||
Pero en realidad no queremos instanciar todo el sitio y leer la
|
||||
configuración para poder comprobar esto, así que lo movemos a la base de
|
||||
datos. De todas formas es información que no tiene sentido almacenar en
|
||||
`_config.yml` porque no tiene uso fuera de Sutty.
|
||||
|
||||
|
||||
## Procedimiento
|
||||
|
||||
* Al cargar el formulario, se incorpora una petición a la API de Sutty
|
||||
que devuelve un recurso vacío y una cookie cifrada solo disponible
|
||||
para HTTP (no para JS). Además agrega una cookie de sesión con un
|
||||
token anti CSRF. Les decimos cookie-token y cookie-sesión
|
||||
respectivamente.
|
||||
|
||||
* Al enviar el formulario, la petición se envía con estas cookies. Si
|
||||
los tokens coinciden, el envío se permite. Esto no es una protección
|
||||
CSRF completa, sino una forma de validar que se solicitó una cookie
|
||||
antes.
|
||||
|
||||
* La sesión es válida si el token de sesión y el de la cookie coinciden.
|
||||
|
||||
* La emisión de sesiones + cookies está limitada en el servidor.
|
||||
|
||||
* Al cargar los datos correctamente, respondemos con una redirección
|
||||
a la página de agradecimiento.
|
||||
|
||||
|
||||
### CSRF
|
||||
|
||||
* Las protecciones contra CSRF permitirían al sitio que envía obtener un
|
||||
token de autenticación que se valida contra la cookie de sesión
|
||||
enviada por el servidor al pedir el recurso.
|
||||
|
||||
* El sitio tiene que enviar este token junto con la petición.
|
||||
|
||||
* No tiene mucho sentido usar protección contra CSRF porque ya estamos
|
||||
haciendo peticiones cruzadas. La protección contra CSRF previene
|
||||
acciones que en realidad queremos realizar (!)
|
||||
|
||||
* Trabajar con protección CSRF requiere que el sitio use JS que no
|
||||
estábamos dispuestes a utilizar, porque hay que tomar el token
|
||||
e incorporarlo al formulario en forma de campo oculto. Queremos que
|
||||
les visitantes con JS deshabilitado puedan interactuar con nuestros
|
||||
formularios también!
|
||||
|
||||
* La validación que estamos haciendo entre cookie cifrada y fecha de
|
||||
vencimiento de la cookie es una forma de protección contra CSRF
|
||||
liviana y sin interacción, aprovechando los mecanismos del navegador.
|
||||
|
||||
* Estamos mirando el valor de Origin para prevenir
|
||||
|
||||
### XSS
|
||||
|
||||
* Es importante limpiar todos las entradas de valores, para proteger
|
||||
a les usuaries del sitio de ataques mayores, por ejemplo que se
|
||||
introduzca JS en un artículo que luego se abre desde el panel.
|
||||
|
||||
* Hay que chequear las protecciones CSRF en los formularios internos del
|
||||
panel!
|
||||
|
||||
* Hay que escapar todas las entradas al mostrarlas!
|
||||
|
||||
* Si la redirección se obtiene desde el mismo formulario, estaría
|
||||
abierto a XSS?
|
||||
|
||||
### DOS
|
||||
|
||||
* La idea es permitir el envío de colaboraciones a una tasa normal (1
|
||||
cada X minutos) y dificultar las tasas de envío agresivas (miles por
|
||||
segundo).
|
||||
|
||||
* El DOS no solo implica bajar el servidor, sino también llenar el sitio
|
||||
de artículos basura y dificultar el uso. O sea, el DOS se aplica
|
||||
a les usuaries, que se estresan.
|
||||
|
||||
* Las cookies se pueden reutilizar, siempre y cuando el token sea
|
||||
válido. Si guardáramos otra información como cantidad de
|
||||
utilizaciones de una cookie, tendríamos que guardar el estado en la
|
||||
base de datos y no queremos usar recursos en esto.
|
||||
|
||||
* Los atacantes pueden descartar la cookie (volverla a emitir)
|
||||
o utilizar siempre la misma.
|
||||
|
||||
* Las cookies se emiten con límite, una cada 5 minutos.
|
||||
|
||||
* La cookie tiene que durar lo que se puede tardar en cargar el
|
||||
formulario y completarlo, con un excedente por las dudas. Los
|
||||
formularios tienen que soportar el guardado offline / autocompletado
|
||||
para evitar que les usuaries se frustren. También es posible hacer la
|
||||
solicitud de cookie usando JS inmediatamente antes del envío para
|
||||
reducir la duración de la cookie aun más.
|
||||
|
||||
* Si la validez de la cookie-token es mayor que la tasa de emisión
|
||||
(digamos, dura 30 minutos), un atacante puede enviar todos los
|
||||
artículos que quiera durante ese tiempo (miles), con lo que tendríamos
|
||||
que llevar un estado de las sesiones de todas formas.
|
||||
|
||||
* El envío de información también puede tener tasa de petición. Si
|
||||
aplicamos `rate limit` en nginx a X minutos entre cada una, tiene que
|
||||
haber una diferencia de tiempo entre la emisión de la cookie y el
|
||||
envío de la información. Si la cookie se reintenta usar muchas veces,
|
||||
también aplica la limitación.
|
||||
|
||||
* Si la tasa de envío es cada 5 minutos, un atacante podría enviar 288
|
||||
artículos por día desde una sola IP.
|
||||
|
||||
## Casos de uso
|
||||
|
||||
* Usuarie ingresa al sitio, completa el formulario y lo envía.
|
||||
|
||||
Este es el caso que queremos.
|
||||
|
||||
* Dogpiling: Usuarios maliciosos pero desorganizados ingresan al sitio,
|
||||
completan el formulario muchas veces y lo envían.
|
||||
|
||||
Para este caso estaríamos protegides por el rate limit. No tenemos
|
||||
protección contra la paciencia y perseverancia del odio.
|
||||
|
||||
Quizás les podamos empezar a enviar zip bombs.
|
||||
|
||||
* DOS: Atacante individual o colectivo genera script que envía muchas
|
||||
veces el formulario automáticamente. Puede enviar desde muchas
|
||||
computadoras y es capaz de entender nuestra protecciones para
|
||||
encontrarles puntos débiles.
|
||||
|
||||
Nos protege el rate limit hasta cierto punto.
|
||||
|
||||
* DDOS: Los atacantes aprovechan la capacidad de muchas personas que
|
||||
voluntaria o inconcientemente ingresan a una URL que es capaz de enviar
|
||||
información basura.
|
||||
|
||||
Nos protege el rate limit, CORS y XSS.
|
Loading…
Reference in a new issue