PHP authentication: Difference between revisions
No edit summary |
No edit summary |
||
(4 intermediate revisions by the same user not shown) | |||
Line 16: | Line 16: | ||
* Set the SameSite attribute appropriately, probably to 'Strict' unless you really need another type (session.cookie_samesite = "Strict" or $options['samesite'] = 'Strict') | * Set the SameSite attribute appropriately, probably to 'Strict' unless you really need another type (session.cookie_samesite = "Strict" or $options['samesite'] = 'Strict') | ||
* Session lifetime is set to zero seconds, which in most browsers will mean the cookie is deleted when the browser is closed (session.cookie_lifetime = 0 or $options['expires'] = 0) | * Session lifetime is set to zero seconds, which in most browsers will mean the cookie is deleted when the browser is closed (session.cookie_lifetime = 0 or $options['expires'] = 0) | ||
Note you cannot rely on the session cookie being deleted when the browser is closed, as this behaviour is not controlled by PHP and many users leave their browsers open all the time anyway. However, you can delete the session identifier from the server (e.g. using garbage collection), which will effectively render the cookie useless because there will be no matching record. | |||
Things you might want to consider: | Things you might want to consider: | ||
Line 22: | Line 24: | ||
* Regenerate the session ID when privileges are changed | * Regenerate the session ID when privileges are changed | ||
* Restricting cookies to the subdomain, e.g. if you use www.example.org then cookies should be set for that domain and not example.org | * Restricting cookies to the subdomain, e.g. if you use www.example.org then cookies should be set for that domain and not example.org | ||
* If you are on a shared server, setting session.save_path to a restricted directory (i.e. not world-readable, which it often is by default) will help prevent other users from viewing the session IDs. Note that all that is required to get the session IDs is the ability to list files in the directory, because the filenames usually contain the session ID - which may be sufficient to hijack sessions. This is redundant if sessions are saved in Redis, a database etc. | |||
Do not mess around with options such as session.hash_function (removed as of 7.1 anyway). Your PHP distribution vendor should have set these to sensible values - if not you have bigger things to worry about. | Do not mess around with options such as session.hash_function (removed as of 7.1 anyway). Your PHP distribution vendor should have set these to sensible values - if not you have bigger things to worry about. | ||
Remember that a session ID is a shared token between the client and the server. Any client with the token can convince the server that it is authorised to access the data held alongside the token, and therefore the token should be viewed as a secret and handled as such (do not output it to the page, do not include in URLs, always send over HTTPS etc.) | |||
== Remember me == | |||
To implement a 'remember me' function, you need to set a cookie manually instead of using PHP sessions. This is because sessions are intended to be short-lived and may disappear from the server (e.g. Debian deletes unused sessions automatically after a period of time). | |||
To prevent timing attacks, the contents of the 'remember me' cookie should have two parts: something to identify/select the record and something to verify the record. The identifier has the same purpose as the username when logging in - you use it to fetch a given row from the database such as: | |||
SELECT token FROM remember_me_tokens WHERE identifier = :identifier | |||
Assuming your identifier is unique (which it should be - generate a unique ID, do not use the user ID or any other column from the database) one row will be returned in more or less constant time (assuming your database engine has indexed the identifier, which most will for a unique column). You can then check if the token matches the verify part of the cookie. In this case the token is not using a password hashing algorithm such as bcrypt, but we need to use the hash_equals function (similar to password_hash, this is designed to neutralise timing attacks by giving an answer in constant time, regardless of whether the answer is true or false). | |||
== References == | == References == | ||
* [https://www.php.net/manual/en/session.security.ini.php Securing Session INI Settings] | * [https://www.php.net/manual/en/session.security.ini.php Securing Session INI Settings] |
Latest revision as of 17:23, 8 April 2023
Login
- Store hash of password in database
- Check password by fetching row based on username, then use password_verify (which is safe against timing attacks)
- After a successful verification, call password_needs_rehash to see if the hash needs to be updated, e.g. if you have increased the hash cost
Session cookies
Whether you use the built-in functionality or setcookie, you need to ensure that:
- Sessions are only sent in cookies, not URL parameters (session.use_cookies = 1, session.use_only_cookies = 1)
- Only send session cookies for HTTP requests (session.cookie_httponly = 1 or $options['httponly'] = true)
- Only send cookies for secure requests (session.cookie_secure = 1 or $options['secure'] = true)
- Cookies are restricted to the domain (session.cookie_domain = "example.org" or $options['domain'] = 'example.org')
- Use strict mode (session.use_strict_mode = 1)
- Set the SameSite attribute appropriately, probably to 'Strict' unless you really need another type (session.cookie_samesite = "Strict" or $options['samesite'] = 'Strict')
- Session lifetime is set to zero seconds, which in most browsers will mean the cookie is deleted when the browser is closed (session.cookie_lifetime = 0 or $options['expires'] = 0)
Note you cannot rely on the session cookie being deleted when the browser is closed, as this behaviour is not controlled by PHP and many users leave their browsers open all the time anyway. However, you can delete the session identifier from the server (e.g. using garbage collection), which will effectively render the cookie useless because there will be no matching record.
Things you might want to consider:
- Regenerate the session ID regularly - this might be important for applications where users stay logged in for a long time, e.g. the entire working day
- Regenerate the session ID when privileges are changed
- Restricting cookies to the subdomain, e.g. if you use www.example.org then cookies should be set for that domain and not example.org
- If you are on a shared server, setting session.save_path to a restricted directory (i.e. not world-readable, which it often is by default) will help prevent other users from viewing the session IDs. Note that all that is required to get the session IDs is the ability to list files in the directory, because the filenames usually contain the session ID - which may be sufficient to hijack sessions. This is redundant if sessions are saved in Redis, a database etc.
Do not mess around with options such as session.hash_function (removed as of 7.1 anyway). Your PHP distribution vendor should have set these to sensible values - if not you have bigger things to worry about.
Remember that a session ID is a shared token between the client and the server. Any client with the token can convince the server that it is authorised to access the data held alongside the token, and therefore the token should be viewed as a secret and handled as such (do not output it to the page, do not include in URLs, always send over HTTPS etc.)
Remember me
To implement a 'remember me' function, you need to set a cookie manually instead of using PHP sessions. This is because sessions are intended to be short-lived and may disappear from the server (e.g. Debian deletes unused sessions automatically after a period of time).
To prevent timing attacks, the contents of the 'remember me' cookie should have two parts: something to identify/select the record and something to verify the record. The identifier has the same purpose as the username when logging in - you use it to fetch a given row from the database such as:
SELECT token FROM remember_me_tokens WHERE identifier = :identifier
Assuming your identifier is unique (which it should be - generate a unique ID, do not use the user ID or any other column from the database) one row will be returned in more or less constant time (assuming your database engine has indexed the identifier, which most will for a unique column). You can then check if the token matches the verify part of the cookie. In this case the token is not using a password hashing algorithm such as bcrypt, but we need to use the hash_equals function (similar to password_hash, this is designed to neutralise timing attacks by giving an answer in constant time, regardless of whether the answer is true or false).