How to prevent CSRF attack without PHP frameworks?

10

I have the following files based on other scripts I've tried to study:

authenticate.php

<?php
session_start();

if (isset($_POST['token'], $_POST['login'], $_POST['senha'])) {
    $token = empty($_SESSION['token']) ? NULL : $_SESSION['token'];

    if ($_POST['token'] === $token) {
         /*Valida $_POST['login'] e $_POST['senha']*/
    } else {
         echo 'Requisição invalida';
    }
} else {
    echo 'Faltam dados no Form';
}

login.php

<?php
session_start();

$_SESSION['token'] = md5(uniqid(rand(), true));
?>

<form method="POST" action="autenticar.php">
<input type="hidden" name="token" value="<? php echo $_SESSION['token']?>" />
<input type="text" name="login" placeholder="login"><br>
<input type="password" name="senha" placeholder="senha"><br>
<button type="submit">Logar</button>
</form>
  • This is enough to help prevent the attack, create a random key (I know it's just a prevention)?
  • uniqid(...) with md5 or sha1 is the best to generate this token?
  • Should I use tokens only at the time of authentication?
  • Should I use tokens when I am already authenticated?
  • Should I use tokens for forms that do not require authentication?
  • Should I use GET or POST only, or does the need vary from the data question to the request type?

I read about it, but I see that a lot of information differs in many ways.

    
asked by anonymous 23.02.2016 / 00:54

2 answers

5

I believe you need to protect all your forms against CSRF attacks, even those that need authentication to gain access, as the attacker can create an account, login and attack.

Any type of entry in your system should be handled, no matter how insignificant it may seem, especially when that entry depends on data you fill in.

As for the choice of the GET or POST method, we first have to know:

  • GET can be cached, this would be bad if you do not want private information exposed in the URL.
  • GET should never be used with sensitive data, such as passwords, bank details, etc., since it is exposed in the URL and cached by the browser.
  • GET has length restriction, meaning your URL is limited to X characters (if I'm not mistaken there are differences between browsers).
  • POST will never be cached, so all the information submitted exists only at that time.
  • POST has no length restrictions.
  • The data is encapsulated in the body of the HTTP request, so it is not exposed in the URL, for example.
  • POST is slightly slower by encapsulating the message.
  • POST accepts other data types, such as binary, in turn GET only accepts ASCII characters.
  • In short, if you want the user to know the URL (routes), for example, access news meusite.com?pagina=noticias&id=12 or meusite.com/news/12 , then use GET, otherwise use POST.

    Although the question code uses only POST, the example below can be used for both GET and POST. We'll use two methods to help prevent attacks on GET and POST requests.

    1st Method: use of token

    This method consists of including a random token (string) on each request, that is, a hidden field is added to each existing form with the token as value.

    This token is generated by PHP and stored in a session for when a request exists, the system compares with the value that was automatically filled in (or malicious) on the form.

    Further down we will join the 2 methods and create a script to prevent the attack. For now let's talk about the second method.

    2nd Method: fields with random names

    This method uses random names for each form field. The random value for each field is stored in a session variable. Each form submission, just like the token, generates a new random name for the field.

    See an example of a POST request passing fields with the fixed name. I used the nome and email fields for example:

    POST /form.php HTTP/1.1
    Host: testes.loc
    Cache-Control: no-cache
    Postman-Token: d2b73c66-68fe-8dc6-4660-010a41a8c9b0
    Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
    
    ----WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name="name"
    
    filipe
    ----WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name="email"
    
    [email protected]
    ----WebKitFormBoundary7MA4YWxkTrZu0gW
    

    Now using the technique described above, see how%% of each field was left:

    //...
    
    ----WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name="C345Gdfbn56789mnbg"
    
    filipe
    ----WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name="ERfbj567Mb867Jpknl6h"
    
    [email protected]
    ----WebKitFormBoundary7MA4YWxkTrZu0gW
    

    When making a new request, we see that name has been changed again:

    //...
    
    ----WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name="Hgfkpso456jnbJYBV097"
    
    filipe
    ----WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name="670JK7hgdTJb60Kjh0I6T"
    
    [email protected]
    ----WebKitFormBoundary7MA4YWxkTrZu0gW
    


    Implementing the above methods:

    You can improve the system and organize everything in classes to later reuse and even for organization / maintenance issues, for now let's just modify your code.

    Modifying the code name .

    <?php
    session_start();
    
    if(isset($_SESSION['Token'], $_SESSION['TokenFieldName'], $_SESSION['LoginFieldName'], $_SESSION['SenhaFieldName'])) {
        if (isset($_POST[$_SESSION['TokenFieldName']], $_POST[$_SESSION['LoginFieldName']], $_POST[$_SESSION['SenhaFieldName']])) { 
            if ($_POST[$_SESSION['TokenFieldName']] === $_SESSION['Token']) {
                 /*Valida $_POST['login'] e $_POST['senha']*/
            } else {
                 echo 'Requisição invalida';
            }
        } else {
            echo 'Faltam dados no Form';
        }
     }
    
    //Apaga o token e os campos
    //Isso é necessário, caso contrário bastava o atacante fazer um inspecionar elemento e ver os respectivos names no formulário e apenas executar um POST Request para o 'autenticar.php'.
    //O token e os respectivos nomes serão gerados novamente no 'login.php', sendo assim terá sempre que passar pelo formulário.
    unset($_SESSION['Token']);
    unset($_SESSION['TokenFieldName']);
    unset($_SESSION['LoginFieldName']);
    unset($_SESSION['SenhaFieldName']);
    

    Now, let's change the autenticar.php :

    <?php
    //Função para gerar código randômico.
    function generateRandomString($length = 15) {
        $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
        $charactersLength = strlen($characters);
        $randomString = '';
        for ($i = 0; $i < $length; $i++)
            $randomString .= $characters[rand(0, $charactersLength - 1)];
    
        return $randomString;
    } 
    
    //Gera um novo token
    $_SESSION['Token'] = generateRandomString();
    
    //Gera um nome aleatório para cada campo do formulário
    $_SESSION['TokenFieldName'] = generateRandomString();
    $_SESSION['LoginFieldName'] = generateRandomString();
    $_SESSION['SenhaFieldName'] = generateRandomString();
    ?>
    
    <form method="POST" action="autenticar.php">
        <input type="hidden" name="<?=$_SESSION['TokenFieldName']?>" value="<?=$_SESSION['Token']?>" />
        <input type="text" name="<?=$_SESSION['LoginFieldName']?>" placeholder="login"><br>
        <input type="password" name="<?=$_SESSION['SenhaFieldName']?>" placeholder="senha"><br>
        <button type="submit">Logar</button>
    </form>
    

    Ready, every time the user accesses the form, each field will have a name different from the previous one and a new token will be generated.

        
    03.03.2016 / 20:34
    3

    In particular, I found the code very messy, not to mention that it is not doing the processing in case the $ _POST does not exist (if there is any improper modification in the form). I recommend you do it as follows:

    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        if (isset($_POST['hiddenKey']) && $_POST['hiddenKey'] === $_SESSION['hiddenKey']) {
    
            $nome = (isset($_POST['nome'])) ? trim($_POST['nome']) : null;
            $sobrenome = (isset($_POST['sobrenome'])) ? trim($_POST['sobrenome']) : null;
            $cpf = (isset($_POST['cpf'])) ? trim($_POST['cpf']) : null;
    
            if (!empty($nome) && !empty($sobrenome) && !empty($cpf)) {
                #code...
            } else {
                echo 'Por favor, preencha todos os dados.';
                exit;
            }
        } else {
            echo 'Ops, algo deu errado. Tente novamente.';
            exit;
        }
    }
    
    $_SESSION['hiddenKey'] = sha1(rand());
    

    In this way, each time the page is updated, a new code will be generated to be assigned to the hidden field, and will not give a problem because it only generates the new code after validating $ _POST, if any. p>

    • This is enough to prevent a CSRF.
    • The value that will be put in the hidden field does not matter, just be random and, preferably, with 3 or more characters (in our case, we have 40 characters).
    • It does not matter if it is $ _GET or $ _POST, but at the beginning, it is recommended to use $ _POST. Only $ _GET is used in very specific situations, and this is for everything that involves form.
    • This validation of hidden to prevent CSRF attacks does not need to be done on login forms.
    • Validation is usually done on contact forms and forms within customer, administration, and etc areas ... See below why.

    Your system has a payment.php page, and this page has a condition that when receiving data via $ _POST performs the function of transferring money from one account to another.

    Assuming the victim is already logged in, it would be enough for the hacker to make a $ _POST request on the pay.php page, but how would he do it? The hacker sends a link to the victim, and this link is nothing more than a PHP script that will load an image to the user, and underneath the screens, will make a $ _POST request for the page of your system. >

    At the end of the day, it will appear that it was the victim who made the money transfer through the form contained in your system.

        
    23.02.2016 / 03:00