# ¿Por qué los endpoints GET que modifican datos necesitan aprobación de escritura?

Un endpoint GET que cambia el estado es una escritura con el uniforme equivocado. El peligro no es teórico. Navegadores, rastreadores, clientes API, herramientas de monitorización, servicios de vista previa de enlaces, cachés y agentes hacen solicitudes GET adicionales porque el protocolo les dice que hacerlo es seguro. Si tu endpoint cancela un trabajo, rota un token, envía un mensaje o altera un registro, esas solicitudes adicionales pueden convertirse en acciones de producción.

Los equipos suelen descubrir estas rutas tras un incidente extraño y luego corrigen el único endpoint que causó problemas. Eso es demasiado limitado. Debes encontrar cada llamada con apariencia de lectura que cambie algo, clasificar el efecto según sus consecuencias y situarla tras la misma barrera de autorización y auditoría que una escritura explícita. Cambiar el verbo importa, pero es solo una parte de la reparación.

## Los métodos HTTP seguros describen la semántica solicitada

Los endpoints GET que modifican datos incumplen la promesa del método, incluso cuando sus autores tuvieron un motivo práctico para elegir GET. RFC 9110 define GET como un método seguro y explica que una solicitud segura no pide al servidor que cambie el estado. El RFC permite efectos incidentales, como el registro y la contabilidad, porque el cliente no solicitó esos efectos. No justifica una ruta cuyo trabajo previsto consiste en hacer un cambio.

Esa distinción detecta una excusa habitual: «El servidor debe actualizar last_seen cuando lee el elemento». Si el cliente pidió recuperar un elemento y el servicio actualiza internamente una marca de tiempo de acceso, puede ser incidental. Si el cliente pidió recuperar un elemento y el servicio marca una factura como pagada, crea una exportación, consume un token o hace avanzar un flujo de trabajo, la modificación es la operación solicitada. Llámala escritura.

RFC 9110 también explica por qué importa desde el punto de vista operativo. Los agentes de usuario pueden automatizar métodos seguros. Un navegador puede obtener una página para crear una vista previa. Un rastreador puede seguir un enlace. Una biblioteca cliente puede reintentar después de perder la respuesta. La semántica del protocolo permite que esos actores se comporten así. Tu servidor no puede depender de que cada llamador haya leído su excepción sin documentar.

No confundas seguro con idempotente. Un DELETE puede ser idempotente porque al repetirlo el recurso sigue eliminado, pero sigue siendo inseguro porque la primera llamada cambia el estado. Un GET que incrementa un contador una vez por solicitud podría ser idempotente solo en el sentido limitado de que se detiene tras un umbral, pero sigue siendo inseguro porque su propósito cambia el estado. Estas palabras responden a preguntas distintas:

- Seguro pregunta si quien llama solicitó un cambio de estado.
- Idempotente pregunta si repetir la misma solicitud tiene el mismo efecto previsto.
- Almacenable en caché pregunta si un intermediario puede reutilizar una respuesta.

Cuando un equipo mezcla estos términos, suele aplicar protección contra reintentos y decidir que el problema está resuelto. Evitar duplicados ayuda. No impide que un escáner de enlaces realice la primera acción destructiva.

## Busca consecuencias, no nombres de rutas sospechosos

Encuentras modificaciones ocultas siguiendo lo que provoca una ruta, no confiando en su nombre ni en su verbo. Rutas llamadas `getReport` y `view` pueden encolar trabajo. Rutas llamadas `reset` pueden ser totalmente inofensivas si devuelven un formulario. Crea tu inventario a partir de pruebas de ejecución y rutas de código.

Empieza por cada controlador registrado para GET y HEAD. Para cada uno, sigue las escrituras directas y los traspasos: transacciones de base de datos, publicación en colas, invalidación de caché con significado de negocio, entrega de correo o chat, pagos, cambios de credenciales, eliminación de archivos y llamadas salientes a otro servicio. Un controlador GET que llama a un servicio interno puede parecer limpio en su propio repositorio mientras esa llamada interna realiza la modificación. Síguela hasta que puedas nombrar el efecto final.

Esta búsqueda sencilla encuentra mucho código antiguo:

```sh
rg -n 'GET|\.get\(|router\.get\(|app\.get\(' src
rg -n 'INSERT|UPDATE|DELETE|enqueue|publish|sendMail|charge|revoke|rotate' src
```

La forma de la salida importa menos que el registro de revisión que creas a partir de ella. Da a cada hallazgo un endpoint, desencadenante, efecto final, sistema afectado y lista de llamadores. No escribas «actualiza el estado» en la columna de efecto. Escribe «marca el despliegue d-481 como cancelado y envía la cancelación al planificador». Las entradas vagas permiten que un revisor apruebe sin atención una acción seria.

Los datos de ejecución encuentran lo que la revisión de código pasa por alto. En un entorno que no sea de producción, envía solicitudes representativas con un ID de correlación. Después consulta los registros de la aplicación, las tablas de trabajos, los registros de llamadas salientes y los registros de auditoría para ese ID. Si una solicitud GET lleva a un mensaje, un cambio de fila, un elemento en cola o una solicitud externa, registra toda la cadena. Una ruta puede modificar algo mediante un trabajador programado varios segundos después, lo que hace que un registro de solicitudes por sí solo resulte engañoso.

Presta atención a las modificaciones que los desarrolladores descartan porque no son escrituras en una base de datos relacional. Generar una URL de descarga de un solo uso consume una capacidad. Iniciar una exportación puede generar una factura elevada. Leer una ruta de «aceptación de invitación» puede añadir a una persona a una organización. Llamar a un endpoint de informes puede activar un costoso trabajo de almacén de datos. El recurso que devuelves puede ser de solo lectura, aunque la operación que lo produjo no lo sea.

## Las API heredadas ocultan escrituras en lugares conocidos

Las peores rutas heredadas suelen haber empezado como atajos para una página dirigida a personas. Alguien hizo que un enlace administrativo fuera fácil de pulsar, luego otro servicio copió la URL, después un script pasó a depender de ella y el atajo se convirtió en un contrato de API.

Los enlaces de confirmación de restablecimiento de contraseña son un caso clásico. Una ruta como `GET /reset/confirm?token=...` parece cómoda porque un navegador puede abrirla. Si abrir esa URL consume el token y cambia la contraseña, los escáneres de correo y las herramientas de vista previa pueden consumirlo antes. El diseño seguro usa GET para mostrar un estado de confirmación sin consumir nada y luego usa POST para enviar la confirmación. La página puede llevar una referencia de servidor de corta duración, pero la escritura ocurre únicamente después de la acción explícita.

Los enlaces para cancelar una suscripción requieren más cuidado. Los sistemas de correo y las leyes de privacidad hacen atractiva la cancelación con un clic, y algunos estándares la esperan. Si un escáner de seguridad de buzón sigue ese enlace, la persona destinataria puede perder una suscripción sin tocar el mensaje. Usa el mecanismo estándar de encabezado cuando corresponda, entiende cómo lo gestiona el ecosistema receptor y haz intencional el comportamiento del endpoint. No copies un patrón genérico de «GET para cancelar suscripción» a una API administrativa sin relación y lo llames precedente.

Otros infractores frecuentes incluyen:

- `GET /jobs/123/retry`, que crea una nueva ejecución cada vez que se actualiza un panel.
- `GET /deployments/123/rollback`, al que una sonda de monitorización puede llamar al probar enlaces.
- `GET /tokens/123/revoke`, que convierte una URL de soporte en una capacidad destructiva.
- `GET /invoices/123/send`, que transforma un bot de vista previa en un remitente de correo.
- `GET /reports/monthly`, que inicia silenciosamente una costosa exportación en lugar de devolver una.

La recomendación popular de «solo exige un parámetro secreto en la consulta» es equivocada. Las cadenas de consulta terminan en el historial del navegador, analítica, registros del servidor, encabezados Referer en algunos flujos, capturas de pantalla y mensajes copiados. Y, más importante, una URL secreta sigue siendo una URL GET. Cualquiera o cualquier cosa que la reciba puede activar la acción sin una barrera de aprobación.

## Los reintentos y las vistas previas amplían el alcance del daño

Un solo GET que modifica datos tiene una audiencia mayor de la que espera quien lo creó, porque los llamadores automatizados lo tratan como repetible. El primer síntoma suele parecer aleatorio: una operación ocurre dos veces, una cuenta cambia durante la noche o una persona ve una acción que no realizó. Los registros de solicitudes muestran credenciales válidas, por lo que el incidente se etiqueta como error del operador. Esa etiqueta suele cerrar la investigación demasiado pronto.

Considera un endpoint heredado que reinicia una compilación remota al recibir `GET /builds/77/retry`. Un agente obtiene la URL a través de una ruta de red que agota el tiempo después de que el servidor aceptó la solicitud. El agente hace lo que hacen muchos clientes HTTP y reintenta. La solicitud original ya ha encolado la compilación 311; la segunda encola la compilación 312. Después un panel carga un enlace de vista previa en el feed de actividad y encola la compilación 313. El controlador puede devolver `200 OK` cada vez, por lo que nada en la respuesta indica que la operación se duplicó.

Una redirección puede añadir otra sorpresa. Si una acción GET antigua redirige a una ruta nueva y esta sigue actuando con GET, la redirección conserva la semántica insegura. Si la redirección cambia el método de una forma que el cliente no espera, los clientes pueden fallar de manera inconsistente. Las redirecciones ayudan en una migración, no son un lugar donde ocultar un cambio en la autorización o en la semántica del método.

Las cachés hacen que el fallo sea más extraño. Una caché compartida no debería almacenar la respuesta de un GET que modifica datos sin instrucciones explícitas, pero los sistemas cometen errores y los desarrolladores añaden encabezados de caché de forma mecánica. Incluso sin almacenamiento en caché, un precargador puede emitir la solicitud antes de que la persona decida actuar. No construyas la seguridad sobre la esperanza de que cada intermediario respete tu intención privada.

La reparación empieza en la barrera. Una operación que puede cambiar un sistema remoto necesita una solicitud de acción explícita antes de que el cliente HTTP la envíe. Quien llama debe ver un nombre de acción, objetivo y consecuencia diferenciados. Un tiempo de espera tras el envío pasa a ser entonces una escritura incierta, que quien llama gestiona mediante una consulta de estado o una clave de idempotencia, en lugar de repetirla a ciegas.

## Da a las acciones un contrato con forma de escritura

Un endpoint reparado debe mostrar el cambio en su URI, método, cuerpo de la solicitud, respuesta y documentación. No necesitas un debate REST cargado de sustantivos para hacerlo bien. Necesitas un contrato que impida que quienes llaman confundan una acción con una consulta.

Para el ejemplo de la compilación, usa un endpoint de acción POST y acepta una clave de idempotencia. El endpoint debe devolver un recurso que identifique la nueva ejecución, no un mensaje genérico de éxito.

```http
POST /v1/builds/77/retries HTTP/1.1
Idempotency-Key: 9ef8b462-97bf-4ca3-bb8b-4396a60ed9ae
Content-Type: application/json

{"reason":"retry after failed dependency download"}
```

```http
HTTP/1.1 201 Created
Content-Type: application/json
Location: /v1/builds/311

{"id":"311","source_build":"77","state":"queued"}
```

Guarda la clave de idempotencia junto con el principal autenticado, el tipo de acción, el objetivo y el resumen criptográfico de la solicitud. Si el mismo llamador envía de nuevo la misma clave y la misma solicitud, devuelve el resultado original. Si reutiliza la clave con un cuerpo u objetivo distintos, devuelve un conflicto. Un registro de claves compartido globalmente puede permitir que un tenant choque con otro, mientras que una clave que ignora el cuerpo de la solicitud puede convertir un error de copiar y pegar en la acción equivocada.

Usa PUT o PATCH cuando la solicitud describa el estado deseado del recurso. `PATCH /v1/deployments/77` con `{"paused":true}` puede tener sentido cuando el recurso posee ese campo. `POST /v1/deployments/77/rollback` describe mejor un comando con una nueva ejecución, un registro de auditoría y un posible resultado asíncrono. No fuerces un comando a PATCH solo para satisfacer la guía de estilo de alguien.

Devuelve suficiente estado para que quien llama pueda recuperarse de la ambigüedad. Si una acción se ejecuta de forma asíncrona, devuelve un ID de operación y proporciona un endpoint GET que solo lea su progreso. Ese GET se puede reintentar, consultar periódicamente, almacenar en caché según sus encabezados de respuesta y abrir en un navegador sin cambiar el mundo.

## La aprobación debe ocurrir antes de inyectar la credencial

La aprobación después de que una solicitud HTTP ha salido de la máquina es puro teatro. Un servicio remoto puede actuar antes de que el cliente reciba una respuesta, y una respuesta de error no demuestra que no haya hecho nada. Toma la decisión donde se prepara la solicitud, antes de adjuntar las credenciales y antes de que los bytes salgan del proceso.

Esto importa cuando un agente de programación con IA llama a una API. El agente puede deducir que una ruta es de lectura por la descripción de una herramienta, copiar una URL antigua de un repositorio o seguir una sugerencia de un ticket. Si tiene credenciales sin restricciones, puede hacer la llamada antes de que una persona vea el endpoint. Un prompt que pide al modelo que tenga cuidado no es un control de autorización.

Da a la capa de aprobación un modelo de acción normalizado. Debe incluir al menos el método HTTP, el host, la ruta, el identificador de objetivo cuando exista y una consecuencia breve. La capa debe clasificar las acciones mediante el contrato de servicio, no solo con `method === "GET"`. Una ruta GET heredada que llama a `revokeToken` debe entrar en la misma ruta de aprobación que `POST /tokens/123/revoke` hasta que la elimines.

Una asignación práctica puede tener este aspecto:

```json
{
  "method": "GET",
  "url": "https://api.example.test/v1/tokens/tk_42/revoke",
  "semantic_action": "revoke credential",
  "target": "tk_42",
  "approval": "required",
  "reason": "legacy GET endpoint changes remote credential state"
}
```

No muestres a quien aprueba solo un nombre de host y un botón verde. El aviso debe indicar que la llamada revoca una credencial y nombrar el objetivo que está a punto de afectar. Si tu sistema no puede determinar la acción semántica, trata la llamada como no clasificada y exige aprobación. Permitir todas las solicitudes GET por el mero hecho de ser GET reproduce el fallo original una capa más abajo.

La autorización por sesión y los controles de credenciales por llamada de Sallyport pueden situarse en esta barrera para agentes que usan su canal HTTP. La decisión de diseño importante no depende de la app: quien guarda la bóveda ejecuta la solicitud, mientras el agente recibe el resultado y no el secreto.

## Mantén útiles las lecturas y difíciles de activar por accidente las escrituras

El patrón de migración limpio conserva un GET seguro para descubrir información e introduce un endpoint de escritura separado para realizar el acto. Puedes mantener una página amigable, un endpoint de estado o una respuesta de simulación sin permitir que una consulta ejecute el comando.

Para un generador de informes, `GET /reports/monthly` puede devolver el informe completado más reciente y el estado de generación actual. `POST /reports/monthly/runs` inicia una nueva generación. Para una operación de credenciales, `GET /tokens/tk_42` puede devolver metadatos, mientras `POST /tokens/tk_42/revocations` crea un evento de revocación. El segmento de ruta adicional es menos ingenioso que un parámetro de acción en la consulta, pero deja mucho más claros los registros, los clientes y las pantallas de revisión.

Una simulación merece un contrato preciso. `POST /deployments/77/rollback?dry_run=true` sigue siendo un POST porque quien llama solicitó evaluar un comando, aunque no confirme nada. Devuelve los objetivos previstos, las condiciones previas esperadas y cualquier valor sin resolver. No hagas que `GET /rollback?preview=true` ejecute la planificación del comando si planificar por sí mismo adquiere bloqueos, reserva capacidad o contacta a un proveedor con un efecto observable.

Algunos equipos intentan conservar integraciones antiguas haciendo que el GET anterior devuelva una página HTML con un formulario que envía automáticamente un POST. Eso solo traslada el riesgo al navegador. Usa una página que requiera una interacción real de la persona y protege el POST con las defensas de mismo origen adecuadas para la aplicación. Los clientes API deben recibir una respuesta clara de desuso y una fecha límite de migración, no un documento de navegador que no pueden usar.

## Prueba a quienes llaman y nunca piden permiso

Una ruta no está corregida hasta que pruebas el comportamiento automatizado que la volvió insegura. Las pruebas unitarias que confirman que un controlador llama a un método de servicio no bastan. Prueba la ruta tal como la encontrarían un navegador, un cliente HTTP con tiempo de espera, un rastreador y un agente.

Para cada acción migrada, ejecuta estas comprobaciones en un entorno aislado:

1. Envía el GET antiguo dos veces y confirma que no puede crear dos acciones. Durante una transición debe fallar de forma segura, mostrar solo un estado de confirmación o devolver una respuesta de desuso.
2. Simula un cliente que pierde la respuesta después del envío y luego repite el POST con la misma clave de idempotencia. Confirma que el servicio devuelve el ID de la acción original.
3. Obtén repetidamente la URL de estado segura y confirma que no crea trabajos, mensajes, asientos de libro mayor ni llamadas externas.
4. Intenta la acción con una aprobación vencida o una sesión revocada y confirma que la solicitud nunca llega al servicio remoto.
5. Inspecciona el registro de auditoría y comprueba que identifica la acción normalizada, no solo la ruta de transporte.

Usa inyección de fallos en el punto incómodo: después de que el servidor confirme la acción, pero antes de enviar una respuesta. Ahí es donde los equipos descubren si su cliente reintenta a ciegas. Si la única estrategia de recuperación es «vuelve a intentarlo», el contrato no ha dado a quien llama información suficiente.

Prueba también los ejemplos de la documentación. Un comando curl copiado en un canal de incidentes se convierte en una interfaz operativa. Si el ejemplo usa GET porque cabe en una línea, alguien lo automatizará. Haz que el ejemplo de lectura segura y el ejemplo de acción explícita sean visiblemente distintos.

## Audita el efecto además de la ruta

Una línea de auditoría que dice `GET /v1/builds/77/retry 200` es una prueba deficiente. Registra un hecho de transporte mientras oculta el evento de negocio. Durante un incidente, quien investiga todavía debe reconstruir si la llamada inició una compilación, reintentó una anterior o simplemente devolvió su estado.

Registra ambas capas. Conserva el método y la ruta recibidos porque el comportamiento heredado importa. Junto a ellos, registra la acción semántica, el objetivo, el proceso o principal que llama, la decisión de aprobación, la identidad de la credencial sin material secreto, el ID de correlación y la referencia del resultado. Para un comando asíncrono, registra el ID de operación o el ID de recurso resultante para que los eventos posteriores se vinculen con la solicitud original.

Un registro a prueba de manipulaciones solo sirve si puedes verificarlo cuando la confianza ya está en duda. Mantén el proceso de verificación separado de la ruta de lectura normal de la aplicación. Sallyport proyecta sus diarios de sesión y llamadas desde un registro de auditoría cifrado, ciego para escritura y encadenado por hash, y `sp audit verify` comprueba esa cadena sin conexión sobre texto cifrado. Es el tipo de propiedad que conviene exigir cuando un agente tuvo autoridad para afectar un sistema externo.

No permitas que la retención de auditoría se convierta en una excusa para registrar secretos. Los cuerpos de las solicitudes suelen contener credenciales, tokens, datos personales o argumentos de comandos que no pertenecen a un registro general de actividad. Registra una descripción normalizada y un resumen criptográfico cuando necesites pruebas de integridad. Guarda el material sensible solo donde los controles de acceso y las reglas de retención puedan respaldarlo.

## Elimina la excepción en lugar de documentarla para siempre

El estado final no tiene rutas GET que modifiquen datos, aunque una puerta de enlace de aprobación las detecte ahora. Mantener viva la excepción invita a que un cliente nuevo, una URL copiada o una futura refactorización eludan la tabla de clasificación. La capa de compatibilidad debe tener una persona responsable, un inventario de llamadores y una fecha en la que deje de aceptar la forma antigua.

Empieza por el endpoint que pueda causar el resultado más irreversible. Añade un contrato POST explícito, comportamiento idempotente, clasificación de aprobación y un registro de auditoría a nivel de efecto. Después haz observable cada llamada GET antigua. Cuando puedas nombrar a quienes siguen llamando, muévelos de forma deliberada en lugar de romper una integración oculta por sorpresa.

No aceptes «nuestro cliente sabe más» como una propiedad de seguridad. Una solicitud GET viaja por sistemas diseñados para repetirla e inspeccionarla. Haz que tu escritura parezca una escritura antes de que uno de esos sistemas decida ayudar.
