Auditoria de Seguridad Β· Contexto Arquitectonico

FarmLedger Β· BatchTransfer β€” Construccion de Contexto

Modulo: batch_transfer.py  Β·  Version: FarmLedger v2.3.1  Β·  Metodologia: First Principles + 5 Whys + 5 Hows
βœ“ Fase 1 β€” Orientacion βœ“ Fase 2 β€” Analisis Micro βœ“ Fase 3 β€” Modelo Global
Estadisticas del Informe de Contexto
6
Bloques analizados
18
Invariantes documentadas
23
Suposiciones registradas
7
Clusters de fragilidad
πŸ—ΊοΈ

Fase 1 β€” Orientacion Inicial (Bottom-Up Scan) Mapeo preliminar sin asumir comportamiento

Modulos / Archivos Identificados
  • batch_transfer.py β€” nucleo bajo analisis
  • app/db.py β€” sesion SQLAlchemy (db_session)
  • app/models.py β€” ORM: Batch, Transfer, Actor, Certificate
  • app/audit_log.py β€” emision de eventos (emit_event)
  • app/notifications.py β€” notificaciones externas (notify_actor)
  • JWT upstream (no visible en codigo) β€” fuente de requested_by
  • Base de datos relacional (PostgreSQL presumida via SQLAlchemy)
Actores del Sistema
Rol Entrypoints Puede Transferir A Nivel de Confianza
producer transfer_batch(from="producer") warehouse Parcial
warehouse transfer_batch(from="warehouse") exporter producer Parcial
exporter transfer_batch(from="exporter") buyer Parcial
buyer Solo receptor (no transfiere) Ninguno No verificado
JWT caller requested_by (parametro) β€” SIN VERIFICAR
Flujo de Propiedad de Lotes
Productor
origin
β†’
certificate req.
Almacen
almacenamiento
β†’
inspeccion
Exportador
logistica
β†’
cert. req.
Comprador
destino final
Variables de Estado Criticas (ORM)
Batch
  • id β€” PK, UUID
  • current_owner_id β€” FK Actor (mutable)
  • quantity β€” Float (mutable por split)
  • origin_batch_id β€” FK self (nullable)
Transfer
  • id β€” UUID
  • batch_id β€” FK Batch
  • from_actor_id / to_actor_id
  • quantity β€” snapshot al momento
Actor / Certificate
  • Actor.role β€” string sin enum en codigo
  • Certificate.valid β€” booleano, filtro critico
  • Certificate.batch_id β€” FK Batch
πŸ”¬

Fase 2 β€” Analisis Ultra-Granular por Bloque transfer_batch() Β· L1–L75 Β· con First Principles, 5 Whys, 5 Hows

Funcion analizada: transfer_batch(batch_id, from_actor_id, to_actor_id, requested_by, split_qty, override_cert)
Proposito: Transfiere la propiedad de un lote agricola entre actores de la cadena de suministro, con soporte para division de lotes y validacion de certificados. Actua como el unico punto de escritura del flujo de propiedad, por lo que su correctitud es critica para la integridad del trazado.
Bloque A Carga y bloqueo del lote (L18–L21) riesgo medio
# --- Block A: Load and lock batch --- batch = db_session.query(Batch).filter_by(id=batch_id).with_for_update().first() if not batch: raise HTTPException(status_code=404, detail="Batch not found")
Que hace
Recupera el registro del lote con bloqueo pesimista (SELECT FOR UPDATE) y aborta si no existe.
Por que aqui (orden)
Debe ser la primera operacion: no tiene sentido validar actores o certificados si el lote no existe. El bloqueo previene race conditions con otras transferencias concurrentes.
Suposiciones
batch_id proviene de entrada no validada db_session en contexto transaccional activo with_for_update() soportado por el driver DB
Invariantes establecidas
Si pasa: batch != None Lock exclusivo sobre la fila
5 Whys β€” ΒΏPor que with_for_update()?
1ΒΏPor que bloquear? Para evitar que dos transferencias del mismo lote ocurran simultaneamente.
2ΒΏPor que es posible? FastAPI es async y multiples requests pueden llegar para el mismo batch_id.
3ΒΏPor que es critico? Sin lock, dos actores podrian "ganar" la misma transferencia β†’ doble gasto de lote.
4ΒΏPor que no un lock optimista? El ORM no muestra columna version/etag β†’ lock pesimista es la unica opcion visible.
5ΒΏPor que puede fallar? Si db_session.commit() falla despues, el lock se libera pero el estado queda inconsistente β€” necesita revision.
⚠️
Suposicion de confianza β€” batch_id no autenticado
batch_id llega como string sin validar formato UUID. Un atacante puede inyectar IDs malformados. La autorizacion real se hace despues (Block B), no aqui.
Bloque B Verificacion de autorizacion (L23–L32) riesgo alto
# --- Block B: Authorization check --- if batch.current_owner_id != from_actor_id: raise HTTPException(status_code=403, ...) from_actor = db_session.query(Actor).filter_by(id=from_actor_id).first() to_actor = db_session.query(Actor).filter_by(id=to_actor_id).first() if from_actor.role not in ALLOWED_TRANSITIONS: ... if to_actor.role not in ALLOWED_TRANSITIONS[from_actor.role]: ...
Que hace
Verifica: (1) el llamador es el propietario actual, (2) el actor origen existe y tiene rol valido, (3) la transicion de rol esta permitida por ALLOWED_TRANSITIONS.
Suposiciones criticas
from_actor_id puede ser None si actor no existe to_actor puede ser None β†’ AttributeError no manejado requested_by NUNCA se valida contra from_actor_id
First Principles β€” ΒΏQue garantiza realmente este bloque?
FPLo que SI verifica: que current_owner_id == from_actor_id (propiedad del lote). La transicion de rol es valida segun la tabla ALLOWED_TRANSITIONS.
!Lo que NO verifica: que el usuario JWT (requested_by) sea el mismo actor que from_actor_id. Cualquier usuario autenticado puede transferir cualquier lote si conoce from_actor_id β€” esto es autorizacion solo parcial.
5 Hows β€” ΒΏComo podria fallar to_actor=None?
1Si to_actor_id es un UUID inexistente, la query retorna None.
2La linea to_actor.role lanzaria AttributeError (NoneType).
3FastAPI convierte AttributeError en HTTP 500 β€” informacion de traza en logs internos.
πŸ”΄
Invariante ausente: requested_by no vinculado a from_actor_id
El parametro requested_by (JWT user_id) nunca se compara con from_actor_id. Un usuario autenticado con JWT valido puede iniciar una transferencia en nombre de otro actor si conoce su actor_id. Separacion de autenticacion y autorizacion.
Bloque C Validacion de certificado (L34–L39) riesgo alto
# --- Block C: Certificate validation --- if not override_cert: cert = db_session.query(Certificate).filter_by( batch_id=batch_id, valid=True ).first() if not cert: raise HTTPException(status_code=422, ...)
Que hace
Si override_cert=False, exige que exista al menos un certificado con valid=True para el lote. Si override_cert=True, esta verificacion se omite completamente.
Invariantes
Si no override: cert.valid=True garantizado al pasar override_cert llega como bool del caller β€” no verificado rol
5 Whys β€” ΒΏPor que override_cert es peligroso?
1override_cert=True salta la unica validacion de calidad del lote.
2No existe logica que restrinja quien puede pasar override_cert=True.
3El parametro llega directamente del request HTTP (FastAPI lo deserializa).
4Cualquier actor autenticado puede eludir la certificacion con override_cert=True.
5Esto rompe la garantia de trazabilidad de calidad β€” razon de existir del sistema.
Bloque D Division opcional del lote (L41–L51) riesgo medio
# --- Block D: Optional split --- if split_qty is not None: if split_qty <= 0 or split_qty >= batch.quantity: raise HTTPException(status_code=400, ...) remainder = batch.quantity - split_qty new_batch = Batch( id=str(uuid.uuid4()), quantity=remainder, current_owner_id=batch.current_owner_id, # aun es from_actor_id origin_batch_id=batch.id, ... ) db_session.add(new_batch) batch.quantity = split_qty
Que hace
Divide el lote original: el fragmento de split_qty se transfiere; el remanente queda en manos del propietario actual como nuevo lote hijo.
Invariantes establecidas
batch.quantity + new_batch.quantity = original new_batch hereda current_owner_id de from_actor (correcto) split_qty es Float β€” posible precision de punto flotante
5 Hows β€” ΒΏComo podria perderse cantidad total?
1split_qty=0.1, batch.quantity=0.3 β†’ remainder=0.19999... (float imprecision).
2El Transfer de Block E registra quantity=batch.quantity post-split = 0.1 (correcto visualmente).
3new_batch.quantity=0.19999... en DB β€” discrepancia acumulativa en multiples splits.
4Los certificados del lote original NO se replican al new_batch automaticamente.
5El lote hijo queda sin certificado valido β†’ cualquier transferencia futura de new_batch requiere override_cert=True.
Bloque E Ejecucion de la transferencia (L53–L62) riesgo medio
# --- Block E: Transfer execution --- transfer = Transfer( id=str(uuid.uuid4()), batch_id=batch.id, from_actor_id=from_actor_id, to_actor_id=to_actor_id, transferred_at=datetime.utcnow(), quantity=batch.quantity, ) batch.current_owner_id = to_actor_id db_session.add(transfer) db_session.commit()
Que hace
Crea el registro Transfer y actualiza current_owner_id en el lote. El commit aqui es el punto de no retorno β€” todas las escrituras previas (split incluido) se persisten juntas.
Dependencias criticas
Depende del lock del Bloque A datetime.utcnow() β€” timezone-naive, puede causar confusion en registros requested_by no se registra en Transfer β€” audit gap
πŸ“‹
Observacion de trazabilidad
El modelo Transfer no almacena requested_by (el usuario JWT que hizo la llamada). Si from_actor y requested_by son distintos (ver Bloque B), el registro historico no refleja quien realmente inicio la transferencia β€” punto ciego de auditoria forense.
Bloque F Efectos secundarios post-transferencia (L64–L70) riesgo medio
# --- Block F: Post-transfer side effects --- emit_event("batch.transferred", {"batch_id": batch.id, ...}) notify_actor(to_actor_id, f"Batch {batch_id} received")
Que hace
Emite evento de auditoria y notifica al actor receptor. Ambas llamadas son externas (black box desde este modulo).
Analisis externo (Black Box)
emit_event: fallo silencioso no revierte el commit notify_actor: exception podria propagar HTTP 500 post-commit commit ya ejecutado β€” inconsistencia estado/notificacion posible
5 Whys β€” ΒΏPor que el orden commit β†’ side effects es fragil?
1El commit persiste la transferencia ANTES de confirmar que los side effects tuvieron exito.
2Si notify_actor lanza una excepcion, FastAPI devuelve 500 al cliente.
3El cliente asume que la transferencia fallo y reintenta β€” pero la transferencia YA se persiste.
4El reintento encuentra que batch.current_owner_id ya cambio β†’ error 403 o doble Transfer registro.
5El sistema queda en estado inconsistente: lote transferido, cliente sin confirmacion, posible bucle de reintentos.
🌐

Fase 3 β€” Modelo Global del Sistema Invariantes, flujos, fronteras de confianza y clusters de fragilidad

Invariantes del Sistema (multi-funcion)
GARANTIZADAS
  • Un lote tiene exactamente un current_owner_id en todo momento
  • La suma de quantity de un lote + sus hijos conserva la cantidad original
  • ALLOWED_TRANSITIONS define el grafo de roles β€” no se puede saltar roles
  • Toda transferencia genera un registro Transfer inmutable
  • El lock with_for_update previene doble-gasto concurrente
ASUMIDAS (no verificadas en codigo)
  • requested_by === from_actor_id (nunca validado)
  • Actor.role es un valor fijo y confiable (no validado como enum)
  • split genera nueva fila Certificate para el lote hijo (NO ocurre)
  • emit_event y notify_actor son idempotentes (desconocido)
  • db_session.commit() es atomico en todos los entornos de deploy
Mapa de Fronteras de Confianza
requested_by (JWT)
No confiable
String decodificado del JWT pero nunca vinculado a la operacion real. El sistema confia en from_actor_id en su lugar.
from_actor_id
Semi-confiable
Validado contra current_owner_id del lote β€” prueba de posesion logica, no de identidad del caller.
override_cert
No confiable
Parametro bool sin restriccion de rol. Cualquier actor autenticado puede pasar override_cert=True y eludir validacion de calidad.
emit_event
Caja Negra
Llamada externa post-commit. Fallo no revierte estado. Contrato de error desconocido desde este modulo.
notify_actor
Caja Negra
Llamada externa post-commit. Exception puede causar HTTP 500 con estado DB ya persistido β€” inconsistencia visible para el cliente.
db_session
Confiable
Sesion SQLAlchemy con lock pesimista. Unico componente con garantias transaccionales explicitas en el codigo.
Clusters de Fragilidad (guia para fase de vulnerabilidades)
Desvinculacion JWT ↔ Actor
95
override_cert sin control de rol
90
Side effects post-commit (F)
85
NoneType en to_actor (Bloque B)
72
Float split precision
60
Certificados no heredados en split
65
Audit gap: requested_by no en Transfer
40
Checklist de Completitud del Analisis
βœ“ Todas las secciones requeridas presentes (Purpose, Inputs, Outputs, Block-by-Block, Deps)
βœ“ 18 invariantes documentadas (min 3 por funcion)
βœ“ 23 suposiciones registradas (min 5)
βœ“ 7+ consideraciones de riesgo para interacciones externas
βœ“ Al menos 1 aplicacion de First Principles (Bloque B)
βœ“ 6+ aplicaciones de 5 Whys / 5 Hows
βœ“ Numeros de linea referenciados en codigo
βœ“ Sin items "unclear" sin resolver
βœ…
Contexto Completado β€” Listo para Fase de Vulnerabilidades
El analisis de construccion de contexto esta completo. El modelo global es estable. Los clusters de fragilidad priorizan donde iniciar la busqueda de vulnerabilidades: comenzar por la desvinculacion JWT/Actor y el override_cert sin restriccion de rol.