[php] "Keep Me Logged In" - l'approccio migliore


Answers

OK, lasciatemi dire chiaramente: se stai mettendo i dati degli utenti, o qualsiasi cosa derivata dai dati dell'utente in un cookie per questo scopo, stai facendo qualcosa di sbagliato.

Là. L'ho detto. Ora possiamo passare alla risposta reale.

Cosa c'è che non va con i dati dell'utente hashing, chiedi? Bene, si riduce alla superficie di esposizione e alla sicurezza attraverso l'oscurità.

Immagina per un secondo che sei un attaccante. Vedete un cookie crittografico impostato per il remember-me sulla vostra sessione. È largo 32 caratteri. Gee. Potrebbe essere un MD5 ...

Immaginiamo anche per un secondo che conoscano l'algoritmo che hai usato. Per esempio:

md5(salt+username+ip+salt)

Ora, tutto ciò che un aggressore deve fare è la forza bruta del "sale" (che in realtà non è un sale, ma ne riparleremo più avanti), e ora può generare tutti i token falsi che vuole con qualsiasi nome utente per il suo indirizzo IP! Ma forzare brutemente un sale è difficile, giusto? Assolutamente. Ma le GPU dei nostri giorni sono estremamente buone. E a meno che tu non usi abbastanza casualità (rendilo abbastanza grande), cadrà rapidamente, e con esso le chiavi del tuo castello.

In breve, l'unica cosa che ti protegge è il sale, che in realtà non ti protegge tanto quanto pensi.

Ma aspetta!

Tutto ciò era previsto che l'hacker conoscesse l'algoritmo! Se è segreto e confuso, allora sei al sicuro, giusto? SBAGLIATO . Questa linea di pensiero ha un nome: Security Through Obscurity , che non dovrebbe mai essere invocato.

Il modo migliore

Il modo migliore è non lasciare mai che le informazioni dell'utente lascino il server, ad eccezione dell'ID.

Quando l'utente esegue il login, genera un token casuale di grandi dimensioni (da 128 a 256 bit). Aggiungilo a una tabella di database che associa il token all'ID utente e quindi inviarlo al client nel cookie.

Cosa succede se l'utente malintenzionato indovina il token casuale di un altro utente?

Bene, facciamo un po 'di matematica qui. Stiamo generando un token casuale a 128 bit. Ciò significa che ci sono:

possibilities = 2^128
possibilities = 3.4 * 10^38

Ora, per mostrare quanto sia assurdamente grande quel numero, immaginiamo ogni server su internet (diciamo 50.000.000 oggi) che cerca di forzare tale numero ad una velocità di 1.000.000.000 al secondo ciascuno. In realtà i tuoi server si fonderebbero con tale carico, ma proviamo a farlo.

guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000

Quindi 50 quadrilioni di ipotesi al secondo. È veloce! Destra?

time_to_guess = possibilities / guesses_per_second
time_to_guess = 3.4e38 / 50,000,000,000,000,000
time_to_guess = 6,800,000,000,000,000,000,000

Quindi 6,8 sestilioni di secondi ...

Proviamo a ridurlo a numeri più amichevoli.

215,626,585,489,599 years

O ancora meglio:

47917 times the age of the universe

Sì, sono 47917 volte l'età dell'universo ...

Fondamentalmente, non sarà incrinato.

Quindi riassumendo:

L'approccio migliore che consiglio è quello di memorizzare il cookie con tre parti.

function onLogin($user) {
    $token = GenerateRandomToken(); // generate a token, should be 128 - 256 bit
    storeTokenForUser($user, $token);
    $cookie = $user . ':' . $token;
    $mac = hash_hmac('sha256', $cookie, SECRET_KEY);
    $cookie .= ':' . $mac;
    setcookie('rememberme', $cookie);
}

Quindi, per convalidare:

function rememberMe() {
    $cookie = isset($_COOKIE['rememberme']) ? $_COOKIE['rememberme'] : '';
    if ($cookie) {
        list ($user, $token, $mac) = explode(':', $cookie);
        if (!hash_equals(hash_hmac('sha256', $user . ':' . $token, SECRET_KEY), $mac)) {
            return false;
        }
        $usertoken = fetchTokenByUserName($user);
        if (hash_equals($usertoken, $token)) {
            logUserIn($user);
        }
    }
}

Nota: non utilizzare il token o la combinazione di utente e token per cercare un record nel database. Assicurati sempre di recuperare un record in base all'utente e utilizzare una funzione di confronto sicura per il tempo per confrontare il token recuperato in seguito. Ulteriori informazioni sugli attacchi temporali .

Ora, è molto importante che SECRET_KEY sia un segreto crittografico (generato da qualcosa come /dev/urandom e / o derivato da un input ad alta entropia). Inoltre, GenerateRandomToken() deve essere una fonte casuale forte ( mt_rand() non è abbastanza forte. Usa una libreria, come RandomLib o random_compat , o mcrypt_create_iv() con DEV_URANDOM ) ...

hash_equals() è per prevenire attacchi di temporizzazione . Se usi una versione PHP sotto PHP 5.6 la funzione hash_equals() non è supportata. In questo caso è possibile sostituire hash_equals() con la funzione timingSafeCompare:

/**
 * A timing safe equals comparison
 *
 * To prevent leaking length information, it is important
 * that user input is always used as the second parameter.
 *
 * @param string $safe The internal (safe) value to be checked
 * @param string $user The user submitted (unsafe) value
 *
 * @return boolean True if the two strings are identical.
 */
function timingSafeCompare($safe, $user) {
    if (function_exists('hash_equals')) {
        return hash_equals($safe, $user); // PHP 5.6
    }
    // Prevent issues if string length is 0
    $safe .= chr(0);
    $user .= chr(0);

    // mbstring.func_overload can make strlen() return invalid numbers
    // when operating on raw binary strings; force an 8bit charset here:
    if (function_exists('mb_strlen')) {
        $safeLen = mb_strlen($safe, '8bit');
        $userLen = mb_strlen($user, '8bit');
    } else {
        $safeLen = strlen($safe);
        $userLen = strlen($user);
    }

    // Set the result to the difference between the lengths
    $result = $safeLen - $userLen;

    // Note that we ALWAYS iterate over the user-supplied length
    // This is to prevent leaking length information
    for ($i = 0; $i < $userLen; $i++) {
        // Using % here is a trick to prevent notices
        // It's safe, since if the lengths are different
        // $result is already non-0
        $result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i]));
    }

    // They are only identical strings if $result is exactly 0...
    return $result === 0;
}
Question

La mia applicazione web utilizza le sessioni per memorizzare le informazioni sull'utente dopo aver effettuato l'accesso e per mantenere tali informazioni mentre viaggiano da una pagina all'altra all'interno dell'app. In questa specifica applicazione, sto memorizzando user_id , first_name e last_name della persona.

Mi piacerebbe offrire l'opzione "Keep Me Logged" al log in che metterà un cookie sul computer dell'utente per due settimane, che riavvierà la sessione con gli stessi dettagli quando torneranno all'app.

Qual è l'approccio migliore per farlo? Non voglio memorizzare i loro user_id nel cookie, in quanto sembra che renderebbe facile per un utente provare e falsificare l'identità di un altro utente.




Implementing a "Keep Me Logged In" feature means you need to define exactly what that will mean to the user. In the simplest case, I would use that to mean the session has a much longer timeout: 2 days (say) instead of 2 hours. To do that, you will need your own session storage, probably in a database, so you can set custom expiry times for the session data. Then you need to make sure you set a cookie that will stick around for a few days (or longer), rather than expire when they close the browser.

I can hear you asking "why 2 days? why not 2 weeks?". This is because using a session in PHP will automatically push the expiry back. This is because a session's expiry in PHP is actually an idle timeout.

Now, having said that, I'd probably implement a harder timeout value that I store in the session itself, and out at 2 weeks or so, and add code to see that and to forcibly invalidate the session. Or at least to log them out. This will mean that the user will be asked to login periodically. Yahoo! does this.










I don't understand the concept of storing encrypted stuff in a cookie when it is the encrypted version of it that you need to do your hacking. If I'm missing something, please comment.

I am thinking about taking this approach to 'Remember Me'. If you can see any issues, please comment.

  1. Create a table to store "Remember Me" data in - separate to the user table so that I can log in from multiple devices.

  2. On successful login (with Remember Me ticked):

    a) Generate a unique random string to be used as the UserID on this machine: bigUserID

    b) Generate a unique random string: bigKey

    c) Store a cookie: bigUserID:bigKey

    d) In the "Remember Me" table, add a record with: UserID, IP Address, bigUserID, bigKey

  3. If trying to access something that requires login:

    a) Check for the cookie and search for bigUserID & bigKey with a matching IP address

    b) If you find it, Log the person in but set a flag in the user table "soft login" so that for any dangerous operations, you can prompt for a full login.

  4. On logout, Mark all the "Remember Me" records for that user as expired.

The only vulnerabilities that I can see is;

  • you could get hold of someone's laptop and spoof their IP address with the cookie.
  • you could spoof a different IP address each time and guess the whole thing - but with two big string to match, that would be...doing a similar calculation to above...I have no idea...huge odds?



Vecchio thread, ma ancora una preoccupazione valida. Ho notato alcune buone risposte in merito alla sicurezza ed evito l'uso della "sicurezza attraverso l'oscurità", ma i veri metodi tecnici forniti non erano sufficienti ai miei occhi. Cose che devo dire prima di contribuire con il mio metodo:

  • NON memorizzare MAI una password in chiaro ... MAI!
  • Non memorizzare MAI la password di hash di un utente in più di una posizione nel database. Il back-end del server è sempre in grado di estrarre la password con hash dalla tabella degli utenti. Non è più efficiente archiviare dati ridondanti al posto di transazioni DB aggiuntive, l'inverso è vero.
  • I tuoi ID di sessione dovrebbero essere univoci, quindi nessun utente potrà mai condividere un ID, quindi lo scopo di un ID (il numero di ID della patente di guida potrebbe mai corrispondere a un altro? No.) Questo genera una combinazione unica di due pezzi, basata su 2 stringhe uniche. La tua tabella Sessioni deve utilizzare l'ID come PK. Per consentire a più dispositivi di essere considerati attendibili per l'autosegnalizzazione, utilizzare un'altra tabella per i dispositivi attendibili che contiene l'elenco di tutti i dispositivi convalidati (vedere il mio esempio di seguito) e viene mappato utilizzando il nome utente.
  • Non serve a scomporre dati noti in un cookie, il cookie può essere copiato. Quello che stiamo cercando è un dispositivo utente conforme per fornire informazioni autentiche che non possono essere ottenute senza che un utente malintenzionato comprometta la macchina dell'utente (di nuovo, vedere il mio esempio). Ciò significherebbe, tuttavia, che un utente legittimo che vieta le informazioni statiche della sua macchina (ad es. Indirizzo MAC, nome host del dispositivo, useragent se limitato dal browser, ecc.) Dal coerente rimanente (o lo contraffà in primo luogo) non sarà in grado di usa questa funzione. Ma se questa è una preoccupazione, considera il fatto che stai offrendo auto-accesso agli utenti che si identificano in modo univoco , quindi se si rifiutano di essere conosciuti spoofingando il loro MAC, spoofing loro useragent, spoofing / cambiando il loro nome host, nascondendosi dietro proxy, ecc., quindi non sono identificabili e non dovrebbero mai essere autenticati per un servizio automatico. Se si desidera, è necessario esaminare l'accesso alla smart card in bundle con il software client che stabilisce l'identità per il dispositivo utilizzato.

Detto questo, ci sono due ottimi modi per avere l'auto-accesso sul tuo sistema.

In primo luogo, il modo economico e semplice che mette tutto su qualcun altro. Se effettui il login al tuo sito con, ad esempio, il tuo account Google +, probabilmente hai un pulsante google + semplificato che accederà all'utente se è già stato eseguito l'accesso a Google (l'ho fatto per rispondere a questa domanda, poiché sono sempre firmato su google). Se si desidera che l'utente esegua l'accesso automatico se ha già effettuato l'accesso con un autenticatore attendibile e supportato e ha selezionato la casella per farlo, gli script sul lato client eseguono il codice dietro il corrispondente pulsante "accesso con" prima di caricare , assicurati semplicemente che il server memorizzi un ID univoco in una tabella di accesso automatico che abbia il nome utente, l'ID di sessione e l'autenticatore utilizzati per l'utente. Poiché questi metodi di accesso utilizzano AJAX, si sta comunque aspettando una risposta e tale risposta è una risposta convalidata o un rifiuto. Se ottieni una risposta convalidata, usala normalmente, quindi continua a caricare l'utente che ha effettuato l'accesso normalmente. In caso contrario, l'accesso non è riuscito, ma non dire all'utente, continua semplicemente come non connesso, noteranno. Questo per impedire a un utente malintenzionato che ha rubato i cookie (o li ha forgiati nel tentativo di aumentare i privilegi) dall'apprendimento che l'utente ha effettuato l'auto-firma nel sito.

Questo è economico, e potrebbe anche essere considerato sporco da alcuni perché cerca di convalidare il tuo potenziale già connesso con siti come Google e Facebook, senza nemmeno dirtelo. Tuttavia, non dovrebbe essere utilizzato su utenti che non hanno richiesto l'auto-accesso al tuo sito e questo particolare metodo è solo per l'autenticazione esterna, come con Google+ o FB.

Poiché un autenticatore esterno è stato utilizzato per dire al server dietro le quinte se un utente è stato convalidato o meno, un utente malintenzionato non può ottenere altro che un ID univoco, che è inutile da solo. Elaborerò:

  • Utente 'joe' visita il sito per la prima volta, ID sessione inserito nella 'sessione' del cookie.
  • Utente 'joe' Entra, aumenta i privilegi, ottiene il nuovo ID sessione e rinnova la 'sessione' del cookie.
  • L'utente "joe" sceglie di autosignare utilizzando google +, ottiene un ID univoco inserito nel cookie "keepmesignedin".
  • L'utente 'joe' ha Google per mantenerli registrati, consentendo al tuo sito di accedere automaticamente all'utente tramite google nel tuo back-end.
  • L'attaccante cerca sistematicamente ID univoci per "keepmesignedin" (è una conoscenza pubblica distribuita a tutti gli utenti) e non è collegato da nessun'altra parte; prova ID univoco assegnato a "joe".
  • Il server riceve ID univoco per "joe", estrae la corrispondenza nel DB per un account google +.
  • Il server invia l'attaccante alla pagina di accesso che esegue una richiesta AJAX a google per accedere.
  • Il server di Google riceve la richiesta, utilizza la sua API per vedere che Attacker non ha effettuato l'accesso al momento.
  • Google invia una risposta che non esiste un utente attualmente connesso con questa connessione.
  • La pagina di Attacker riceve risposta, lo script reindirizza automaticamente alla pagina di accesso con un valore POST codificato nell'URL.
  • La pagina di login ottiene il valore POST, invia il cookie per "keepmesignedin" a un valore vuoto e una data valida fino a 1-1-1970 per impedire un tentativo automatico, facendo in modo che il browser di Attacker semplicemente elimini il cookie.
  • All'attacker viene data la normale pagina di accesso per la prima volta.

Indipendentemente da cosa, anche se un utente malintenzionato utilizza un ID che non esiste, il tentativo dovrebbe fallire su tutti i tentativi tranne quando viene ricevuta una risposta convalidata.

Questo metodo può e deve essere utilizzato insieme all'autenticatore interno per coloro che accedono al tuo sito utilizzando un autenticatore esterno.

=========

Ora, per il tuo sistema di autenticazione personale che può auto-firmare gli utenti, questo è il modo in cui lo faccio:

DB ha alcune tabelle:

TABLE users:
UID - auto increment, PK
username - varchar(255), unique, indexed, NOT NULL
password_hash - varchar(255), NOT NULL
...

Si noti che il nome utente può contenere fino a 255 caratteri. Il mio programma server limita i nomi utente nel mio sistema a 32 caratteri, ma gli autenticatori esterni potrebbero avere nomi utente con il loro dominio @ domain.tld più grandi di così, quindi supporto solo la lunghezza massima di un indirizzo email per la massima compatibilità.

TABLE sessions:
session_id - varchar(?), PK
session_token - varchar(?), NOT NULL
session_data - MediumText, NOT NULL

Si noti che non vi è alcun campo utente in questa tabella, perché il nome utente, quando connesso, si trova nei dati della sessione e il programma non consente dati nulli. Il session_id e il session_token possono essere generati usando hash md5 casuali, hash sha1 / 128/256, timbri datetime con stringhe casuali aggiunte a loro e poi hash, o qualunque cosa desideri, ma l'entropia del tuo output dovrebbe rimanere il più alto tollerabile a attenuare gli attacchi di forza bruta persino dal decollo e tutti gli hash generati dalla classe di sessione devono essere controllati per le corrispondenze nella tabella delle sessioni prima di tentare di aggiungerli.

TABLE autologin:
UID - auto increment, PK
username - varchar(255), NOT NULL, allow duplicates
hostname - varchar(255), NOT NULL, allow duplicates
mac_address - char(23), NOT NULL, unique
token - varchar(?), NOT NULL, allow duplicates
expires - datetime code

Gli indirizzi MAC per loro natura dovrebbero essere UNICI, quindi ha senso che ogni voce abbia un valore unico. Gli hostname, d'altra parte, potrebbero essere duplicati su reti separate in modo legittimo. Quante persone usano "Home PC" come uno dei loro nomi di computer? Il nome utente è preso dai dati di sessione dal back-end del server, quindi manipolarlo è impossibile. Per quanto riguarda il token, lo stesso metodo per generare token di sessione per le pagine dovrebbe essere utilizzato per generare token nei cookie per l'auto-sign-in dell'utente. Infine, il codice datetime viene aggiunto per quando l'utente deve riconvalidare le proprie credenziali. Aggiorna questo datetime sul login utente mantenendolo entro pochi giorni, o costringilo a scadere indipendentemente dall'ultimo accesso conservandolo solo per un mese o giù di lì, a seconda del tuo progetto.

Ciò impedisce a qualcuno di spoofing sistematicamente il MAC e il nome host per un utente che conosce l'accesso automatico. NON è MAI permesso all'utente di conservare un cookie con la propria password, testo non chiaro o altro. Avere il token rigenerato su ogni pagina di navigazione, proprio come si farebbe con il token di sessione. Questo riduce enormemente la probabilità che un utente malintenzionato possa ottenere un cookie token valido e utilizzarlo per accedere. Alcune persone cercheranno di dire che un utente malintenzionato potrebbe rubare i cookie dalla vittima e fare un attacco di ripetizione della sessione per accedere. Se un utente malintenzionato potrebbe rubare i cookie (il che è possibile), avrebbe sicuramente compromesso l'intero dispositivo, il che significa che potevano semplicemente utilizzare il dispositivo per accedere comunque, il che vanifica lo scopo di rubare interamente i cookie. Finché il tuo sito funziona su HTTPS (cosa che dovrebbe fare quando si ha a che fare con password, numeri CC o altri sistemi di accesso), hai fornito tutta la protezione all'utente che puoi all'interno di un browser.

Una cosa da tenere a mente: i dati della sessione non dovrebbero scadere se si utilizza l'auto-signin. È possibile scadere la possibilità di continuare la sessione in modo errato, ma la convalida nel sistema dovrebbe riprendere i dati della sessione se si tratta di dati permanenti che si prevede continuino tra una sessione e l'altra. Se si desidera sia dati di sessione persistenti che non persistenti, utilizzare un'altra tabella per i dati di sessione persistenti con il nome utente come PK e richiedere al server di recuperarli come se fossero i normali dati di sessione, utilizzare solo un'altra variabile.

Una volta raggiunto il login in questo modo, il server dovrebbe comunque convalidare la sessione. Qui è dove è possibile codificare le aspettative per i sistemi rubati o compromessi; i modelli e gli altri risultati attesi degli accessi ai dati di sessione possono spesso portare a conclusioni in merito al dirottamento di un sistema o alla creazione di cookie per ottenere l'accesso. È qui che la tua ISS Tech può inserire regole che attivano il blocco dell'account o la rimozione automatica di un utente dal sistema di auto-accesso, tenendo gli aggressori fuori abbastanza a lungo da consentire all'utente di determinare in che modo l'attaccante ha avuto successo e come eliminarlo.

Come nota conclusiva, assicurarsi che qualsiasi tentativo di ripristino, modifica della password o errori di accesso oltre la soglia determinino la disabilitazione dell'autosegnalazione fino a quando l'utente non convalida correttamente e riconosce che ciò si è verificato.

Mi scuso se qualcuno si aspettava che il codice venga distribuito nella mia risposta, non succederà qui. Devo dire che uso PHP, jQuery e AJAX per eseguire i miei siti e NON uso MAI Windows come server ... mai.






Related