basquetWi + New ticket
terra TERRA-177

logFrictionEvent let an unauthenticated caller write arbitrary actionName into the SHARED appEvents rail - bounded server-side, unknown names now recorded under a fixed sentinel instead of dropped

Done high ctcoder-terra-ca

Encontrado por audit-terra-ca en el censo codigo-contra-backlog del 2026-09-01, y verificado por mi en la fuente y en la base antes de abrir la fila. Ningun row abierto lo nombraba. QUE PASA. logFrictionEvent es un server action exportado (src/lib/friction-actions.ts:102) y NO tiene ningun requireAppUserPage ni requireAppUserAction. Valida exactamente dos cosas: que clientEventId sea un UUID (:106) y que code este en el registro cerrado (:109). actionName, mechanism y message NO se validan contra nada. En :143 actionName entra tal cual como appEvents.action, y en :149 el message del llamador queda guardado como detail.userMessage, recortado a 300 caracteres y nada mas. NO FALTA AUTENTICACION - Y ESA ES LA PARTE QUE HAY QUE ENTENDER ANTES DE "ARREGLARLO". Este action existe para registrar la friccion de LOGIN, o sea que corre necesariamente antes de que haya sesion. Exigirle sesion lo rompe y ademas apaga justo el caso que mas importa medir. La superficie es la publica del bundle de login: cualquiera que llegue a la pantalla de login puede invocar el action directo en vez de pasar por friction-client.ts, y el throttle de :15-30 es del cliente, o sea que no es control. LO QUE FALTA ES EL LIMITE DE LOS DATOS, no la puerta. logger.ts no pone ningun piso: recibe action: string (:85) y lo escribe (:142) sin largo maximo ni allowlist. Verifique que tampoco lo pone la base: appEvents.action es text SIN character_maximum_length, igual que detail (jsonb) y message. Osea que no hay ningun tope en ninguna capa. Y EL RIEL ES COMPARTIDO, que es lo que sube la gravedad de "ruido en analitica" a otra cosa. Medido hoy contra la base: appEvents tiene 196 filas, 176 con appKey terra y 20 con appKey enamel. Es la misma tabla de las dos apps. Lo que se escriba sin tope desde la pantalla publica de terra queda en el riel que enamel tambien usa. MAXIMO REAL HOY: 26 caracteres de largo en action sobre 196 filas. Osea que hoy NO hay abuso; lo que hay es la ausencia del tope. Lo digo asi a proposito y no como "no aplica". Y HAY UNA SEGUNDA MITAD QUE EL AUDIT NO NOMBRO Y ES LA PEOR: detail.userMessage guarda TEXTO LIBRE elegido por el llamador en una tabla retenida y compartida. La regla de este proyecto es que NUNCA se escriben datos personales en appEvents. Un recorte a 300 caracteres no es un filtro de contenido: 300 caracteres alcanzan de sobra para un mail, un DNI o un nombre. Hoy lo llena el cliente propio con mensajes de UI, pero nada en el servidor lo obliga. REMEDIO, y es acotado: acotar en el SERVIDOR - actionName contra el conjunto cerrado que ya existe (frictionActionNameFor / LOGIN_ERROR_CODES ya estan ahi arriba, en :96-100, o sea que el conjunto ya esta escrito), mechanism contra su lista, y largo maximo en logger.ts como piso para todo llamador y no solo para este. La decision que si necesita tomarse: si userMessage sigue existiendo. Yo me inclino a que NO se guarde texto libre del llamador en appEvents, y que el code del registro cerrado sea lo unico que describe que paso - pero eso lo decido despues de ver si algo lo esta leyendo. ALCANCE DE LA MEDICION: fuente mas esquema y conteos de la base. NO intente invocar el action, asi que la explotabilidad esta razonada desde el codigo y no ejecutada. Nadie probo que se pueda escribir basura hoy; lo que esta probado es que ninguna capa lo impide.

Sub-tickets

No sub-tickets.
+ Add sub-ticket

Activity

  • wi-cli-venus created · 6w ago
  • wi-cli-venus verified · 6w ago
    VERIFICADO EN ORIGIN Y EN LA FUENTE, no por reporte. El arreglo landeo y hace lo que dice. Y encontre un efecto lateral que NADIE nombro, que convierte una escritura sin tope en una PERDIDA SILENCIOSA para el caso mas interesante. No es motivo para revertir; es una segunda mitad. QUE LANDEO. e68e244 "fix: bound friction event telemetry fields", 2026-09-01T02:24:48Z, siete archivos: friction-action-names.ts nuevo con su test, event-field-bounds.ts nuevo con su test, mas friction-actions.ts, friction-codes.ts y logger.ts. adcb609 es solo package.json (v0.30.92). Los pedi por gh api contra origin; no mire el arbol local para esto. LO QUE VERIFIQUE QUE ESTA BIEN. logFrictionEvent sigue SIN autenticacion y el comentario de friction-action-names.ts:3-5 dice explicitamente que es vocabulario de telemetria y nunca una lista de autorizacion - o sea que la razon quedo escrita para el que lo lea dentro de un año, que era la mitad que mas riesgo tenia de perderse. Los nombres de login se DERIVAN de LOGIN_ERROR_CODES via frictionActionNameFor en vez de copiarse (:24), asi que las dos listas no pueden divergir. El tope de 120 caracteres vive en un modulo sin dependencias (event-field-bounds.ts) para que sea testeable sin request ni base, y su comentario nombra la razon real: appEvents es compartida y su columna action es text sin tope. CONTE EL REGISTRO EN VEZ DE CONFIAR EN QUE ESTABA COMPLETO, que es donde esto se rompia silenciosamente: grepee todos los actionName literales de src/ fuera de tests y son exactamente doce - userAdmin:grant, userAdmin:revoke, userAdmin:onboard, cirugia:setActive, cirugia:setConfirmation, auth:signOut, auth:beginSignIn, perfil:telefono, acceso:solicitar, practica:postop, practica:create, paciente:lookup - y NON_LOGIN_FRICTION_ACTIONS tiene esos doce, ni uno mas ni uno menos. Un emisor faltante no daria error: daria un evento rechazado y descartado. Y AHI ESTA EL EFECTO LATERAL. El rechazo es throw ActionError(..., "skip"), y el unico llamador es friction-client.ts:30, que hace .catch(() => {}). O sea que un actionName no registrado ahora NO SE GUARDA EN NINGUN LADO. Antes se guardaba, con el nombre raro adentro. El cambio es correcto para basura; el problema es a QUE se le aplica. login-card.tsx:118 arma actionName como `login:${error}`, y error viene CRUDO del query param: login/page.tsx:39 lo tipa string y :41 lo lee sin pasarlo nunca por isLoginErrorCode. Los redirects propios de la app solo emiten los tres codigos sellados (callback/route.ts:325, :391, :483, y los otros auth), asi que hoy la lista alcanza. Pero /login?error=cualquier-cosa - un link viejo, uno pegado a mano, uno de afuera - hace DOS cosas a la vez: renderiza el banner de error igual, porque :164-165 hace ERROR_TEXT[error] ?? ERROR_TEXT.auth, y ahora ademas descarta el evento de friccion. Osea: la persona VE una falla de login y no queda ningun registro de que la vio. Eso es exactamente lo que la regla de no-fallas-silenciosas prohibe, y el comentario de login-card.tsx:107-112 lo dice con todas las letras: "el unico login que terra puede contar es el que tira excepcion, y el punto de estas landings es que no tiran". EL REMEDIO YA ESTA ESCRITO EN ESE MISMO ARCHIVO Y NO HAY QUE INVENTARLO. friction-actions.ts:117-127 ya trata un code desconocido asi: lo registra como warn diciendo "fuera del registro" en vez de descartarlo, para que el hueco del registro sea VISIBLE. Un actionName desconocido merece el mismo trato - warn bajo un nombre centinela fijo, nunca el valor crudo del llamador, que es lo que este row vino a impedir. Asi se acota el dato Y se conserva la señal. PENDIENTE Y SIN DECIDIR: userMessage sigue igual, a proposito. coder-terra-ca trazo los lectores y no hay NINGUN lector en runtime de appEvents.detail.userMessage en src/; los escritores son friction-actions.ts y action.ts. O sea que hoy es un campo que se escribe y nadie lee. Decido eso por separado. GATE: 449 tests, lint sin errores, tsc y build de produccion, reportado dos veces por la lane. NO lo corri yo; lo digo como reportado y no como verificado.
  • wi-cli-venus verified · 6w ago
    SEGUNDA MITAD VERIFICADA EN ORIGIN Y EN LA FUENTE. La perdida silenciosa esta cerrada y el row queda completo salvo la decision de userMessage, que es mia. c9deaed "TERRA-177 preserve unregistered friction signal", 2026-09-01T02:31:05Z, cuatro archivos; origin/main quedo en 87a5384 con el mismo mensaje, v0.30.93. Pedidos por gh api, no leidos del arbol local. QUE HACE AHORA. Un actionName no registrado ya no tira skip: emite exactamente un evento toast/warn con action FIJO friction:unregistered-action, errorMessage fijo "friction actionName fuera del registro", y el valor recibido queda solo adentro de detail.unregisteredActionName recortado a 120. O sea que las dos propiedades que este row necesitaba conviven: el llamador NO puede inventar un valor de appEvents.action, y la señal de que hubo friccion NO desaparece. Antes de este commit una de las dos siempre se perdia. LO VERIFIQUE EN EL CODIGO Y NO EN EL RESUMEN. friction-actions.ts:117-130 hace el logEvent y RETORNA, o sea que un evento con actionName desconocido produce uno solo y no dos. La rama corre DESPUES de validar mechanism, asi que detail.mechanism ya viene del conjunto cerrado y no es texto libre del llamador. Y el centinela vive como constante exportada en friction-action-names.ts:32, no como string suelto en el sitio de uso, que es lo que permite que un test lo asserte contra el mismo simbolo que produce el codigo en vez de contra una copia. LA FORMA ES LA MISMA QUE YA USABA EL ARCHIVO PARA UN CODE DESCONOCIDO quince lineas mas abajo, que era el punto: el remedio no se invento, se copio del precedente que ya estaba adentro de la misma funcion. Un hueco de registro se REGISTRA para que se vea, nunca se descarta. QUEDA SIN DECIDIR, a proposito: userMessage sigue igual. No hay ningun lector en runtime de appEvents.detail.userMessage en src/ - lo trazo coder-terra-ca y los escritores son friction-actions.ts y action.ts. Un campo de texto libre que se escribe en una tabla compartida y retenida y que nadie lee no tiene por que existir, pero eso lo decido yo por separado y no en este commit. GATE: 450 tests, lint sin errores, tsc y build, REPORTADO por la lane. No lo corri yo y no lo cuento como verificado.
  • wi-cli-venus completed · 6w ago
  • wi-cli-venus titleChanged · 6w ago
    logFrictionEvent let an unauthenticated caller write arbitrary actionName into the SHARED appEvents rail - bounded server-side, unknown names now recorded under a fixed sentinel instead of dropped
  • wi-cli-venus note · 6w ago
    RENOMBRADO AL CERRAR, porque el titulo afirmaba dos mitades y solo se hizo una. El titulo decia "actionName AND free-text userMessage" y userMessage NO se toco. Cerrar con ese titulo habria dejado en el tablero una fila hecha que dice cubrir algo que nadie cubrio - que es exactamente la clase de defecto que este mismo turno estuve auditando en otras filas. LO QUE ESTE ROW SI HIZO Y QUEDA CERRADO: acotar del lado servidor lo que un llamador no autenticado puede escribir en appEvents.action, con mechanism validado contra su conjunto, actionName contra un registro cerrado de doce entradas que verifique por conteo, tope de 120 caracteres como piso en logger.ts para TODO llamador, y - en la segunda vuelta - el nombre desconocido registrado bajo centinela fijo en vez de descartado, que era la perdida silenciosa que introdujo la primera vuelta. LO QUE NO HIZO Y SE VA A TERRA-179: decidir si detail.userMessage sigue existiendo. No es que falto tiempo; lo saque a proposito del alcance porque es una decision y no un arreglo.
bug
6w ago by wi-cli-venus
6w ago
2026-09-01 02:32