La lunghezza di una password non ne implica sempre la sicurezza , la password "Passw0rd!" è composta da 9 caratteri di cui uno maiuscolo ,un numero ed un carattere speciale quindi formalmente è una password forte, in realtà non è così nella pratica se le credenziali sono sottoposte ad un attacco a dizionario usando le password più comuni ( https://it.wikipedia.org/wiki/Attacco_a_dizionario ) . Probabilmente un dizionario usato nell'attacco potrebbe contenere anche la password di cui sopra perchè è molto comune , per mitigare questo tipo di problemi possiamo utilizzare delle liste che contengono le password più utilizzate per verificare in fase di registrazione dell'utente che la password scelta dall'utente non sia tra queste . A quest'indirizzo https://github.com/danielmiessler/SecLists/tree/master/Passwords/Common-Credentials potete trovare delle liste di password comuni (1000000 di password e c'è anche 'Passw0rd!' , :-) ) .
Ovviamente un attacco dizionario non è comunque facile da portare a segno se abbiamo configurato la nostra applicazione adeguatamente, ad esempio in asp.net core possiamo settare così la nostra applicazione :
builder.Services.AddDefaultIdentity<IdentityUser>
(options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
options.Lockout.MaxFailedAccessAttempts = 3;
options.Lockout.AllowedForNewUsers = true;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
});
impostare il lockout con la valorizzazione dei tentativi falliti dopo i quali l'account è locked ed il periodo per cui esso sarà lockout è quindi relativamente semplice .
Per verificare se la password inserita dall'utente appartiene a quelle da ritenere troppo comuni per prima cosa carichiamo le "password comuni" in una tabella 'Passwords' creata nel db "GestioneUtenti" ,un pò di "pseudocodice" :
private void button1_Click(object sender, EventArgs e)
{
IEnumerable<string> lines = File.ReadLines(@"C:\Users\gianm\Desktop\10-million-password-list-top-1000000.txt");
long l = lines.LongCount<string>(); //999998
SqlConnection conn = new SqlConnection("Data Source=(localdb)\\mssqllocaldb;Initial Catalog=GestioneUtenti;Integrated Security=True");
conn.Open();
foreach (string line in lines) {
SqlParameter par = new SqlParameter("@password", System.Data.SqlDbType.Text);
par.Value = line;
SqlCommand cmd = new SqlCommand("insert into Passwords(password) values (" + par + ")",conn);
cmd.Parameters.Add(par);
cmd.ExecuteNonQuery();
par = null;
cmd = null;
}
conn.Close();
}
il codice impiegherà da qualche decina di secondi ad alcuni minuti per caricare le passwords nella tabella 'Passwords'
segue un metodo per verificare se la password inserita dall'utente esiste nella tabella Passwords:
private bool CheckPasswordExists(string s)
{
bool b = false;
SqlConnection conn = new SqlConnection("Data Source=(localdb)\\mssqllocaldb;Initial Catalog=GestioneUtenti;Integrated Security=True");
conn.Open();
SqlParameter par = new SqlParameter("@password", System.Data.SqlDbType.Text);
par.Value = s;
SqlCommand cmd = new SqlCommand();
cmd.Parameters.Add(par);
cmd.Connection = conn;
cmd.CommandType = System.Data.CommandType.Text;
cmd.CommandText = "select * from Passwords where Password like " + par;
SqlDataReader reader = cmd.ExecuteReader();
bool read = reader.Read();
b = read;
reader.Close();
conn.Close();
return b;
}
se il metodo torna true significa che la password esiste nella lista delle password comuni e quindi bisogna indicare all'utente di scegliere un' altra password ,
se il metodo torna false la password non è tra quelle della nostra lista e possiamo lasciar procedere l'utente nella registrazione .
Ora vedremo come utilizzare ulteriori funzionalità proprie di Asp .Net Core per verificare che la password immessa dall'utente non sia troppo comune :
nel file Program.cs ,di un progetto AspNetCore, possiamo aggiungere alla configurazione un AddPasswordValidator con cui costruire una nostra logica di validazione della password ( https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addpasswordvalidator?view=aspnetcore-7.0 ) :
builder.Services.AddDefaultIdentity<IdentityUser>
(options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
options.Lockout.MaxFailedAccessAttempts = 3;
options.Lockout.AllowedForNewUsers = true;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
}) .AddPasswordValidator<TestPasswordValidator<IdentityUser>>()
.AddEntityFrameworkStores<AppDbContext>();
creiamo una classe TestPasswordValidator :
public class TestPasswordValidator<TUser> : IPasswordValidator<TUser> where TUser : class
{
public TestPasswordValidator()
{
}
public Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user, string password)
{
SearchIntoDb s = new SearchIntoDb();
bool res = s.CheckPasswordExists(password);
IdentityResult result;
if (res)
{
result = IdentityResult.Failed(new IdentityError { Description = "Password troppo facile da indovinare" });
} else {
result = IdentityResult.Success;
}
s = null;
return Task.FromResult(result);
}
}
Il metodo ValidateAsync utilizza una classe SearchIntoDb ed un metodo CheckPasswordExists (già riportato sopra ) per effettuare la select nel db al fine di verificare se la password appartiene alla tabella Passwords oppure no.
public class SearchIntoDb
{
private string connectionString = null;
public SearchIntoDb()
{
var config = new ConfigurationBuilder()
.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
.AddJsonFile("appsettings.json").Build();
string t=config.GetSection("ConnectionStrings").GetSection("UtentiConnection").Value;
connectionString = t;
}
public bool CheckPasswordExists(string password)
{
bool t = false;
SqlConnection conn = new SqlConnection(connectionString);
conn.Open();
SqlParameter par = new SqlParameter("@password", System.Data.SqlDbType.Text);
par.Value = password;
SqlCommand cmd = new SqlCommand();
cmd.Parameters.Add(par);
cmd.Connection = conn;
cmd.CommandType = System.Data.CommandType.Text;
cmd.CommandText = "select * from Passwords where Password like " + par;
SqlDataReader reader = cmd.ExecuteReader();
bool b = reader.Read();
t = b;
reader.Close();
conn.Close();
return t;
}
}
proviamo il codice in un applicazione asp .net core , inseriamo la famigerata password "Passw0rd!"
ed otteniamo quanto ci aspettavamo :
Il post ha solo scopo di esempio e la lista di password utilizzata non rappresenta un elenco esaustivo delle password "comuni" .