How to prevent CSRF attack without PHP frameworks?

Asked

Viewed 3,664 times

12

I have the following files based on other scripts I 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>
  • That’s 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?
  • Shall I wear tokens only at the time of authentication?
  • Shall I wear tokens when I’m already authenticated?
  • Shall I wear tokens for forms that do not require authentication?
  • Should I use in GET method or only in POST or the variation of the need goes from the question of data and not the type of request?

I read about it, but I see that a lot of information differs in various respects.

  • You can improve the system by adding fields with random names. I’ll give an example in the answer.

  • @Filipemoraes Sounds like a really great suggestion

2 answers

8


I believe it is necessary to protect all your forms against CSRF attacks, even those that need authentication to gain access, since the attacker can create an account, log in and attack.

Every type of entry into your system should be handled, however insignificant it may seem, especially when that entry depends on data completed by the user.

When choosing the GET or POST method, first we have to know:

  1. GET can be cached, this would be bad if you don’t want private information exposed in the URL.
  2. GET should never be used with sensitive data such as passwords, banking data, etc... since it is exposed in the URL and cached by the browser.
  3. GET has length restriction, meaning your URL is limited to X characters (if I’m not mistaken there are differences between browsers).
  4. POST will never be cached, so all information sent exists only at that time.
  5. POST has no length restrictions.
  6. The data is encapsulated in the HTTP request body, so it is not exposed in the URL, for example.
  7. POST is slightly slower by encapsulating the message.
  8. 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 a news meusite.com?pagina=noticias&id=12 or meusite.com/news/12, then use GET, otherwise use POST.

Although the question code uses only the POST, the example below can be used for both GET and POST. We will 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) in each request, that is, a hidden field will be added to each existing form with the token as value.

This token is generated by PHP and stored in a session so when there is a request, the system will compare it to the value that was filled automatically (or malicious) in 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. With each form submission, as with the token, a new random name is generated for the field.

See an example of a POST request passing fields with the fixed name. I used the fields nome and email 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 the name each field:

//...

----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 the name was 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 then reuse and even for organization/maintenance reasons, for now let’s just modify your code.

Modifying the code autenticar.php.

<?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 login.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.

  • I haven’t finished reading the answer, but you explain well about GET and the question of Cache that may conflict with the POST, really seems a great answer, I will study it and put in practice, for now thanks! As soon as I draw all the conclusions I’ll be back to deliver the reward!

  • @Guilhermenascimento study yes! Now in my opinion using frameworks is a good option. But if you don’t want to use a framework, you can use ready-made components like Symfony Components. You don’t need to use the Symfony framework, you can only use the components, by the way, Symfony Framework itself uses Symfony Components.

  • If I thought about it, that’s the question just, understand how to do, understand how it works, I usually use Laravel that uses the Symfony Components. But I am creating a totally independent own framework that the ram consumption is 500k vs Laravel which is 8mb. I know it seems vague or even useless from the point of view of those outside the project, but the idea is to create an extremely light and simple framework, but still "scalable" to a certain level, however understand how was the generated token "anti-csrf" was complicated, thanks for while :)

  • There are "lighter" and simpler micro-frameworks, for example Silex: http://silex.sensiolabs.org/ is from the same company that makes Symfony. But that’s up to you, of course! In my opinion it’s not worth building a new one, but that’s my opinion and I’ll never use it to make anyone say I’m right! :)

  • I know and understand, Aliyis understand everything as suggestions and constructive criticism, but deep down beyond a personal challenge is an ambition :D - About Silex I’ve tested it, as well as tested others, yet in a test with Apachebench my personal framework almost always gets to be at least 20% faster than all I tested (of course the test was done in "basic", a simple Helloworld route). Thanks anyway for the tips. [edited] Silex 86 requests per second and my 420 requests per second framework.

  • The implementation makes the structure of a framework somewhat complex, but I chose the answer because it explains important issues how Cache with anti-csrf will conflict.

Show 1 more comment

6

I, in particular, found the code very messy, not to mention that it is not doing the treatment in case $_POST does not exist (if any improper modification is made to the form). I recommend you do it this way:

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());

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 problem because it only generates the new code after validating $_POST, if it exists.

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

Your system has a.php payment 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 (or Cracker) to request $_POST on the.php payment page, but how would he do that? The hacker sends a link to the victim, and this link is nothing more than a PHP script that will upload an image to the user, and underneath the scenes, will make a $_POST request to your system page.

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

  • 1

    is because it was just an example and I didn’t get into the details of the other inputs that wasn’t the focus, I’m going to edit the question and the code. I think $_SERVER['REQUEST_METHOD'] === 'POST' a bad practice (no offense intended), it may be best to check everything with isset($_POST[login], $_POST[senha], $_POST[token]).

  • The other data passed via form will be validated with ternary operator. I did not put because as you said, is not the focus. I will edit and put a more complete example to be sure.

  • 1

    The use of $_SERVER['REQUEST_METHOD'] === 'POST' is not a bad practice. This varies with the business policy of each environment. I can, for example, restrict a request to be necessarily sent by the POST method. If a user sends by another method I will then have a way to identify and return a more precise error code regarding the restriction. Thus the user can correct and adapt to the imposed business rule, without more difficulty. It is merely an extra level of bureaucracy. It doesn’t mean it’s right or wrong or even bad practice.

  • @Danielomine actually I did not mean that it was wrong, but it leads to a "false true", many tie themselves in it as being the way, and the entries of the request can vary that the use of isset($_POST['token'], $_POST['login'], $_POST['senha']) can fall and much better use, the use of $_SERVER['REQUEST_METHOD'] === 'POST' will be for use of specific situations, such as access for example "maybe" in the use of php://input which would dispense with openness with fopen needlessly.

  • 1

    I didn’t understand anything William and besides that has nothing to do. rsrsr

  • @Danielomine yes has nothing to do even, what happened was that Clayderson gave a tip and you gave another and I gave my point of view on how to use, IE were only 3 points of view, no use criticizing me saying that "has nothing to do with", was just a series of criticisms of how to validate the POST, an exchange of ideas that we 3 made.

  • 1

    The use of the function isset is sufficient if the POST not existing will not execute the code anyway. That first if is unnecessary.

  • @Exact Filipemoraes, that’s what I said, I hope the staff will understand as constructive criticism.

Show 3 more comments

Browser other questions tagged

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