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:
- GET can be cached, this would be bad if you don’t want private information exposed in the URL.
- 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.
- 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 information sent exists only at that time.
- POST has no length restrictions.
- The data is encapsulated in the HTTP request body, 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 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.
You can improve the system by adding fields with random names. I’ll give an example in the answer.
– Filipe Moraes
@Filipemoraes Sounds like a really great suggestion
– Guilherme Nascimento