Supongamos que Alice pertenece al espacio alpha. Solicita /workspaces/alpha/documents/doc-b/ y la respuesta contiene un documento de beta. El servidor reconoció a Alice y comprobó su pertenencia al espacio. Ambas comprobaciones pasaron. ¿Dónde se desvió la solicitud?
Este ejemplo didáctico usa documentos e identidades ficticios. Requiere Node.js 22 o posterior; el método de lectura sirve también para otros lenguajes y frameworks. La pregunta útil es concreta: ¿qué espacio se comprobó y cuál limitó la consulta del documento?
Empieza con una solicitud y la regla que debe cumplir
Mantén juntos método, ruta, identidad y resultado esperado:
GET /workspaces/alpha/documents/doc-b/
Demo identity: alice, a member of alpha only
Stored document: doc-b belongs to beta
Expected: 404, with no document in the response
Broken result: 200, containing Beta release notes
La política del ejemplo concede a los miembros acceso a todos los documentos de su espacio. No hay documentos compartidos individualmente ni excepciones de administrador. Para un miembro de alpha, un documento externo a alpha obtiene el mismo estado y cuerpo que uno inexistente. Una solicitud que nombre un espacio al que el usuario no pertenece recibe 403 antes de llamar al almacén.
Hay dos relaciones que seguir: el usuario autenticado debe pertenecer al espacio de la URL y el documento devuelto debe pertenecer a ese mismo espacio. Comprobar la primera deja la segunda en manos de la consulta de datos.
En un informe real, registra también la compilación o versión de código. La ruta de tu copia debe coincidir con la que atendió la solicitud. Usa un fixture sintético para reproducir localmente.
Encuentra el registro de la ruta antes del manejador
Descarga request-flow.mjs y request-flow.test.mjs en la misma carpeta. Busca los sustantivos distintivos de la ruta, como workspaces y documents, con el editor. Si tienes ripgrep, estos comandos acotan los siguientes pasos:
rg -n 'workspaces|documents' request-flow.mjs
rg -n 'middleware|runMiddleware|requireWorkspaceMember|findById|findInWorkspace' request-flow.mjs
Abre createApp. Su objeto de ruta reúne método, patrón, middleware y manejador seleccionado:
const route = {
method: 'GET',
pattern: /^\/workspaces\/([a-z0-9-]+)\/documents\/([a-z0-9-]+)\/$/,
middleware: [authenticate, requireWorkspaceMember],
handler: lookup === 'unscoped' ? getDocumentUnsafe : getDocument,
};
Sigue también las líneas posteriores. handle hace coincidir new URL(request.url).pathname, comprueba el método y asigna los segmentos capturados a workspaceId y documentId. Ninguno procede de parámetros de consulta. El ejemplo acepta minúsculas, dígitos y guiones y exige la barra final mostrada; no es un enrutador general.
En un framework, la ruta completa puede dividirse entre un montaje padre como /workspaces/:workspaceId y una ruta hija como /documents/:documentId. Lee el montaje y confirma cómo llegan los parámetros del padre. Encontrar el manejador no revela qué middleware envuelve su llamada.
Lee el middleware en su orden real
runMiddleware usa reduceRight para envolver el manejador. Con [authenticate, requireWorkspaceMember], primero autentica, después comprueba pertenencia y finalmente ejecuta el manejador cuando cada paso llama a next(). Los valores de return next() vuelven a handle, cuyo catch convierte un HttpError lanzado en respuesta.
Request → path and method match → authenticate
→ requireWorkspaceMember → document lookup → JSON Response
↘ thrown error → handle's catch → error Response
authenticate busca la cabecera Authorization en un mapa fijo y guarda el resultado en ctx.user. demo-alice y demo-bob son entradas públicas del fixture que representan una sesión verificada. Bastan para seguir el origen de la identidad aquí; la verificación real de sesiones o tokens debe leerse aparte.
Abre ahora el middleware de pertenencia:
async function requireWorkspaceMember(ctx, next) {
const { workspaceId } = ctx.params;
const allowed = ctx.user.workspaceIds.includes(workspaceId);
ctx.trace.push(`membership:${workspaceId}:${allowed ? 'allowed' : 'denied'}`);
if (!allowed) throw new HttpError(403, 'Forbidden');
return next();
}
Para Alice y una ruta con alpha, allowed es true. Esta función no examina documentId ni carga documentos. Anota la conclusión exacta: «Alice puede entrar en la ruta alpha». Escribir solo «autorizada» puede atribuir a la comprobación más de lo que hace.
Señala también dónde se detiene. Una identidad ausente o desconocida produce 401 antes de comprobar pertenencia. Un no miembro obtiene 403 antes del manejador de documentos. Si el middleware llama a next() antes de decidir, el manejador podría haberse ejecutado ya; la posición de la llamada importa tanto como el nombre.
Separa usuario, espacio solicitado y documento guardado
Para la solicitud fallida, una nota útil cabe en cuatro filas:
| Valor | Origen | Valor de esta solicitud | Dónde seguirlo |
|---|---|---|---|
| Usuario y pertenencias | authenticate lee el mapa de identidades |
Alice, ['alpha'] |
requireWorkspaceMember |
| Espacio solicitado | Primera captura de la ruta | alpha |
Comprobación de pertenencia y argumentos de consulta |
| Documento solicitado | Segunda captura de la ruta | doc-b |
Consulta del documento |
| Espacio real del documento | Fila almacenada | beta |
Predicado de consulta y respuesta |
Añade archivos y líneas al inspeccionar productores y consumidores. En una aplicación mayor pueden ser tipos y paquetes distintos, pero la distinción sigue sirviendo: la URL expresa la petición, la identidad aporta pertenencias y la fila almacenada determina la propiedad.
Ambos identificadores llegan juntos. Comprueba ahora si salen juntos del manejador.
Abre la consulta aunque el manejador parezca normal
El manejador deliberadamente inseguro dice:
async function getDocumentUnsafe(ctx) {
ctx.trace.push(`lookup:unscoped:${ctx.params.documentId}`);
const document = await ctx.store.findById(ctx.params.documentId);
if (!document) throw new HttpError(404, 'Not found');
return json(200, document);
}
Solo documentId llega al almacén. Sigue findById hasta createStore: el predicado es document.id === documentId. No compara espacios ni comprueba después la propiedad antes de serializar. El middleware aceptó alpha; esta consulta puede devolver una fila de cualquier espacio.
El fallo no necesita una carrera ni una sesión falsificada. Alice conserva su identidad y la ruta permitida alpha, y proporciona doc-b del fixture. La aplicación comprueba un alcance de recursos y lee otro.
Un nombre de función sería evidencia más débil en un proyecto real. Un método de repositorio podría añadir un filtro de tenant internamente, heredar una sesión de base de datos acotada o depender de políticas por fila. Lee la implementación seleccionada y su configuración antes de concluir que omitir un argumento implica una consulta sin alcance. Aquí el predicado en memoria es toda la consulta y se puede comprobar directamente.
Ejecuta el contraejemplo por toda la cadena
Desde la carpeta de ambas descargas, ejecuta:
node request-flow.mjs
node --test request-flow.test.mjs
No necesitas instalación, cuenta ni servicio. makeRequest construye un Request web y las pruebas lo pasan a handle, que devuelve un Response. La URL example.invalid solo se analiza en memoria. El ejemplo nunca llama a fetch ni abre un socket de escucha.
La demostración imprime estas respuestas y, bajo cada una, los pasos visitados y llamadas registradas al almacén:
normal: 200 {"id":"doc-a","workspaceId":"alpha","title":"Alpha release notes"}
reproduce: 200 {"id":"doc-b","workspaceId":"beta","title":"Beta release notes"}
fixed: 404 {"error":"Not found"}
Para la solicitud incorrecta, los pasos son:
request:GET -> route:alpha:doc-b -> authenticate:alice -> membership:alpha:allowed -> lookup:unscoped:doc-b -> response:200
El array de traza es instrumentación añadida al fixture y queda fuera de la respuesta. La prueba también comprueba el documento devuelto y la llamada registrada a findById('doc-b'). Estas aserciones conectan las ramas recorridas con los datos recibidos.
La prueba reproduce: pasa cuando la versión insegura devuelve el documento de beta. Su resultado verde confirma la reproducción. Otra prueba exige que la implementación corregida devuelva 404 ante la misma solicitud.
Lleva el espacio comprobado hasta la consulta
El manejador corregido conserva ambos valores:
async function getDocument(ctx) {
const { workspaceId, documentId } = ctx.params;
ctx.trace.push(`lookup:scoped:${workspaceId}:${documentId}`);
const document = await ctx.store.findInWorkspace(workspaceId, documentId);
if (!document) throw new HttpError(404, 'Not found');
return json(200, document);
}
Dentro de findInWorkspace, el predicado exige document.workspaceId === workspaceId y document.id === documentId. El middleware ya comprobó ese mismo workspaceId. El fixture no modifica ctx.params entre esos pasos; documentos y pertenencias permanecen fijos durante cada ejecución.
Para /workspaces/alpha/documents/doc-b/ ya no hay coincidencia. Tanto un identificador ausente como uno de otro espacio producen 404 con {"error":"Not found"}. Igualar estado y cuerpo evita revelar explícitamente cuál ocurrió; las pruebas no afirman nada sobre tiempos u otros canales laterales.
Comprobar document.workspaceId tras una lectura global también podría detener esta filtración concreta. La consulta acotada expresa la restricción al llamar al almacén y no entrega filas ajenas al manejador. En memoria, Array.find aún examina filas para evaluar el predicado. Las llamadas registradas muestran argumentos, no cantidades de E/S ni aislamiento físico de base de datos.
Si lo aplicas a SQL u ORM, examina la condición emitida y la vinculación de parámetros, y prueba la capa de datos real. Revocar pertenencias, mover documentos, compartirlos y otras rutas necesitan reglas y comprobaciones propias.
Prueba el éxito y los puntos donde debe detenerse
La suite tiene diez pruebas: una reproducción intencional y nueve comprobaciones de la aplicación corregida. Varias ejercitan más de una entrada:
| Solicitud o condición | Respuesta esperada | Comportamiento del almacén |
|---|---|---|
Alice lee alpha/doc-a; Bob lee beta/doc-b |
200, documento correcto |
Ambos identificadores llegan a findInWorkspace |
Alice lee alpha/doc-b |
404, sin documento |
La consulta sigue acotada a alpha |
Alice lee alpha/missing |
Mismo estado y cuerpo que la fila anterior | Sin documento coincidente |
| Identidad de demostración ausente o desconocida | 401 |
Sin llamada al almacén ni comprobación de pertenencia |
Alice lee beta/doc-b |
403 |
Sin llamada al almacén |
Alice lee alpha/doc-b?workspaceId=beta |
404 |
La consulta no sustituye el alcance de la ruta |
| Ruta desconocida; método incorrecto en ruta conocida | 404; 405 con Allow: GET |
Sin llamada al almacén |
| El almacén lanza un error inesperado | 500, cuerpo genérico |
El error llega al envoltorio de respuesta |
Las rutas abreviadas se expanden a /workspaces/{workspaceId}/documents/{documentId}/, con la consulta después de la barra final. Las dos lecturas correctas impiden una «corrección» que rechace todo. Las aserciones sin llamadas detectan rechazos que llegan después de acceder al almacén. Juntas comprueban más que estados finales.
Cubren el manejo de solicitudes dentro del proceso, el orden del middleware, los argumentos de consulta y las respuestas JSON. No ejercitan transporte HTTP, proxies inversos, verificación de identidad de producción ni bases de datos. En una aplicación real, empieza por su adaptador HTTP y verifica que seleccione el manejador y middleware que seguiste.
Deja una nota que otra persona pueda verificar
Organiza el resultado alrededor de la solicitud y los lugares exactos donde se usan sus valores:
Trigger: Alice + GET /workspaces/alpha/documents/doc-b/
Route: createApp captures alpha and doc-b.
Membership: requireWorkspaceMember allows Alice for alpha.
Old read: getDocumentUnsafe passes only doc-b to findById.
Observed: 200 returns a row whose workspaceId is beta.
Fix: getDocument passes alpha and doc-b to findInWorkspace.
Regression: same request returns 404; both valid member reads still return 200.
Scope: in-memory request pipeline; production adapter and storage are untested.
Adjunta ubicaciones y comando de prueba. Si alguien pregunta «¿pero esta ruta no tiene ya autorización?», la nota muestra qué cubría la comprobación y dónde dejó de usarse el espacio.