====== PHP RFC: Semi-Automatic CSRF Protection ====== * Version: 0.1 * Date: 2016-05-10 * Author: Yasuo Ohgaki * Status: Inactive * First Published at: http://wiki.php.net/rfc/automatic_csrf_protection ===== Introduction ===== CSRF ([[https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)|Cross Site Request Forgery]]) has been major vulnerability for PHP applications. CSRF protection is not difficult to implement especially with good web application frameworks. However, CSRF protection requires many lines of code if users have to implement by themselves. This proposal extends session module to provide optional automatic/manual CSRF protection for PHP web applications. Although basic authenticity (CSRF protection) should be part of web infrastructure, it was not addressed in session managers because * CSRF token must be embedded in POST(web form) or GET(query string) request. To embed tokens, * Output buffering is required * On demand buffer rewrite is required PHP has output buffer and URL rewriter. These are used for Trans SID (Transparent Session ID) for session management without cookie currently. These can be extended to add CSRF protection. This proposal does not intended to replace framework's CSRF protections, but it provides alternative to PHP applications with minimum code/setting changes. ===== Proposal ===== ====TL;DR;==== This RFC archives: * **Web applications can be protected from CSRF attacks easily**, if CSRF protection is required only for POST requests from web forms. * For more complex CSRF protections, developers may use CSRF token and validation manually. * Existing CSRF protections can be used. (Feature is disabled by default) Session module is extended to manage CSRF validation. When CSRF protection is enabled, CSRF attack is prevented by session_start(). Execution is terminated by default, but developers may suppress error and detect CSRF attack by themselves via session_csrf_status(). Validation can be done by developers also via session_csrf_validate(). CSRF protection token is generated by random secret key stored in session data and specified TTL value. Stolen CSRF protection token only allows attacks for specific period upto TTL. Textbook web forms like
can be protected by one line. (( INI settings can be used to set protection options. )) SESSION_CSRF_POST, 'csrf_validate'=SESSION_CSRF_POST]); ?> How many CSRF vulnerabilities exist in PHP applications? Use of this CSRF protection is simple and easy. This RFC would help huge number of PHP applications. ====Example - Automatic site wide CSRF protection via POST ==== SESSION_CSRF_POST, 'csrf_validate'=>SESSION_CSRF_POST); ?> All post pages that use PHP session are protected. If application modify data only by POST, above session_start() is enough for POST CSRF protection. Developers can catch E_RECOVERABLE_ERROR ((Error level is configurable)) by set_error_handler() callback and handle them. NOTE: Browsers cannot send POST request directly. i.e. It must display "form" before submit. Forms will have CSRF token by URL rewriter always. Therefore, POST requests have CSRF token always. ====Example - Manual CSRF protection ==== Manually embedding tokens and validation codes is mistakable, but it is supported. To protect CSRF from manually, user can * Disable automatic rewrites (Default). * Disable automatic validation (Default). then, user can add token manually SESSION_CSRF_NONE, 'csrf_validate'=>SESSION_CSRF_NONE]); ?> // GET Delete ID:1234 // POST - put this inside form tag then, validate manually SESSION_CSRF_NONE, 'csrf_validate'=>SESSION_CSRF_NONE]); // This is POST. if (session_csrf_validate($_POST) !== SESSION_CSRF_VALID) { die('Invalid request'); } Followings are example for manual protection. Entry page. SESSION_CSRF_NONE, 'csrf_validate'=SESSION_CSRF_NONE]); ?> Delete ID:1234 Show ID:1234
Output to browser will be something like Remove ID:1234 Show ID:1234
delete.php SESSION_CSRF_NONE) and validation explicitly ('csrf_validate'=>SESSION_CSRF_NONE) - they are disabled by default session_start(['csrf_rewrite'=>SESSION_CSRF_NONE, 'csrf_validate'=>SESSION_CSRF_NONE]); if (session_csrf_validate($_GET) !== SESSION_CSRF_VALID) { die('CSRF Attack or expired CSRF token'); } // Delete data ?> show.php SESSION_CSRF_NONE, 'csrf_validate'=SESSION_CSRF_NONE]); // No CSRF validation token for this // Show data ?> edit.php SESSION_CSRF_NONE, 'csrf_validate'=SESSION_CSRF_NONE]); if (session_csrf_validate($_POST) !== SESSION_CSRF_VALID) { die('CSRF Attack or expired CSRF token'); } // Modify data ?> ====Example - Automatic rewrite and validation ==== ==GET rewrite== session_start(['csrf_rewrite'=>SESSION_CSRF_GET]); This rewrites Remove ID:1234 to Remove ID:1234 NOTE: Something like "SESSCSRF=1462920523-5fd057a6ff9dc7a124fa5c814765a498e5aa024a" is added automatically. ==POST rewrite== // Add CSRF protection token for POST ('csrf_rewrite'=>SESSION_CSRF_POST) session_start(['csrf_rewrite'=>SESSION_CSRF_POST]); This rewrites
to
NOTE: Something like "" is added to form automatically. ==CSRF Token Validation== Validate GET request. // 'csrf_validate'=>SESSION_CSRF_GET enables $_GET CSRF token validation session_start(['csrf_validate'=>SESSION_CSRF_GET]); Validate POST request. // 'csrf_validate'=>SESSION_CSRF_POST enables $_POST CSRF token validation session_start(['csrf_validate'=>SESSION_CSRF_POST]); Validate GET and POST request. // Enable both $_GET/$_POST CSRF token validation session_start(['csrf_validate'=>SESSION_CSRF_GET|SESSION_CSRF_POST]); ====Added/Extended settings/features==== It may seem implementation is complex due to number of configuration parameters/functions/constants, but it is straightforward and simple. - Add hidden internal data structure to session data that stores CSRF token generation key. \\ "__PHP_SESSION__" array is stored in session data, but user cannot see the key from their application. - Add **session.csrf_token_name**(string) INI. \\ CSRF token name. Default "SESSCSRF". - Add constants for session.csrf_rewrite/session.csrf_validate/session_csrf_token() * SESSION_CSRF_NONE : 0 * SESSION_CSRF_POST : 1 * SESSION_CSRF_GET : 2 * SESSION_CSRF_POST|SESSION_CSRF_GET : 3 - Add **session.csrf_rewrite**(int) INI. \\ Enable CSRF rewrite. (This INI value may be specified as session_start() parameter) Default: session.csrf_rewrite=SESSION_CSRF_NONE. - Add **session.csrf_validate**(int) INI. \\ Enable CSRF validation when session.csrf_protection=1. (This INI value may be specified as session_start() parameter) Default: session.csrf_validate=SESSION_CSRF_NONE. - Add **session.csrf_ttl**(int - seconds) INI. \\ Controls expiration of CSRF protection token when value is greater than 0. 0 for disabling TTL control. Default: 1800 - Add **session.csrf_domains**(string) INI. \\ Controls trusted domains default to "". When empty, HTTP_HOST is used. Default: empty - Add **session.csrf_hash**(string) INI. \\ Controls hash algorithm used for CSRF token generation. Any supported hash functions. "hmac-" prefix for HMAC. Default "hmac-sha256". - Add **session.csrf_error**(int - error level) INI. \\ Controls which error is raised for CSRF token validation error. Default: E_RECOVERABLE_ERROR - Extend **session_start()** \\ Support csrf_rewrite, csrf_validate, csrf_ttl, csrf_domains, csrf_error options. - Add int **session_csrf_status(void)** function \\ It returns CSRF token validation status. * SESSION_CSRF_DISABLED : CSRF protection is not enabled * SESSION_CSRF_INVALID : Invalid request * SESSION_CSRF_EXPIRED : CSRF token expired * SESSION_CSRF_VALID : Valid request - Add int **session_csrf_validate(array $input_to_validate)** function \\ It validates CSRF token manually, $_GET/$_POST or whatever array variable contains CSRF token. - Add string **session_csrf_token([int $token_string_type = SESSION_CSRF_NONE])** function returns CSRF token string. * SESSION_CSRF_NONE : Return CSRF token value only. * SESSION_CSRF_GET : Return CSRF token string for GET(query string). e.g. ini_get('session.csrf_token_name') . '=' . session_csrf_token(). * SESSION_CSRF_POST : Return CSRF token string for POST. e.g. ''; ====Behaviors==== ==Page Generation== - Generate random CSRF token key (csrf_token_key) if it does not exist in __PHP_SESSION__ array. - Compute CSRF protection token. \\ SESSCSRF (CSRF protection token value) = time()+session.csrf_ttl ."-". hash_hmac('sha256', time()+session.csrf_ttl , csrf_token_key) by default. - Set SESSCSRF as URL rewrite var. (This results URLs/forms have SESSCSRF=token_value in the page) ==Obtaining CSRF token manually== JS apps may need to obtain CSRF token manually. get_csrf_token.php SESSION_CSRF_GET]); // CSRF token is secure because it is generated by using secret random key stored in session data. echo json_encode(['SESSCSRF'=>session_csrf_token()]); ?> ==Token Validation == - Validation is performed according to session.csrf_validate setting. - Split SESSCSRF token value by "-" - Check CSRF validation mode (i.e. SESSION_CSRF_POST/GET). - Check TTL value part > time(). If expired, raise error. - Check CSRF token part === hash_hmac('sha256', ttl_value , csrf_token_key). If does not match, raise error. - If error is not raised, CSRF protection status can be verified by session_csrf_status() manually. ====Limitations==== * Since users may enable/disable CSRF protection, pages that accept requests must enable CSRF protection. Otherwise, protection will not work. i.e. This feature is not fool proof. * Since GET CSRF protections adds CSRF protection token to all applicable URLs, pages that have both private URL and public URL cannot use automatic GET CSRF protection. * CSRF token in URLs has the same risk as Trans SID. (CSRF token in URL is not recommended) * POST/GET must have a element to be validated. If you need to validate empty POST/GET or any other special inputs, use session_csrf_validate() manually. It returns SESSION_CSRF_DISABLED for empty array. ===== Q & A ===== ==Why PHP should have this?== Simplicity and Security for user code. If application requires POST CSRF protection only, 2 INI settings is good enough to protect whole application. No code modification is required. There are many existing applications that could be protected by this. This implementation is more secure than most CSRF protection implementations because it has TTL. CSRF token is only valid specified time and token value changes according to TTL. This implementation is much secure than session lifetime CSRF token (semi static CSRF token). It adds codes to PHP session module, but implementation (patch for this RFC) is straightforward and simple. ==CSRF protection does not belong to session management== Session task, by its definition, is to distinguish and manage state of requests. Session must distinguish requests, i.e. must keep authenticity. CSRF is obvious authenticity issue. Some of us see session manager as just a storage, but it's not sufficient definition. One can argue that "CSRF protection is not a mandatory requirement for session management". I agree with this argument. It's not considered as a mandatory today at least. However, "CSRF protection does not belong to session management" cannot be correct because authenticity is basic feature of session. The main reason why CSRF protection is not included in other languages' session managers is technical limitation. Output buffering and buffered content rewriter is required for implementation. PHP has them both, but other languages do not. PHP is made for web and not utilizing ability would be a waste of features. ==How to use with JS applications?== If there is no pages, you may use PHP script (get_csrf_token.php) mentioned above. Be Careful for TTL, but don't get new token always. It's waste of resources. ==CSRF tokens in URLs(GET requests) are security risk== Yes, it increases security risk of CSRF token exposure. It's not recommended for public web sites. However, have you ever write simple web tools for your development or operation environment? Did you implement full CSRF protection for every tool? I don't. GET protection is handy for such tools and provides instant full CSRF protection. ((You need redirection setup for GET requests because the very 1st request does not have CSRF token. It could be done by web server rewrite or auto_prepend_file.)) URL may be pasted to chat/mail/etc, but it is expired in 1800 seconds by default. ==get_csrf_token.php seems insecure== It's secure. Random CSRF token generation key is stored in session data which is private to users. CSRF token is generated by using the secret key. Therefore, attacker cannot get CSRF token unless they have stolen session already. ==Should all applications use this CSRF protection?== No. It works for large/complex applications, but one can use their own implementation. For instance, I have CSRF lib that detects both CSRF attack and multiple submits. Session module will never support multiple submit detection. ===== Backward Incompatible Changes ===== * None ===== Proposed PHP Version(s) ===== * PHP 7.1 ===== RFC Impact ===== ==== New Constants ==== * Validation status * SESSION_CSRF_DISABLED * SESSION_CSRF_INVALID * SESSION_CSRF_EXPIRED * SESSION_CSRF_VALID * CSRF types used for INI and function * SESSION_CSRF_NONE * SESSION_CSRF_GET * SESSION_CSRF_POST ==== php.ini Defaults ==== * hardcoded default values * php.ini-development values * php.ini-production values All defaults are the same * session.csrf_token_name = "SESSCSRF" * session.csrf_rewrite = SESSION_CSRF_NONE (0) * session.csrf_validate = SESSION_CSRF_NONE (0) * session.csrf_ttl = 1800 (seconds) * session.csrf_hash = "hmac-sha256" * session.csrf_domains = "" * session.csrf_error = E_RECOVERABLE_ERROR ===== Open Issues ===== - Any names (functions/constants/etc) are subject to be changed. Please comment. - How it works is subject to be changed. Please comment. - URL rewriter must be fixed before this RFC. (It has a issue. Patch exists, not applied yet) ===== Unaffected PHP Functionality ===== * This RFC does not affect how session ID is managed. * If there is CSRF protection module, this proposal works as long as session.csrf_token_name is not used in GET/POST. ===== Future Scope ===== * Implement timestamp based session management. Session is unreliable without timestamp based management. ===== Proposed Voting Choices ===== Include these so readers know where you are heading and can discuss the proposed voting options. State whether this project requires a 2/3 or 50%+1 majority (see [[voting]]) ===== Patches and Tests ===== TBD ===== Implementation ===== After the project is implemented, this section should contain - the version(s) it was merged to - a link to the git commit(s) - a link to the PHP manual entry for the feature ===== References ===== Links to external references, discussions or RFCs ===== Rejected Features ===== Keep this updated with features that were discussed on the mail lists.