Security¶
Overview¶
NOTE
Requires PHP's openssl extension to be present in the system
Phalcon\Encryption\Security is a component that helps developers with common security-related tasks, such as password hashing and Cross-Site Request Forgery protection (CSRF).
NOTE
By default, the component will use password_hash
to hash a string using the Phalcon\Encrtyption\Security::CRYPT_DEFAULT
which defaults to Phalcon\Encryption\Security::CRYPT_BCRYPT
and corresponds to PHP's PASSWORD_BCRYPT
.
Password Hashing¶
Storing passwords in plain text is a bad security practice. Anyone with access to the database will immediately have access to all user accounts thus being able to engage in unauthorized activities. To combat that, many applications use popular one-way hashing methods md5 and sha1. However, hardware evolves on a daily basis and as processors become faster, these algorithms are becoming vulnerable to brute-force attacks. These attacks are also known as rainbow tables.
The security component uses bcrypt as the hashing algorithm. Thanks to the Eksblowfish key setup algorithm, we can make the password encryption as slow
as we want. Slow algorithms minimize the impact of brute-force attacks.
Bcrypt, is an adaptive hash function based on the Blowfish symmetric block cipher cryptographic algorithm. It also introduces a security or work factor, which determines how slow the hash function will be to generate the hash. This effectively negates the use of FPGA or GPU hashing techniques.
Should hardware become faster in the future, we can increase the work factor to mitigate this. The salt is generated using pseudo-random bytes with the PHP's function openssl_random_pseudo_bytes.
This component offers a simple interface to use the algorithm:
<?php
use Phalcon\Encryption\Security;
$security = new Security();
echo $security->hash('Phalcon');
// $2y$08$ZUFGUUk5c3VpcHFoVUFXeOYoA4NPFEP4G9gcm6rdo3jFPaNFdR2/O
We can now check if a value sent to us by a user through the UI of our application is identical to our hashed string:
<?php
use Phalcon\Encryption\Security;
$password = $_POST['password'] ?? '';
$security = new Security();
$hashed = $security->hash('Phalcon');
echo $security->checkHash($password, $hashed); // true / false
The above example simply shows how the checkHash()
can be used. In production applications, we will definitely need to sanitize input, and also we need to store the hashed password in a data store such as a database. Using controllers, the above example can be shown as:
<?php
use MyApp\Models\Users;
use Phalcon\Http\Request;
use Phalcon\Mvc\Controller;
use Phalcon\Encryption\Security;
/**
* @property Request $request
* @property Security $security
*/
class SessionController extends Controller
{
public function loginAction()
{
$login = $this->request->getPost('login');
$password = $this->request->getPost('password');
$user = Users::findFirst(
[
'conditions' => 'login = :login:',
'bind' => [
'login' => $login,
],
]
);
if (false !== $user) {
$check = $this
->security
->checkHash($password, $user->password);
if (true === $check) {
// OK
}
} else {
$this->security->hash(rand());
}
// ERROR
}
public function registerAction()
{
$login = $this->request->getPost('login', 'string');
$password = $this->request->getPost('password', 'string');
$user = new Users();
$user->login = $login;
$user->password = $this->security->hash($password);
$user->save();
}
}
NOTE
The code snippet above is incomplete and must not be used as is for production applications
The registerAction()
above accepts posted data from the UI. It sanitizes it with the string
filter and then creates a new User
model object. It then assigns the passed data to the relevant properties before saving it. Notice that for the password, we use the hash()
method of the Phalcon\Encryption\Security component so that we do not save it as plain text in our database.
The loginAction()
accepts posted data from the UI and then tries to find the user in the database based on the login
field. If the user does exist, it will use the checkHash()
method of the Phalcon\Encryption\Security component, to assess whether the supplied password hashed is the same as the one stored in the database.
NOTE
You do not need to hash the supplied password (first parameter) when using checkHash()
- the component will do that for you.
If the password is not correct, you can then inform the user that something is wrong with the credentials. It is always a good idea not to provide specific information about your users to people who want to hack your site. So for instance our example above can produce two messages:
- User not found in the database
- Password is incorrect
Separating the error messages is not a good idea. If a hacker that is using brute force attack detects the second message, they can stop trying to guess the login
and concentrate on the password, thus increasing their chances of gaining access. A more appropriate message for both potential error conditions could be
Invalid Login/Password combination
Finally, you will notice in the example that when the user is not found, we call:
This is done to protect against timing attacks. Irrespective of whether a user exists or not, the script will take roughly the same amount of time to execute, since it is computing a hash again, even though we will never use that result.
Work Factor¶
The work factor is what we also refer to as cost
. It is a number that is passed in the crypt()
method to hash the string. The work factor can be any number between 4
and 31
. The higher the number, the slower the algorithm will be.
The work factor can be set using the setWorkFactor()
method or passed as an element of the second parameter to the hash()
method.
<?php
use Phalcon\Encryption\Security;
$password = 'password1';
$security = new Security();
$hashed = $security->hash('Phalcon', ['cost' => 31]);
echo $security->checkHash($password, $hashed); // true / false
The workFactor
(or cost
) is used when: - We are using a legacy hash (i.e. one that does not use the password_hash
method) and in particular the Phalcon\Encryption\Security::CRYPT_BLOWFISH_A
or Phalcon\Encryption\Security::CRYPT_BLOWFISH_X
. - We are using a non-legacy hash (i.e. using password_hash
) with the Phalcon\Encryption\Security::CRYPT_DEFAULT
or Phalcon\Encryption\Security::CRYPT_BCRYPT
algorithms.
Argon2i¶
Phalcon\Encryption\Security
also supports the new Argon2i hashing algorithm. This algorithm is the winner of the Password Hashing Competition and is considered to be the best algorithm for hashing passwords. It is also the default algorithm used by PHP's password_hash()
method.
<?php
use Phalcon\Encryption\Security;
$password = 'password1';
$security = new Security();
$security->setDefaultHash(Security::CRYPT_ARGON2I);
$hashed = $security->hash(
'Phalcon',
[
'memory_cost' => PASSWORD_ARGON2_DEFAULT_MEMORY_COST,
'time_cost' => PASSWORD_ARGON2_DEFAULT_TIME_COST,
'threads' => PASSWORD_ARGON2_DEFAULT_THREADS,
]
);
echo $security->checkHash($password, $hashed); // true / false
If no options are set, the defaults will be used.
Exceptions¶
Any exceptions thrown in the Security component will be of type Phalcon\Encryption\Security\Exception. You can use this exception to selectively catch exceptions thrown only from this component. Exceptions can be raised if the hashing algorithm is unknown, if the session
service is not present in the Di container etc.
<?php
use Phalcon\Encryption\Security\Exception;
use Phalcon\Mvc\Controller;
class IndexController extends Controller
{
public function index()
{
try {
$this->security->hash('123');
} catch (Exception $ex) {
echo $ex->getMessage();
}
}
}
CSRF Protection¶
Cross-Site Request Forgery (CSRF) is another common attack against websites and applications. Forms designed to perform tasks such as user registration or adding comments are vulnerable to this attack.
The idea is to prevent the form values from being sent outside our application. To fix this, we generate a random nonce (token) in each form, add the token in the session, and then validate the token once the form posts data back to our application by comparing the stored token in the session to the one submitted by the form:
<form method='post' action='session/login'>
<!-- Login and password inputs ... -->
<input type='hidden'
name='<?php echo $this->security->getTokenKey() ?>'
value='<?php echo $this->security->getToken() ?>'/>
</form>
Then in the controller's action, you can check if the CSRF token is valid:
<?php
use Phalcon\Mvc\Controller;
/**
* @property Request $request
* @property Security $security
*/
class SessionController extends Controller
{
public function loginAction()
{
if ($this->request->isPost()) {
if ($this->security->checkToken()) {
// OK
}
}
}
}
NOTE
It is important to remember that you will need to have a valid session
service registered in your Dependency Injection container. Otherwise, the checkToken()
will not work.
Adding a captcha to the form is also recommended to completely avoid the risks of this attack.
Functionality¶
Hash¶
getDefaultHash() / setDefaultHash()
Getter and setter for the default hash that the component will use. By default, the hash is set to CRYPT_DEFAULT
(0
). The available options are:
CRYPT_BLOWFISH_A
CRYPT_BLOWFISH_X
CRYPT_BLOWFISH_Y
CRYPT_MD5
CRYPT_SHA256
CRYPT_SHA512
CRYPT_DEFAULT
hash()
Hashes a string or password and returns the hashed string back. The second parameter is optional and allows you to set temporarily a specific workFactor
or pass that overrides the default one.
checkHash()
Accepts a string (usually the password), an already hashed string (the hashed password), and an optional minimum password length. It checks them both and returns true
if they are identical and false
otherwise.
isLegacyHash()
Returns true
if the passed hashed string is a valid bcrypt hash.
HMAC¶
computeHmac()
Generates a keyed hash value using the HMAC method. It uses PHP's hash_hmac method internally, therefore all the parameters it accepts are the same as the hash_hmac.
Random¶
getRandom()
Returns a Phalcon\Encryption\Security\Random object, which is a secure random number generator instance. The component is explained in detail below.
getRandomBytes()
/ setRandomBytes()
Getter and setter methods to specify the number of bytes to be generated by the openssl pseudo-random generator. It defaults to 16
.
getSaltBytes()
Generates a pseudo-random string to be used as a salt for passwords. It uses the getRandomBytes()
value for the length of the string. It can however be overridden by the passed numeric parameter.
Token¶
getToken()
Generates a pseudo-random token value to be used as input value in a CSRF check.
getTokenKey()
Generates a pseudo-random token key to be used as input's name in a CSRF check.
getRequestToken()
Returns the value of the CSRF token for the current request.
checkToken()
Check if the CSRF token sent in the request is the same as the current in session. The first parameter is the token key and the second one is the token value. It also accepts a third boolean parameter destroyIfValid
which, if set to true
will destroy the token if the method returns true
.
getSessionToken()
Returns the value of the CSRF token in the session
destroyToken()
Removes the value of the CSRF token and key from the session
Random¶
The Phalcon\Encryption\Security\Random class makes it really easy to generate lots of types of random data to be used in salts, new user passwords, session keys, complicated keys, encryption systems, etc. This class partially borrows SecureRandom library from Ruby.
It supports the following secure random number generators: * random_bytes
* libsodium
* openssl
, libressl
* /dev/urandom
To utilize the above you will need to ensure that the generators are available in your system. For instance to use openssl
your PHP installation needs to support it.
<?php
use Phalcon\Encryption\Security\Random;
$random = new Random();
echo $random->hex(10); // a29f470508d5ccb8e289
echo $random->base62(); // z0RkwHfh8ErDM1xw
echo $random->base64(16); // SvdhPcIHDZFad838Bb0Swg==
echo $random->base64Safe(); // PcV6jGbJ6vfVw7hfKIFDGA
echo $random->uuid(); // db082997-2572-4e2c-a046-5eefe97b1235
echo $random->number(256); // 84
echo $random->base58(); // 4kUgL2pdQMSCQtjE
base58()
Generates a random base58
string. If the $len
parameter is not specified, 16
is assumed. It may be larger in the future. The result may contain alphanumeric characters except 0
(zero), O
(capital o
), I
(capital i
), and l
(lowercase L
).
It is similar to base64()
but has been modified to avoid both non-alphanumeric characters and letters which might look ambiguous when printed.
<?php
use Phalcon\Encryption\Security\Random;
$random = new Random();
echo $random->base58(); // 4kUgL2pdQMSCQtjE
base62()
Generates a random base62
string. If the $len
parameter is not specified, 16
is assumed. It may be larger in the future. It is similar to base58()
but has been modified to provide the largest value that can safely be used in URLs without needing to take extra characters into consideration because it is [A-Za-z0-9]
<?php
use Phalcon\Encryption\Security\Random;
$random = new Random();
echo $random->base62(); // z0RkwHfh8ErDM1xw
base64()
Generates a random base64
string. If the $len
parameter is not specified, 16
is assumed. It may be larger in the future. The length of the result string is usually greater than $len
. The size formula is:
4 * ($len / 3)
rounded up to a multiple of 4.
<?php
use Phalcon\Encryption\Security\Random;
$random = new Random();
echo $random->base64(12); // 3rcq39QzGK9fUqh8
base64Safe()
Generates a URL-safe random base64
string. If the $len
parameter is not specified, 16
is assumed. It may be larger in the future. The length of the result string is usually greater than $len
.
By default, padding is not generated because =
may be used as a URL delimiter. The result may contain A-Z
, a-z
, 0-9
, -
, and _
. =
is also used if $padding
is true
. See RFC 3548 for the definition of URL-safe base64
.
<?php
use Phalcon\Encryption\Security\Random;
$random = new Random();
echo $random->base64Safe(); // GD8JojhzSTrqX7Q8J6uug
bytes()
Generates a random binary string and accepts as input an integer representing the length in bytes to be returned. If $len
is not specified, 16
is assumed. It may be larger in the future. The result may contain any byte: x00
- xFF
.
<?php
use Phalcon\Encryption\Security\Random;
$random = new Random();
$bytes = $random->bytes();
var_dump(bin2hex($bytes));
// Possible output: string(32) "00f6c04b144b41fad6a59111c126e1ee"
hex()
Generates a random hex string. If $len
is not specified, 16 is assumed. It may be larger in the future. The length of the result string is usually greater than $len
.
<?php
use Phalcon\Encryption\Security\Random;
$random = new Random();
echo $random->hex(10); // a29f470508d5ccb8e289
number()
Generates a random number between 0
and $len
. Returns an integer: 0 <= result <= $len
.
<?php
use Phalcon\Encryption\Security\Random;
$random = new Random();
echo $random->number(16); // 8
uuid()
Generates a v4 random UUID (Universally Unique IDentifier). The version 4 UUID is purely random (except the version). It doesn't contain meaningful information such as MAC address, time, etc. See RFC 4122 for details of UUID.
This algorithm sets the version number (4 bits) as well as two reserved bits. All other bits (the remaining 122 bits) are set using a random or pseudorandom data source. Version 4 UUIDs have the form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
where x is any hexadecimal digit and y
is one of 8
, 9
, A
, or B
(e.g., f47ac10b-58cc-4372-a567-0e02b2c3d479
). *
<?php
use Phalcon\Encryption\Security\Random;
$random = new Random();
echo $random->uuid(); // 1378c906-64bb-4f81-a8d6-4ae1bfcdec22
Dependency Injection¶
If you use the Phalcon\Di\FactoryDefault container, the Phalcon\Encryption\Security is already registered for you. However, you might want to override the default registration in order to set your own workFactor()
. Alternatively, if you are not using the Phalcon\Di\FactoryDefault and instead are using the Phalcon\Di\Di the registration is the same. By doing so, you will be able to access your configuration object from controllers, models, views, and any component that implements Injectable
.
An example of the registration of the service as well as accessing it is below:
<?php
use Phalcon\Di\FactoryDefault;
use Phalcon\Encryption\Security;
// Create a container
$container = new FactoryDefault();
$container->set(
'security',
function () {
$security = new Security();
$security->setWorkFactor(12);
return $security;
},
true
);
setWorkFactor()
sets the password hashing factor to 12 rounds. The component is now available in your controllers using the security
key
<?php
use Phalcon\Mvc\Controller;
use Phalcon\Encryption\Security;
/**
* @property Security $security
*/
class MyController extends Controller
{
private function getHash(string $password): string
{
return $this->security->hash($password);
}
}
Also in your views (Volt syntax)