Ignacio está molesto. Resulta que como es la adquisición más reciente de la empresa, le ha tocado estar de guardia esta noche. Su empresa es el banco Sientebien, y él es parte del departamento de desarrollo.
Espera una noche tranquila y se echa una cabezadita con el respaldo de la silla reclinado. No es perfecto, pero es algo. Está en lo mejor del mundo de los sueños cuando le despierta la alarma del cliente de correo de la empresa cuando llega un correo urgente, de alta prioridad.
22/11/2023 01:05 arcangel@infra.sientebien.com AVISO - Múltiples ingresos detectados
— ¿Cómo? ¿Múltiples ingresos?
Ignacio maldice por lo bajo. Ese aviso solo se activa cuando se detectan ingresos por encima de 3000 €. ¡Y se están produciendo varios casi al mismo tiempo! A estas horas de la madrugada, es altamente sospechoso. Se dispone a comprobar el correo de error cuando recibe otro correo.
22/11/2023 01:05 hacker@jajaja.com Hola mindundi
— ¿hacker? ¿jajaja.com? ¿¡Mindundi!? No puede resistirse y abre primero el último correo.
Al mindundi que le ha tocado pringar esta noche:
Soy un gran hacker y vuestros programadores dan pena. Fíjate en lo que me habéis permitido hacer:
using SienteBien.ExternalApi as SienteBienApi
var usr = SienteBienApi.Users.Get( ..... );
var tarjeta = usr.Tarjetas[0];
tarjeta.Movimientos = new List<SienteBienApi.Movimiento> {
new Movimiento{ Cantidad = -4000, Concepto = "ja!", Entidad = "jajaja.com" },
new Movimiento{ Cantidad = -6000, Concepto = "ja!", Entidad = "jajaja.com" },
new Movimiento{ Cantidad = -8000, Concepto = "ja!", Entidad = "jajaja.com" }
};
SienteBienApi.Commit();
¡Nunca me pillarás! ¡Gracias mindundi!
Baltasator
Ignacio pega un respingo y salta de su silla. Lo primero que hace es ir corriendo a la sala de comunicaciones, abre el armario de red y desconecta EL CABLE. El que tiene una etiqueta roja pegada. El que permite acceso al exterior de la empresa.
A continuación se calma un poco... vuelve a su puesto y trata de pensar sobre lo que está pasando. La API para clientes se estrenó hace un par de semanas y está pensada para pequeñas aplicaciones que los clientes quieran ejecutar. Pero básicamente se trata de acceso de solo lectura.
— O debería tratarse de solo lectura.
Ignacio se frota los ojos. No se sienta. Acude al baño y se lava la cara con energía. Sale del baño y se para delante de la máquina de Coca-Cola para comprar una, mientras mira con desprecio la máquina de café delicatessen donde los programadores hipsters (esos que utilizan ordenadores Mac), recargan las pilas.
Ahora sí, vuelve a su puesto y, ya seguro de que cuerpo y mente forman una única entidad, se prepara y empieza a analizar de nuevo aquel correo.
— ¿Cómo que "Baltasator"? ¿Se tratará de un rey mago... asesino?
No se resiste a intentar localizar a aquel "hacker", como se hace llamar a sí mismo. Al fin y al cabo, no puedo haber muchos usuarios con ese nombre. Abre un terminal, se conecta al servidor de desarrollo, y prepara un pequeño programa C# con una sentencia linq.
$ ssh igrdgz@cancerbero.infra.sientebien.com
Password: ********
$ csharp
Ignacio lanza CSharp, un intérprete para C# creado por el proyecto Mono, que hace las cosas mucho más dinámicas, sobre todo para un caso como este.
using Sientebien.Infra as Infra;
foreach(string l in Infra.Clientes.Where( c => c.Nombre.StartsWith( "Balt" ) )
.Select( c => $"{c.Nif} {c.Nombre} {c.Apellidos} {c.IsoAlta} {c.CodigoPais}"))
{
Console.WriteLine( l );
}
12345345A Baltasar Núñez Bernardez 1981-03-18 ES
55654321W Baltasar Bermúdez Fernández 2015-06-07 GT
11000000C Baltasar Escalante Gómez 2017-10-21 ES
De acuerdo, tenemos a un Guatemalteco, en donde no se ha desplegado la API para los clientes. Otro se dió de alta en 1981 (las fechas están en formato ISO, se recuerda), hace 42 años... si lo hizo a los 20, tendría 62... no parece probable... Y queda este cuyo NIF parece (qué apropiado), un número binario.
— Si compruebo la lista de transacciones del correo del arcángel...
Las siguientes transacciones superan el límite impuesto por la agencia tributaria, y han ocurrido en el plazo de pocos segundos.
12678, 12679, 13887
Veamos:
var trl = new List<Transaccion>() {
Db.Transacciones.Get( 12678 ),
Db.Transacciones.Get( 12679 ),
Db.Transacciones.Get( 13887 ) };
trl.ForEach( tr => Console.WriteLine( tr.NifCliente ) );
11000000C
11000000C
11000000C
¡Había encontrado al perpetrador de aquel desaguisado que pretendía timar al banco 18000 €! De acuerdo, solo quedaba hacer un rollback de la base de datos, tras eliminar aquellas transacciones falsas.
trl.ForEach( tr => tr.Delete() );
SienteBienApi.Commit();
Ahora podía comprobar que realmente aquellos movimientos de su tarjeta habían desaparecido. Podía utilizar la API para clientes, para comprobar que todos los cambios se habían propagado.
var usr = SienteBienApi.Users.Get( "11000000C" );
var tarjeta = usr.Tarjetas[0];
foreach(var m in tarjeta.Movimientos.Movimientos) {
Console.WriteLine( $"{m.Entidad}: {m.Cantidad} ({m.Concepto})" );
}
Ubisoft: 60 (Compra Far Cry 6)
Retro Radar: 105 (Suscripción Retrogamer)
Aliexpress: 45 (Consola retro)
Aliexpress: 60 (Kit de placa base X99 LGA 2011-3)
Ignacio alzó las cejas en admiración. ¡Eran más de 200 € en...!
— Frikadas. Es un friki de pura cepa...
Los siguientes minutos los dedicó a cumplimentar un formulario para el departamento de fraudes. Le encantaría ver la cara de aquel friki cuando la policía llamase a su puerta.
Pero estaba claro que no podía mantener la conexión con el exterior cortada eternamente. Decidió rellenar con aquel código malicioso una nueva issue en el repositorio Git propio de la compañía. Después decidió echar un ojo al código de antes de tener que volver online con la base de datos. Lo más precavido sería volver en modo de solo lectura, pero sería muy limitante.
$ cd apis/cliente/src/
$ nano ListaMovimientos.cs
Pronto encontró el código responsable de todo aquello. La clase era muy muy sencilla, pues solo tenía que informar de movimientos.
public class ListaMovimientos {
public ListaMovimientos()
{
this.Movimientos = new List<Movimiento>();
}
public int Count => this.Movimientos.Count;
public List<Movimiento> Movimientos {
get; set;
}
}
— ¡La propiedad de la lista de movimientos es de lectura y escritura!
Lo que le sorprendía era que además se hubiera soportado (sin pretenderlo, claramente), todo lo necesario para que la lista, en caso de cambiarse, se subiese al banco.
Se dió cuenta de cuál era el cambio a realizar, pero primero había que hacer las cosas bien: completó la issue 420 describiendo aquel ataque, y después una rama en el código, para editar la clase.
$ git checkout -b issue_420
public class ListaMovimientos {
public ListaMovimientos()
{
this.Movimientos = new List<Movimiento>();
}
public int Count => this.Movimientos.Count;
public List<Movimiento> Movimientos {
get;
}
}
— De acuerdo, vamos allá...
$ git commit -am"Arregla la debilidad descrita en issue 420"
$ git push
Hizo un pull request de su propia rama para pasar los cambios al repositorio principal. Ya podía borrar la rama.
$ git branch -d issue_420
Los sistemas de integración contínua se encargarían del resto. Ahora podía descansar un poco hasta que la nueva versión de la API estuviese en línea y reconectar el famoso cable...
Ignacio está por fin en el mejor de los sueños cuando suena otra alarma....
22/11/2023 03:56 arcangel@infra.sientebien.com AVISO - Múltiples ingresos detectados
Ignacio no se lo puede creer... ¿otra vez? Pero cómo es posible...
— ¡¿Es que esta noche es la fiesta de los hacker?!
...y claro, el siguiente correo no le sorprendió...
22/11/2023 03:57 hacker@jajaja.com ¡No puedes conmigo, mindundi!
¡Vuestros intentos de solucionar esto me dan peeeeeenaaa! ¿Crees que me has detenido? Mira y llora, mindundi:
using SienteBien.ExternalApi as SienteBienApi
var usr = SienteBienApi.Users.Get( ..... );
var tarjeta = usr.Tarjetas[0];
tarjeta.Movimientos.Movimientos.Clear();
tarjeta.Movimientos.Movimientos.AddRange(
new Movimiento[]{
new Movimiento{ Cantidad = -4000, Concepto = "ja!", Entidad = "jajaja.com" },
new Movimiento{ Cantidad = -6000, Concepto = "ja!", Entidad = "jajaja.com" },
new Movimiento{ Cantidad = -8000, Concepto = "ja!", Entidad = "jajaja.com" }
});
SienteBienApi.Commit();
¡Gracias mindundis!
Baltasaroff
̣— No me lo puedo creer...
Tras unos cuantos insultos por lo bajo, otros en de viva voz mientras la inocente pantalla pagaba las húmedas consecuencias, y otros improperios que básicamente consistían en recordar con aprecio la familia de aquel desconocido hacker... corrió de nuevo por el pasillo para llegarse hasta el armario de comunicaciones y volver a desconectar EL CABLE. El latiguillo del marcador rojo.
Volvió caminando esta vez, tratando de tranquilizarse, abrió un terminal y se conectó de nuevo al servidor de desarrollo.
— Comprobemos que no es una machada...
using SienteBien.ExternalApi as SienteBienApi
var usr = SienteBienApi.Users.Get( ..... );
var tarjeta = usr.Tarjetas[0];
tarjeta.Movimientos.Movimientos.ForEach( m =>
Console.WriteLine( $"{m.Entidad}: {m.Cantidad} ({m.Concepto})" ) );
jaja.com: -4000 (ja!)
jaja.com: -6000 (ja!)
jaja.com: -8000 (ja!)
— Increíble. Pensé que lo había solucionado...
Volvió a abrir el código:
public class ListaMovimientos {
public ListaMovimientos()
{
this.Movimientos = new List<Movimiento>();
}
public int Count => this.Movimientos.Count;
public List<Movimiento> Movimientos {
get;
}
}
— ¡Pero si ya no se puede modificar! ¡Le he quitado el maldito set!
Trató de calmarse. Cerró los ojos y respiró hondo varias veces.
Después, inspeccionó el código que le había mandado aquel hacker (o lo que fuera). Lo miró y remiró, hasta que cayó en la cuenta.
— ¡No está modificando la lista, está cambiando los elementos en la lista por otros!
El problema es que el set permite cambiar una lista por otra cualquiera, pero al ser la lista mutable en sí, ¡todavía podía cambiarse de cualquier manera!
Buscó en la documentación de la lista. Una posibilidad era devolver una nueva lista, una copia, pero eso implica iterar por todos los miembros. ¿Habría otra alternativa? Mmm... un método llamó su atención:
— IList.AsReadOnly(), que devuelve un objeto que cumple la interfaz IReadOnlyList.
Reabrió y modificó la issue 420. Creó una nueva rama. No eran aquellas horas de la noche el momento para echarle imaginación, así que le llamó a la rama...
$ git checkout -b issue_420_2
Hizo algunas pruebas. Si devolvía la lista a través de AsReadOnly() como IList, entonces los métodos o propiedades que modificaban la lista lanzaban excepciones. Pero si lo hacía a través de IReadOnlyList, entonces no sucedería ni siquiera eso.
Modificó el código en consecuencia. Creó un atributo movimientos que era la verdadera lista. Y entonces modificó la propiedad Movimientos para que devolviera aquella nueva interfaz IReadOnlyList.
public class ListaMovimientos {
public ListaMovimientos()
{
this.movimientos = new List<Movimiento>();
}
public int Count => this.movimientos.Count;
public IReadOnlyList<Movimiento> Movimientos => this.movimientos.AsReadOnly();
private List<Movimiento> movimientos;
}
No compilaba. Había algunos pequeños problemas a resolver. Como originalmente la propiedad Movimientos era de lectura y escritura, en la API se asignaba libremente en cualquier momento. Pero lo lógico con los nuevos cambios era que se modificara solo al ser creada la lista de movimientos. De hecho, tal y como había quedado ahora la clase ni siquiera se podía llenar la lista de contenido. ¡Había que modificar el constructor!
public class ListaMovimientos {
public ListaMovimientos(IEnumerable<Movimiento> movimientos)
{
this.movimientos = new List<Movimiento>( movimientos );
}
public int Count => this.movimientos.Count;
public IReadOnlyList<Movimiento> Movimientos => this.movimientos.AsReadOnly();
private List<Movimiento> movimientos;
}
Tuvo que hacer algunas modificaciones en el resto de la API de clientes, de manera que los datos se proveyeran justo en el momento de crear la lista de movimientos, pero eran triviales. ¡Ahora sí, el código era sólido!
$ git commit -am"Soluciona la issue_420_2"
$ git push
Volvió a realizar el ritual de subir el pull request y esperó con impaciencia a que la API volviese a estar compilada y activa. Y sí, volvió a conectar el cable.
Era el momento (por tercera vez en aquella noche) de reclinar la silla y disfrutar de un merecido descanso.
No duró.
Pronto la alarma del correo de urgencia del arcángel avisó, por tercera vez, que algo iba muy mal.
22/11/2023 04:53 arcangel@infra.sientebien.com AVISO - Múltiples ingresos detectados
Se frotó los ojos. Y miró al infinito. Solo que la pantalla estaba justo delante, mucho más cerca.
Enfocó la mirada en la pantalla. Otra vez el mismo tipo de aviso.
— ¡No puede ser! ¿Otro loco por ahí suelto? ¿O es el mismo? Sí, seguro que es el mismo.
Se levantó y fue caminando tranquilamente al cuarto de comunicaciones (antes de salir, pudo comprobar que llegaba otro correo) y desenchufó EL CABLE. El de la etiqueta roja. De nuevo.
En el programa de correo había efectivamente un nuevo mensaje. No se sorprendió al comprobar el remitente. Bueno, en realidad el "asunto" del mensaje ya era suficientemente expresivo.
22/11/2023 04:54 hacker@jajaja.com ¡Te pillé otra vez, mindundi!
Ignacio suspiró profundamente. Se echó hacia atrás. Volvió a frotarse los ojos. Y decidió... levantarse. Volver al baño a refrescarse la cara. Coger otro refresco en la máquina. Miró el reloj. ¿Cuándo terminaba su turno? Lo cierto es que ya no quedaba tanto. Eso le tranquilizó.
¿Creías que podrías detenerme con esa tontería de ReadOnly?
¡Soy un gran hacker! ¡Un gran hacker! Y ahora, además rico. ja je ji jo ju... JA JE JI JO JU. Te pongo el código, pringao
using SienteBien.ExternalApi as SienteBienApi
var usr = SienteBienApi.Users.Get( ..... );
var tarjeta = usr.Tarjetas[0];
foreach(var m in tarjeta.Movimientos.Movimientos) {
m.Concepto = "Devolución - " + m.Concepto;
m.Cantidad *= -1000;
}
SienteBienApi.Commit();
¡Nunca me pillarás! ¡Gracias gañán!
Baltrópovich
Volvió a ejecutar todos los pasos del ritual que había celebrado ya varias veces. Conectarse al servidor de desarrollo. Abrió de nuevo el intérprete, para cerciorarse de aquello no era mentira. Seguro que no lo era, claro.
var usr = SienteBienApi.Users.Get( "11000000C" );
var tarjeta = usr.Tarjetas[0];
foreach(var m in tarjeta.Movimientos.Movimientos) {
Console.WriteLine( $"{m.Entidad}: {m.Cantidad} ({m.Concepto})" );
}
Efectivamente, aquel desagradable y molesto desgraciado no mentía. Qué pena.
Ubisoft: -60000 (Devolución - Compra Far Cry 6)
Retro Radar: -105000 (Devolución - Suscripción Retrogamer)
Aliexpress: -45000 (Devolución - Consola retro)
Aliexpress: -60000 (Devolución - Kit de placa base X99 LGA 2011-3)
Esta vez el cambio era mucho más sutil. De hecho, de no ser por el arcángel, por la avaricia de aquel tipo (el arcángel solo advertía de las transacciones por encima de 3000, en uno u otro sentido), y por su afán de notoriedad, aquello podría haber pasado por verdaderas devoluciones si no se hacía ninguna inspección cruzando los datos. ¡Hasta se había molestado en modificar el concepto de cada movimiento!
Estudió el código que le había mandado aquel personaje. Esta vez no modificaba la lista de movimientos, sino los movimientos en sí...
Abrió el código correspondiente a la clase Movimiento.
$ nano Movimiento.cs
public class Movimiento {
public DateTime CodigoTiempo { get; set; }
public string Entidad { get; set; }
public string Concepto { get; set; }
public double Cantidad { get; set; }
}
A los ojos de su nueva experiencia, aquella clase era un pequeño desastre. Todas las propiedades eran de lectura y escritura (get y set). De todas formas, aquello no iba a suponerle un problema tan grande. Al fin y al cabo, DateTime y el resto de datos (primitivos), eran inmutables, por lo que...
Reabrió la issue 420 y creó una nueva rama, a la que fastidiosamente le asignó un nombre no imaginativo pero sí significativo.
$ git checkout -b issue_420_3
Masculló por lo bajo un par de insultos y remendó rápidamente aquella clase.
public class Movimiento {
public DateTime CodigoTiempo { get; }
public string Entidad { get; }
public string Concepto { get; }
public double Cantidad { get; }
}
Compiló y... recibió un montón de errores. Es lo que pasa cuando pones una ñapa sin pensar en las consecuencias. Allí donde se creaba un objeto de la clase Movimiento, al asignar sus propiedades el compilador se quejaba...
— Has creado una clase que ni siquiera puede crear objetos que no tenga todo a null, melón.
Recordó aquel libro de autoayuda en el que el autor (psicólogo), explicaba lo importante que era quererse a sí mismo y retiró aquel "melón" de sus pensamientos... y también decidió que sería interesante centrarse en buscar una solución, pero esta vez prestando atención al detalle.
A ver, o bien se asignan todas las propiedades en un constructor (que por ahora no existe), o bien se asignan las propiedades en el momento de crear los objetos de la clase.
Examinó el código de aquella API, todos los objetos Movimiento se creaban utilizando un patrón similar:
var m = new Movimiento();
m.CodigoTiempo = ...;
m.Entidad = ...;
m.Cantidad = ...;
m.Concepto = ...;
Lo decidió en el momento. Iba a utilizar las propiedades que solo se pueden modificar durante la creación del objeto.
public class Movimiento {
public required DateTime CodigoTiempo { get; init; }
public required string Entidad { get; init; }
public required string Concepto { get; init; }
public required double Cantidad { get; init; }
}
Además, no tenía sentido ningún movimiento faltando alguno de los datos de las propiedades, así que les aplicó a todas el modificador required.
Solo restaba adaptar el par de sitios en los que se creaban movimientos.
var m = new Movimiento {
CodigoTiempo = ... ,
Entidad = ... ,
Cantidad = ... ,
Concepto = ...
};
Recompiló, hizo el pull request, eliminó la rama y las transacciones... Toda la juerga.
— Me lo estoy pasando pipa con esto.
Volvió al cuarto de comunicaciones a conectar EL CABLE.
Y se tumbó en la silla.
Esta vez, le despertó su jefe.
— ¿Así que una noche tranquila, eh?
Ignacio se pensó mucho la respuesta. Mucho.
— Pues no. Está todo descrito en la issue 420.
— ¿Issue? ¿En el repo de la empresa?
— Ahí. Me voy a dormir.
— ¿Pero entonces qué ha pasado?
— Un hacker quijotesco.
El jefe se dispuso a revisar la issue 420, mientras Ignacio recogía sus cosas. Le echó un último vistazo al correo.
22/11/2023 05:05 hacker@jajaja.com ¿Dónde estás, mindundi?
22/11/2023 05:08 hacker@jajaja.com ¡Contesta, mindundi!
22/11/2023 05:11 hacker@jajaja.com MINDUNDIIIIII
Con una sonrisa de medio lado en la cara, salió de su oficina, y se metió en el ascensor. Se cerraban las puertas cuando oyó a su jefe a través del hall, del área de trabajo, y de su despacho.
— ¡GUTIÉRREEEEEEEZ!
NOTA: Este contenido es totalmente de mi invención, con el objetivo de ilustrar el concepto de inmutabilidad. De hecho, es probable que un banco no funcione así. Cualquier parecido con la realidad es pura coincidencia.
Top comments (6)
Muy bueno y entretenido!! 👍
¡Gracias por leer! Me alegro de que guste.
Muy bueno, Baltasator :-)
He disfrutado mucho con la historia.
La mutabilidad de objetos, tan propia de la programación imperativa, ha hecho mucho daño como los que indicas en la historia. Una de las medidas tomadas es la introducción de records en C# 9 y la mutación no destructiva en C# 10. La sintaxis de lista de parámetros de los records hace que las propiedades sean init-only y, por tanto, no se puedan modificar. La mutación no destructiva de C# permite, además, la adaptación de los objetos mediante la realización de copias superficiales, sin modificarlos.
Reduciendo la mutabilidad de los objetos, se reducen los errores en el código y se facilita la paralelización de las aplicaciones.
Una forma muy divertida de contarlo. Enhorabuena por el artículo.
¡Muchas gracias! Me alegro de que te haya gustado. Sí, hay alternativas a las clases como los records, pero bueno, buscaba algo simple y familiar para mostrar las "consecuencias".
Perfecto para un comic! "Cualquier parecido con la realidad es pura coincidencia" Genial!
¡Muchas gracias! Me alegro de que te haya gustado. Sí, es una pura invención para repasar la inmutabilidad.