How can I improve the security of my system to keep the user connected?

Asked

Viewed 127 times

0

Starting from the idea that the user entered the login and password correctly on the login page, I will save a cookie to authenticate the user so that the login remains active and the user can connect to other devices without being logged in in the previous session.

For this I started creating a table in the database with the following features:

CREATE TABLE `manter_login` (
  `id` int(11) NOT NULL,
  `id_user` int(11) NOT NULL,
  `login_history` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`login_history`))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

ALTER TABLE `manter_login`
  ADD PRIMARY KEY (`id`),
  ADD KEY `id_user` (`id_user`);

ALTER TABLE `manter_login`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
COMMIT;

And creating the following functions:

define("USER2", 'root');
define("PASS2", '');
define("NAME2", 'users');

//conexão area de login
try {
    $db2 = new PDO("mysql:host=".HOST.";dbname=".NAME2.";charset=utf8", "".USER2."", "".PASS2."");
    $db2->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $db2->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    $db2->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
} catch (PDOException $e2) {
    echo $e2->getMessage();
}

// Função basica para gerenciamento PDO
function sql2($db2, $q, $params, $return) {
  // Prepare statement
    $stmt = $db2->prepare($q);
  // Execute statement
    $stmt->execute($params);
  // Decida se deseja retornar as linhas ou apenas contar as linhas
    if ($return == "rows") {
        return $stmt->fetch();
    }elseif ($return == "rowsall") {
        return $stmt->fetchAll();
    }elseif ($return == "count") {
        return $stmt->rowCount();
    }elseif( $return == "lastid"){
        return $db2->lastInsertId();
    }elseif( $return == "fake"){
        //fake sem nada
    }
}

function salvarDadosLoginCookie($user_id, $removeSeletorLogin = 0){
    global $db2;

    $contar_registros = sql2($db2, "SELECT id FROM manter_login WHERE id_user = ?", array($user_id), "count");
    $pega_ip = filter_input(INPUT_SERVER, 'HTTP_CF_CONNECTING_IP', FILTER_VALIDATE_IP);
    $seletor = base64_encode(random_bytes(9));
    $autenticador = random_bytes(33);
    $meu_token = $seletor.':'.base64_encode($autenticador);
    
    //Array com as informações do usuario para podermos autenticar mais tarde
    $novoArray = [
        'ip' => $pega_ip,
        'seletor' => $seletor,
        'autenticador' => hash('sha256', $autenticador),
        'data' => date('Y-m-d h:i:s', time())
    ];
    
    //Se a variavel removeSeletorLogin for diferente de zero, e porque a função foi chamada apos validar o hash_equals, então precisamos dar update no array e remover o seletor, para que não possa ser mais usado
    if($removeSeletorLogin !== 0){
        $lrdados = sql2($db2, "SELECT * FROM manter_login WHERE id_user = ?", array($user_id), "rows");
        $gerarNovoArrayRemovendoSeletor = removeArrayLogin($removeSeletorLogin, $lrdados['login_history']);

        sql2($db2, "UPDATE manter_login SET login_history = ? WHERE id_user = ?", array($gerarNovoArrayRemovendoSeletor, $user_id), "fake");
    }
    
    //Se não houver registros iremos inserir um novo com o array de informações, Se ja exister um registro, o mesmo será modificado e iremos acrescentar um novo indice de array a ele, Esse Array poderá ter ate 5 indices, ou seja iremos permitir o uso da conta de um usuario a 5 dispostivos
    if($contar_registros > 0){
        $ldados = sql2($db2, "SELECT * FROM manter_login WHERE id_user = ?", array($user_id), "rows");
        $gerarNovoArray = updateArrayLogin($ldados['login_history'], $novoArray);
        
        //Inciaremos esse session para poder fazer logout desse seletor mais tarde
        $_SESSION['seletor_login_atual'] = $seletor;
        
        sql2($db2, "UPDATE manter_login SET login_history = ? WHERE id_user = ?", array($gerarNovoArray, $user_id), "fake");
    }else{
        //Array Inicial
        $novoArrayInsert = array($novoArray);
        sql2($db2, "INSERT INTO manter_login(id_user, login_history) VALUES (?, ?)", array($user_id, json_encode($novoArrayInsert)), "fake");
    }
    
    // Cookie com token e id do usuario, esse id será usado para validar se existe um seletor para um usuario, se houver um seletor pra ele, iremos permitir o login, se não houver o login e recusado
    setcookie('user_valid', json_encode(array( 'user_id' => $user_id, 'token' => $meu_token)), time() + (86400 * 7), '/', null, true, true);
}

function autenticarLogin(){
    global $db2;

    // Verificamos se o cookie existe
    if (!isset($_COOKIE['user_valid']) || empty($_COOKIE['user_valid'])) {
        return false;
    }

    // Verificamos se o cookie e valido
    if(!$cookie_login = @json_decode($_COOKIE['user_valid'], true)) {
        return false;
    }

    // Verificamos se os parametros estão corretos
    if (!(isset($cookie_login['user_id']) || isset($cookie_login['token']))) {
        return false;
    }

    // Verificamos o tamanho do cookie 
    if(strlen($_COOKIE['user_valid']) > 1500){
        return false;
    }
    
    // Verificamos se o separador existe
    if ((strpos($cookie_login['token'], ':') !== false) === false) {
        return false;
    }
    
    $id_user_by_cookie = abs($cookie_login['user_id']);

    // Verificamos se existe algum registro com o id do usuario no banco de dados
    $contar_registros_user = sql2($db2, "SELECT * FROM manter_login WHERE id_user = ?", array($id_user_by_cookie), "count");

    if($contar_registros_user === 0){
        return false;
    }

    if (empty($_SESSION['user_logado_id']) && !empty($_COOKIE['user_valid'])) {
        list($seletor, $autenticador) = explode(':', $cookie_login['token']);
        
        $registro_atual = sql2($db2, "SELECT * FROM manter_login WHERE id_user = ?", array($id_user_by_cookie), "rows");
        $encoda_login_history = json_decode($registro_atual['login_history'], true);

        // definido inicialmente como 0, se no foreach houver um seletor igual ao cookie, iremos definir o valor pra 1
        $verifica_existencia_seletor = 0; 
        $captura_autenticador = '';
        $captura_ip = '';
        $captura_seletor = '';
        
        //Vamos descobrir se os seletores do usuario pesquisado e igual ao que está no cookie
        foreach($encoda_login_history as $hlogin => $data) {
            if($data['seletor'] == $seletor){
                //Seletor Foi encontrado então iremos alterar o valor pra 1
                $verifica_existencia_seletor = 1;
                //Passamos o valor do token autenticador pra variavel
                $captura_autenticador = $data['autenticador'];
                //Passamos o ip para uma variavel externa para podermos validar tbm
                $captura_ip = $data['ip'];
                //Pegamos o seletor
                $captura_seletor = $data['seletor'];
                
                //vou limitar a vida de um cookie a 7 dias, ja que geramos um novo sempre que o session leva update
                if(calculaDiferencaData($data['data']) >= 7){
                    //Removemos o Indice do array que tem o seletor gerado a mais de 7 dias
                    sql2($db2, "UPDATE manter_login SET login_history = ? WHERE id_user = ?", array(removeArrayLogin($seletor, $registro_atual['login_history']), $registro_atual['id_user']), "fake");

                    //Nesse caso o seletor deixou de existir, então voltaremos o valor dele para zero
                    $verifica_existencia_seletor = 0;
                }
            }
        }

        //Verificamos se o seletor foi encontrado
        if($verifica_existencia_seletor === 0){
            setcookie('user_valid', null);
            return false;
        }

        $pega_ip = filter_input(INPUT_SERVER, 'HTTP_CF_CONNECTING_IP', FILTER_VALIDATE_IP);
        
        //Se os ips do banco de dados e o ip da maquina do usuario forem diferentes não iniciaremos a sessao de login
        if($captura_ip !== $pega_ip){
            return false;
        }

        //Validamos o hash - se estiver tudo ok, podemos iniciar a session
        if (hash_equals($captura_autenticador, hash('sha256', base64_decode($autenticador)))) {
            //Inciaremos a session com id do usuario
            $_SESSION['user_logado_id'] = $registro_atual['id_user'];
            
            //Agora que o login foi bem sucessido, iremos re-criar o cookie e deletar o seletor do array, para que ele não possa ser utilizado novamente
            salvarDadosLoginCookie($registro_atual['id_user'], $captura_seletor);

            return true;
        }
    }

    if(isset($_SESSION['user_logado_id'])){
        return true;
    }
}

//Função destinada para deslogar o dispositivo que o usuario ta logado
function deslogarDispositivoAtual(){
    global $ta_logado, $db2;
    
    if($ta_logado === 1){
        $contar_registros_logout = sql2($db2, "SELECT * FROM manter_login WHERE id_user = ?", array($_SESSION['user_logado_id']), "count");
        
        if($contar_registros_logout > 0){
            $registros_logout = sql2($db2, "SELECT * FROM manter_login WHERE id_user = ?", array($_SESSION['user_logado_id']), "rows");

            sql2($db2, "UPDATE manter_login SET login_history = ? WHERE id_user = ?", array(removeArrayLogin($_SESSION['seletor_login_atual'], $registros_logout['login_history']), $_SESSION['user_logado_id']), "fake");
        }
    }
    
    session_destroy();
    setcookie('user_valid', null);
}

//Função para usar quando o usuario mudar a senha
function deslogarDeTodosDispositivos($id_deslogar){
    global $db2;
    
    sql2($db2, "DELETE FROM manter_login WHERE id_user = ?", array($id_deslogar), "fake");
    
    session_destroy();
    setcookie('user_valid', null);
}

function calculaDiferencaData($DataEvento){
    $hoje = new DateTime();
    $diferenca = $hoje->diff(new DateTime($DataEvento));
    return $diferenca->days;
}

function updateArrayLogin($arrayAtual, $novoArray){
    
    $arrayAtual = json_decode($arrayAtual, true);
    
    if(!is_array($arrayAtual)){
        $arrayAtual = array();
    }
    
    $arrayAtual[] = $novoArray;
    
    $arrayNovo = array_filter($arrayAtual, function($value) {
        return is_array($value);
    });

    if(($quantidade = count($arrayNovo)) > 5){
        $arrayNovo = array_slice($arrayNovo, $quantidade - 5, 5);
    }
    
    return json_encode($arrayNovo);
}

function removeArrayLogin($seletor, $arrayEdit){
    
    $arrayEdit = json_decode($arrayEdit, true);

    if(!is_array($arrayEdit)){
        $arrayEdit = array();
    }
    
    foreach($arrayEdit as $arrayE => $data) {
        if($data['seletor'] == $seletor){
            unset($arrayEdit[$arrayE]);
        }
    }
    
    return json_encode($arrayEdit);
}

and finally starting the functions to check if the user is logged in

$ta_logado = 0;

//Se o session user_logado_id não existe, iremos verificar se uma possivel sessão pode ser iniciada
if(!isset($_SESSION['user_logado_id']) || empty($_SESSION['user_logado_id'])){
    autenticarLogin();
}
//Se ja existir user_logado_id, iremos chamar os dados no banco de dados
if(isset($_SESSION['user_logado_id']) && !empty($_SESSION['user_logado_id'])){
    
    //Porem antes vamos validar e ver se o usuario não mudou a senha. Pois quando o usuario mudar a senha o registro no banco de dados e deletado.
    $contar_registros_user_atual = sql2($db2, "SELECT * FROM manter_login WHERE id_user = ?", array($_SESSION['user_logado_id']), "count");
    
    if($contar_registros_user_atual === 0){
        session_destroy();
        setcookie('user_valid', null);
    }else{
        $verificaDados = sql2($db2, "SELECT * from usuarios WHERE id = ?", array($_SESSION['user_logado_id']), "rows");
        if(isset($verificaDados['usuario']) && !empty($verificaDados['usuario'])) {
            $ta_logado = 1;
        }
    }
    
}

How can I improve the security of my system?

1 answer

0

There is no way to improve the security of a system without assuming what the problems would be. You need to define what the problems would be, so you can define what to do for each of the situations (and what new problems this would create).


But, in general, there are some points:

The validation of IP:

Make sure you are denying any to the site/server outside Cloudflare. Otherwise, if you allow you to access the page from outside Cloudflare (this is, http://123.123.123.123/sua-pagina.php) would be possible the header of HTTP_CF_CONNECTING_IP.

  • In general, restricting the IP in this way also implies a usability problem, since those who use mobile data can constantly have the IP changed. In addition multiple users can use the same IP due to CGNAT and network sharing.

Write/update access to the database:

You are using hash instead of hmac, This makes the database have the hash and the user to pre-imagem da hash. However, this is not enough if the attacker has write (or update) access to the database.

  • I believe that a better construction would be something like: hmac('sha256', $_CHAVE_DA_APLICAO, $_ID_DO_USARIO + $_TOKEN_ALEATORIO). This construction prevents two attack variants, which currently the hash does not protect:

    • If it is possible to create a record on manter_login arbitrarily: the token will have to use the $_CHAVE_DA_APLICAO, what will be unknown to the attacker (assuming that the $_CHAVE_DA_APLICAO be safe until that point).
    • If it is possible to update and view data from manter_login, the concatenation of $_ID_DO_USARIO + $_TOKEN_ALEATORIO will make it impossible to re-use a known pre-image. This is more difficult to explain: suppose I have an account, and I have TOKEN=A corresponds to HASH=B and gives access to ID=100. I could change the user ID linked to the HASH=B, so my TOKEN=A would have access to another account (ID=111). Concatenation invalidates this attack, since pre-image is built based on the user ID, so updating the ID in the database will be useless. Then even if I change the database to ID=111, the hmac will be of 100 + token and then will be invalid for 111 + token.

$The $seletor is not used in SELECT

I don’t know what the purpose of $selector, but initially I thought this was to intentionally generate collisions, but apparently not the case.

  • I believe that the $selector could be used in the SELECT, in order to generate larger collisions.

    • This construction makes it difficult to know if a user has been logged in recently, or if there are many or few accesses, based on response time (timing-Attack). This is because several tokens, from several different users, could use the same $selector.

    • This solution may also end up having a Dos, since due to the amount of collisions it could contain many records.


Perhaps there are more problems, or the problems mentioned here are unanswerable. As I said at the beginning, it depends on what problem you really want to solve and what you regard as a problem. For example, in some cases you can assume that: if the database is committed to the point of allowing sessions to be added, the same database is also committed to the point of allowing you to change a user’s password arbitrarily.

  • I switched to hmac and put a CHAVE_DA_APLICAO in the "$novoArray" array, because this way each session will have a CHAVE_DA_APLICAO itself

Browser other questions tagged

You are not signed in. Login or sign up in order to post.