Skip to content
This page has been auto-translated and may contain errors.View in English

Cómo pensar en seguridad

Una funcionalidad puede funcionar perfectamente y aun así no ser segura. Imagina una ruta de pedidos muy simple:

js
app.get('/orders/:id', async (req, res) => {
  const order = await db.orders.findById(req.params.id)
  res.json(order)
})

La ruta lee un id, busca un pedido y lo devuelve. Si la pruebas con tu propio pedido, se ve bien. Ahora cambia el id en la URL. Si el servidor nunca verifica quién es el dueño de ese pedido, un cliente que inició sesión puede leer el recibo de otro cliente.

Esa es la mentalidad de seguridad: no basta con preguntarte si el código funciona. Pregúntate qué podría hacer alguien con él.

JunoCómo pensar en seguridad El código que funciona responde una sola pregunta: ¿esta funcionalidad hace lo que queríamos?

La seguridad plantea una segunda pregunta: ¿qué pasa cuando alguien la usa de una forma que no habíamos previsto?

JunoCómo pensar en seguridad El error en la ruta de pedidos no es un error de JavaScript. El handler se ejecuta, la consulta devuelve un resultado y la respuesta es un JSON válido.

El paso clave al revisar el código es nombrar la decisión en voz alta: este código verifica si un pedido existe, pero no si este usuario tiene permiso para leerlo. Una vez que puedes nombrar la decisión que falta, puedes escribir la prueba que falta.

JunoCómo pensar en seguridad La mayoría de los errores de seguridad en aplicaciones son rutas normales del producto con alguien malintencionado en la sala. La ruta del producto sigue funcionando, por eso la prueba del camino feliz te sonríe mientras el reporte del incidente afila los dientes.

Lee la funcionalidad como afirmaciones bajo presión: la URL afirma un id, la sesión afirma un usuario, el cuerpo afirma una intención. El servidor tiene que decidir cuáles de esas afirmaciones se convierten en hechos.

Detecta el límite

Un límite de confianza es la línea que separa el código que controlas de algo que puede enviarte un valor que tú no elegiste.

El navegador está del otro lado de esa línea. Un usuario puede cambiar la URL, editar un campo oculto de un formulario, repetir un parámetro de consulta, eliminar una cookie, o saltarse tu front end y enviar una solicitud a mano.

Eso hace del navegador el lugar equivocado para tomar decisiones que te importan.

Las validaciones del navegador son útiles, pero no son el control

La validación del lado del cliente ayuda a las personas a llenar formularios. Detecta errores de escritura temprano y da una mejor experiencia.

El servidor igual tiene que verificar el valor de nuevo, porque el servidor es la parte que protege los datos.

Aquí está la misma ruta de pedidos con la decisión faltante hecha explícita:

js
app.get('/orders/:id', async (req, res) => {
  const order = await db.orders.findById(req.params.id)

  if (!order || order.userId !== req.user.id) {
    return res.status(404).json({ error: 'Order not found' })
  }

  res.json(order)
})

La línea importante es la comparación entre order.userId y req.user.id. El servidor está diciendo: este pedido puede existir, pero también tiene que pertenecer a la persona que lo está pidiendo.

Una solicitud se permite cuando el servidor puede comprobar la decisión por sí mismo.

JunoDetecta el límite Trata al navegador como a un visitante en la recepción. Puede pedir cosas, pero no le corresponde a él decidir qué está autorizado a ver.

Coloca las verificaciones importantes en el servidor. Ahí es donde tu app puede comparar la solicitud con los datos en los que ya confía.

JunoDetecta el límite Un límite de confianza es el punto donde un valor pasa de un lugar que no controlas hacia código que va a tomar una decisión.

Un atajo útil para revisar código: enumera cada valor que lee el handler y marca cuáles vinieron de quien hizo la solicitud. Todo lo que haya elegido quien llama necesita una regla del lado del servidor antes de que afecte datos, identidad, permisos o carga de trabajo.

JunoDetecta el límite Los límites van más allá de los bordes de la red. Tareas en segundo plano, herramientas de administración, webhooks, configuración de entorno y servicios internos pueden traer valores desde algún lugar que tu código actual no eligió.

La línea va donde cambia tu disposición a absorber la falla de otro sistema. Una vez que la trazas, le debes al cruce una verificación, una prueba, un camino de error y, a algún pobre ingeniero futuro, una pista de por qué existe.

Nombra lo que podría fallar

Una vez que detectas una decisión riesgosa, ponle nombre a la posible falla:

PreguntaPalabra de seguridadEjemplo
¿Quién puede leer esto?ConfidencialidadUn cliente lee el pedido de otro cliente.
¿Quién puede cambiar esto?IntegridadUn cliente cambia el precio antes de pagar.
¿La gente sigue pudiendo usar esto?DisponibilidadUna sola solicitud hace que la app se ponga lenta para todos.

En conjunto, esto se llama la tríada CIA: confidencialidad, integridad y disponibilidad.

Un recibo filtrado es un problema de confidencialidad. Un campo de precio oculto es un problema de integridad. Una ruta de búsqueda que le permite a una sola solicitud pedir un millón de registros es un problema de disponibilidad.

Algunos errores tocan más de una propiedad a la vez. Si un cliente puede editar la dirección de entrega de otro cliente, puede tanto verla como cambiarla.

JunoNombra lo que podría fallar Usa las tres preguntas antes que los tres nombres. ¿Quién puede leerlo? ¿Quién puede cambiarlo? ¿La gente sigue pudiendo usarlo?

Esas preguntas convierten una preocupación vaga en algo que puedes explicar. Un reporte de error se vuelve más claro cuando puedes nombrar el daño.

JunoNombra lo que podría fallar Confidencialidad, integridad y disponibilidad te ayudan a elegir la solución correcta.

Una fuga de lectura necesita una verificación de acceso. Un cambio hecho por el usuario equivocado necesita autoridad del lado del servidor. Una ruta costosa necesita límites de tamaño, frecuencia o tiempo. Misma ruta, propiedad distinta, reparación distinta.

JunoNombra lo que podría fallar Las tres propiedades ponen en evidencia los compromisos que la gente prefiere no dejar por escrito. Bloquear una cuenta protege la confidencialidad contra adivinar contraseñas, pero le da a los atacantes una forma de bloquear a usuarios reales.

La disponibilidad es la propiedad que los equipos sacrifican a propósito con más frecuencia. Deja ese compromiso explícito en el diseño, o alguien lo va a redescubrir durante una caída del servicio, con café más frío y menos opciones.

Piensa desde el otro lado

La frase "pensar como un atacante" puede sonar dramática. En el desarrollo del día a día, es más tranquila. Elige un dato de entrada y pregúntate:

  • ¿Qué pasa si este valor falta?
  • ¿Qué pasa si es mucho más largo de lo esperado?
  • ¿Qué pasa si es del tipo equivocado?
  • ¿Qué pasa si le pertenece a otro usuario?
  • ¿Qué pasa si la misma solicitud se envía miles de veces?

Toma como ejemplo un formulario de perfil. La página podría mostrar un campo de texto para displayName, un userId oculto y un botón de enviar. El camino previsto es simple: la persona que inició sesión actualiza su propio nombre.

El camino de la seguridad plantea otras preguntas. ¿Por qué está userId en el formulario en primer lugar? ¿Qué pasa si apunta a otra persona? ¿El servidor usa la sesión iniciada, o confía en el campo enviado?

Oculto no significa confiable

Un campo oculto de un formulario está oculto de la página, no de la persona que envía la solicitud.

Si el valor importa, derívalo en el servidor a partir de la sesión o de la base de datos. No le pidas al navegador que te devuelva una decisión que el servidor ya conoce.

El mismo hábito funciona fuera de los formularios. Un archivo subido afirma tener un nombre de archivo y un tipo de contenido. Un token afirma una identidad. Un webhook afirma que ocurrió un evento. Verifica la afirmación en el punto donde tu código está a punto de depender de ella.

JunoPiensa desde el otro lado Empieza con un solo campo. Pregúntate qué pasa si falta, si es muy largo, si tiene la forma equivocada, si le pertenece a otra persona, o si se envía una y otra vez.

Eso es pensar en seguridad, en una forma pequeña y práctica.

JunoPiensa desde el otro lado La versión útil de pensar como atacante es pensar en los datos de entrada. Lee el handler y marca cada afirmación que hace quien llama: identidad, id, rol, precio, nombre de archivo, URL de retorno, tipo de contenido.

Luego pregúntate dónde se verifica cada afirmación. Si una afirmación afecta una decisión antes de que ocurra esa verificación, tienes la forma de un error que vale la pena probar.

JunoPiensa desde el otro lado Ordena las preguntas por qué tan alcanzables son. Un error que un usuario normal con sesión iniciada puede provocar con una sola solicitud merece atención antes que uno que necesita cronometraje preciso, acceso de alguien interno o una bandera de despliegue vieja de 2019.

Eso no vuelve inofensivos los casos extremos. Simplemente mantiene la revisión enfocada en los caminos que los atacantes reales, los usuarios aburridos y los escáneres automatizados van a probar antes de almorzar.

Hacia dónde va esto ahora

Esta mentalidad es la preparación para la siguiente parte del curso.

El modelado de amenazas se pregunta qué podría salir mal antes de que una funcionalidad salga a producción. OWASP te da nombres compartidos para los errores en aplicaciones reales. La clasificación de errores te ayuda a decidir qué arreglar primero.

Mantén el ciclo simple:

  1. Encuentra el límite.
  2. Nombra lo que podría fallar.
  3. Verifica la afirmación en el servidor.