Métodos No Seguros

Fundamentos Security Notes Jan 9, 2026 HTTP
http rest métodos-http modificación-estado crud

Definición

Los métodos no seguros son métodos HTTP que están diseñados para modificar el estado del servidor - crean, actualizan o eliminan recursos. A diferencia de los métodos seguros (GET, HEAD, OPTIONS) que son de solo lectura, los métodos no seguros tienen efectos secundarios que cambian los datos o estado del servidor. Los cuatro métodos no seguros principales son POST, PUT, PATCH y DELETE.

El término “no seguro” no significa peligroso o inseguro - significa que estos métodos no son seguros de reintentar ciegamente sin consecuencias. Si accidentalmente refrescas una página después de enviar un formulario con POST, podrías crear un pedido duplicado o enviar un email duplicado. Por esto los navegadores muestran advertencias “¿Estás seguro de que quieres reenviar este formulario?” para peticiones POST.

Debido a que los métodos no seguros modifican estado, requieren manejo más cuidadoso: autenticación y autorización apropiadas, protección CSRF, claves de idempotencia para reintentos, limitación de tasa para prevenir abuso y logging de auditoría. Mientras PUT y DELETE son idempotentes (seguros de reintentar), POST y PATCH generalmente no lo son, haciendo la lógica de reintento más compleja. Los métodos no seguros son los caballos de batalla de las APIs REST - son cómo realmente HACES cosas en lugar de solo leer información.

Ejemplo

POST - Crear Recurso: Cuando te registras para una cuenta, el navegador envía POST /api/users con tu email y contraseña. Esto crea un nuevo registro de usuario. Enviar el mismo POST nuevamente crea otro usuario duplicado (a menos que la API implemente verificaciones de idempotencia).

PUT - Reemplazar Recurso: Cuando editas tu perfil y guardas, el cliente envía PUT /api/users/123 con todos tus campos de perfil. Esto reemplaza completamente el recurso de usuario. PUT es idempotente - enviarlo 5 veces resulta en los mismos datos de perfil.

PATCH - Actualización Parcial: Para cambiar solo tu dirección de email, el cliente envía PATCH /api/users/123 con {"email": "[email protected]"}. Esto actualiza solo el campo de email. PATCH puede ser idempotente si se diseña cuidadosamente (valores absolutos) pero a menudo no lo es (cambios incrementales).

DELETE - Eliminar Recurso: Cuando eliminas una publicación, el cliente envía DELETE /api/posts/456. Esto elimina la publicación. DELETE es idempotente - eliminar la misma publicación 10 veces resulta en que la publicación esté eliminada (intentos subsecuentes devuelven 404).

Checkout de E-commerce: Cuando haces clic en “Hacer Pedido,” el cliente envía POST /api/orders con ítems del carrito e información de envío. Esto crea un pedido, decrementa inventario y cobra tu tarjeta. Refrescar accidentalmente crearía un pedido duplicado - por eso los sistemas de pago usan claves de idempotencia.

Botón de Me Gusta en Redes Sociales: Hacer clic en “me gusta” en una publicación podría enviar POST /api/posts/789/likes. Esto crea un registro de me gusta. Hacer clic nuevamente podría quitar el me gusta (toggle) o crear un duplicado. Las APIs bien diseñadas hacen esto idempotente: POST crea me gusta si no existe, devuelve me gusta existente si existe.

Carga de Archivo: Subir un archivo envía PUT /api/files/photo.jpg con el contenido del archivo. PUT es idempotente - subir el mismo archivo múltiples veces resulta en que el mismo archivo sea almacenado, sobrescribiendo versiones previas.

Analogía

Escribir en un Cuaderno: Cuando escribes algo en un cuaderno, lo cambias permanentemente. No puedes “deshacer” la escritura leyendo la página. POST, PUT, PATCH y DELETE son como escribir - modifican el estado y tienen efectos duraderos.

Enviar un Formulario: Cuando envías por correo un formulario físico (como una declaración de impuestos), no puedes des-enviarlo abriendo tu buzón y mirando adentro. El envío (POST) es una operación no segura con consecuencias. Leer tu buzón (GET) es seguro.

Hacer una Compra: Comprar algo en una tienda cambia el estado - el dinero sale de tu cuenta, el inventario disminuye, se registra una transacción. Esta es una operación no segura. Navegar productos es seguro, pero “Agregar al Carrito” y “Checkout” son operaciones no seguras.

Editar una Pizarra: Borrar y reescribir una pizarra cambia su estado permanentemente. Esto es como PUT (reemplazar) o PATCH (actualizar parte de ella). Mirar la pizarra (GET) no la cambia.

Eliminar un Archivo: Cuando eliminas un archivo de tu computadora, desaparece (o va a la papelera). Puedes eliminarlo múltiples veces, pero después de la primera vez ya está eliminado - similar a cómo DELETE es idempotente. Leer el archivo (GET) no lo elimina.

Ejemplo de Código

# ===== POST - Crear Recurso (NO Idempotente) =====
# Cada petición crea un nuevo recurso
POST /api/orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer token123

{
  "product_id": 456,
  "quantity": 2,
  "shipping_address": "123 Main St"
}

# Respuesta
HTTP/1.1 201 Created
Location: /api/orders/789
Content-Type: application/json

{
  "id": 789,
  "product_id": 456,
  "quantity": 2,
  "total": 39.98,
  "status": "pending"
}

# Reintentar crea OTRO pedido (id: 790) - NO idempotente
# Solución: Usar cabecera Idempotency-Key

POST /api/orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer token123
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

{
  "product_id": 456,
  "quantity": 2
}

# Reintentar con misma clave devuelve pedido original (id: 789) - AHORA idempotente

# ===== PUT - Reemplazar Recurso (Idempotente) =====
# Reemplaza recurso completo
PUT /api/products/456 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer token123

{
  "name": "Widget Actualizado",
  "price": 12.99,
  "stock": 150,
  "description": "Nueva descripción"
}

# Respuesta
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 456,
  "name": "Widget Actualizado",
  "price": 12.99,
  "stock": 150,
  "description": "Nueva descripción"
}

# Reintentar con mismos datos - recurso termina en mismo estado (idempotente)

# ===== PATCH - Actualización Parcial (Usualmente NO Idempotente) =====
# Actualiza solo campos especificados
PATCH /api/users/123 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer token123

{
  "email": "[email protected]"
}

# Respuesta
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 123,
  "name": "John Doe",
  "email": "[email protected]"
}

# Idempotente si se establecen valores absolutos
# NO idempotente si se usan operaciones como incremento:
PATCH /api/products/456 HTTP/1.1
Content-Type: application/json

{
  "stock": { "$inc": -1 }  # Decrementar en 1
}

# Reintentar decrementa nuevamente - ¡NO idempotente!

# ===== DELETE - Eliminar Recurso (Idempotente) =====
# Primera petición
DELETE /api/posts/789 HTTP/1.1
Host: api.example.com
Authorization: Bearer token123

# Respuesta
HTTP/1.1 204 No Content

# Reintentar - recurso ya eliminado
DELETE /api/posts/789 HTTP/1.1
Host: api.example.com
Authorization: Bearer token123

# Respuesta (puede ser 404 o 204, ambos aceptables)
HTTP/1.1 404 Not Found

# El resultado es el mismo: el recurso no existe (idempotente)
// Ejemplos en JavaScript de métodos no seguros

// ===== POST - Crear Recurso =====
async function createOrder(productId, quantity) {
  const response = await fetch('/api/orders', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`
    },
    body: JSON.stringify({
      product_id: productId,
      quantity: quantity
    })
  });

  if (response.ok) {
    const order = await response.json();
    console.log('Pedido creado:', order.id);
    return order;
  }
  throw new Error('Falló la creación del pedido');
}

// Llamar dos veces crea DOS pedidos (no seguro de reintentar)
await createOrder(456, 2); // Crea pedido #789
await createOrder(456, 2); // Crea pedido #790 (¡duplicado!)

// POST con Clave de Idempotencia
async function createOrderSafely(productId, quantity, idempotencyKey) {
  const response = await fetch('/api/orders', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`,
      'Idempotency-Key': idempotencyKey
    },
    body: JSON.stringify({
      product_id: productId,
      quantity: quantity
    })
  });

  return response.json();
}

const key = crypto.randomUUID();
await createOrderSafely(456, 2, key); // Crea pedido #789
await createOrderSafely(456, 2, key); // Devuelve pedido #789 (el mismo)

// ===== PUT - Reemplazar Recurso =====
async function updateProduct(id, productData) {
  const response = await fetch(`/api/products/${id}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`
    },
    body: JSON.stringify(productData)
  });

  return response.json();
}

// PUT es idempotente - seguro de reintentar
const product = {
  name: 'Widget Actualizado',
  price: 12.99,
  stock: 150
};

await updateProduct(456, product); // Producto actualizado
await updateProduct(456, product); // Mismo resultado (idempotente)

// ===== PATCH - Actualización Parcial =====
async function updateUserEmail(userId, newEmail) {
  const response = await fetch(`/api/users/${userId}`, {
    method: 'PATCH',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`
    },
    body: JSON.stringify({
      email: newEmail
    })
  });

  return response.json();
}

// PATCH idempotente (valor absoluto)
await updateUserEmail(123, '[email protected]'); // Email cambiado
await updateUserEmail(123, '[email protected]'); // Email sigue siendo '[email protected]' (idempotente)

// PATCH no idempotente (operación de incremento)
async function incrementViewCount(postId) {
  const response = await fetch(`/api/posts/${postId}`, {
    method: 'PATCH',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`
    },
    body: JSON.stringify({
      views: { $inc: 1 }
    })
  });

  return response.json();
}

await incrementViewCount(789); // Vistas: 101
await incrementViewCount(789); // Vistas: 102 (¡NO idempotente!)

// ===== DELETE - Eliminar Recurso =====
async function deletePost(postId) {
  const response = await fetch(`/api/posts/${postId}`, {
    method: 'DELETE',
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });

  if (response.status === 204 || response.status === 404) {
    console.log('Publicación eliminada o ya eliminada');
    return true;
  }
  throw new Error('Falló la eliminación de la publicación');
}

// DELETE es idempotente - seguro de reintentar
await deletePost(789); // 204 No Content (eliminado)
await deletePost(789); // 404 Not Found (ya eliminado)
// El resultado es el mismo: la publicación no existe

// ===== Manejo de Errores y Lógica de Reintento =====
async function safeRetry(operation, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await operation();
    } catch (error) {
      if (i === maxRetries - 1) throw error;

      // Backoff exponencial
      const delay = Math.pow(2, i) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

// Seguro de reintentar operaciones idempotentes
await safeRetry(() => updateProduct(456, product)); // PUT - seguro
await safeRetry(() => deletePost(789));             // DELETE - seguro

// NO seguro de reintentar operaciones no idempotentes sin clave de idempotencia
// await safeRetry(() => createOrder(456, 2)); // ¡NO HAGAS ESTO - crea duplicados!

// Seguro con clave de idempotencia
const idempKey = crypto.randomUUID();
await safeRetry(() => createOrderSafely(456, 2, idempKey)); // Seguro ahora

Diagrama

graph TB
    subgraph "Métodos No Seguros (Modifican Estado)"
        POST[POST
Crear recurso] PUT[PUT
Reemplazar recurso] PATCH[PATCH
Actualización parcial] DELETE[DELETE
Eliminar recurso] end subgraph "Idempotencia" I1[POST: NO idempotente
a menos que use claves de idempotencia] I2[PUT: Idempotente
Seguro de reintentar] I3[PATCH: Usualmente NO
idempotente si es incremental] I4[DELETE: Idempotente
Seguro de reintentar] end POST --> I1 PUT --> I2 PATCH --> I3 DELETE --> I4 subgraph "Preocupaciones de Seguridad" S1[Autenticación Requerida] S2[Verificaciones de Autorización] S3[Protección CSRF] S4[Limitación de Tasa] S5[Logging de Auditoría] S6[Validación de Entrada] end POST -.-> S1 POST -.-> S2 POST -.-> S3 POST -.-> S4 POST -.-> S5 POST -.-> S6 PUT -.-> S1 PUT -.-> S2 PUT -.-> S3 PUT -.-> S6 PATCH -.-> S1 PATCH -.-> S2 PATCH -.-> S3 PATCH -.-> S6 DELETE -.-> S1 DELETE -.-> S2 DELETE -.-> S3 DELETE -.-> S4 DELETE -.-> S5 subgraph "Casos de Uso" U1[POST: Registrarse, Crear pedido, Subir archivo] U2[PUT: Actualizar perfil, Reemplazar documento] U3[PATCH: Cambiar email, Actualizar estado] U4[DELETE: Eliminar cuenta, Eliminar publicación] end style POST fill:#FFB6C1 style PUT fill:#FFD700 style PATCH fill:#FFA500 style DELETE fill:#FF6347

Notas de Seguridad

SECURITY NOTES

CRÍTICO - …

Configuración y Validación:

  • Todos los métodos no seguros DEBEN implementar autenticación y autorización apropiadas.
  • Nunca confiar en entrada del cliente - validar y sanitizar todos los datos antes de procesar.
  • Implementar protección CSRF usando tokens o cookies SameSite para operaciones que cambien estado.
  • Usar limitación de tasa para prevenir abuso - limitar peticiones POST, PUT, PATCH, DELETE por usuario por ventana de tiempo.
  • Implementar claves de idempotencia para POST para prevenir creación de recursos duplicados desde reintentos o ataques de replay maliciosos.
  • Registrar todas las operaciones que cambien estado con ID de usuario, timestamp y detalles para rastros de auditoría.
  • Validar propiedad de recursos antes de permitir modificaciones - asegurar que los usuarios solo puedan modificar sus propios recursos.
  • Usar HTTPS exclusivamente para proteger credenciales y datos sensibles en cuerpos de petición.

Monitoreo y Protección:

  • Implementar manejo de errores apropiado sin exponer detalles internos o stack traces.
  • Para operaciones DELETE implementar eliminaciones suaves o pasos de confirmación para datos críticos.
  • Usar verificaciones de autorización fuertes - no solo autenticación.
  • Un usuario autenticado no debería poder eliminar recursos de otro usuario.
  • Implementar rollback de transacciones para operaciones complejas que involucren múltiples recursos.
  • Monitorear patrones sospechosos - creación rápida, eliminación masiva o patrones de actualización inusuales.
  • Usar validación de Content-Type para prevenir ataques de confusión de tipo de contenido.
  • Implementar límites de tamaño en cuerpos de petición para prevenir DoS vía payloads grandes.

Mejores Prácticas

  1. Siempre requerir autenticación: Los métodos no seguros nunca deberían ser accesibles sin credenciales válidas
  2. Implementar autorización apropiada: Verificar que el usuario tenga permiso para realizar la acción
  3. Usar claves de idempotencia para POST: Prevenir creación de recursos duplicados desde reintentos
  4. Devolver códigos de estado apropiados: 201 para POST crear, 200/204 para PUT/PATCH/DELETE
  5. Incluir cabecera Location para POST: Apuntar al recurso recién creado
  6. Soportar actualizaciones parciales con PATCH: No requerir que los clientes envíen el recurso completo
  7. Hacer DELETE idempotente: Devolver 204 o 404 en reintento, no error
  8. Validar entrada exhaustivamente: Verificar tipos de datos, formatos, rangos, campos requeridos
  9. Implementar limitación de tasa: Prevenir abuso de operaciones que cambien estado
  10. Registrar rastros de auditoría: Registrar quién hizo qué y cuándo para responsabilidad

Errores Comunes

Sin protección CSRF: Operaciones que cambien estado vulnerables a ataques de falsificación de petición entre sitios.

Falta de autenticación: Permitir peticiones POST, PUT, PATCH, DELETE sin autenticar.

Autorización débil: Verificar si el usuario está autenticado pero no si posee el recurso que está modificando.

Sin idempotencia para POST: Crear recursos duplicados cuando los clientes reintentan peticiones fallidas.

Usar POST para todo: No usar PUT para actualizaciones o DELETE para eliminación - viola principios REST.

No devolver 201 para POST: Devolver 200 en lugar de 201 Created con cabecera Location.

Tratar 404 en DELETE como error: DELETE es idempotente - 404 en reintento es aceptable, no un fallo.

Sin limitación de tasa: Permitir peticiones POST ilimitadas habilita spam, abuso y ataques DoS.

Exponer errores internos: Devolver errores de base de datos o stack traces en respuestas 500.

Permitir PATCH sin validación: Aceptar parches JSON arbitrarios sin verificar campos permitidos.

Sin logging de auditoría: No registrar quién creó, actualizó o eliminó recursos.

Ignorar límites de tamaño de datos: Aceptar tamaño de cuerpo de petición ilimitado para operaciones POST/PUT.

Estándares y RFCs

Términos Relacionados