rfc:automatic_csrf_protection

PHP RFC: Semi-Automatic CSRF Protection

Introduction

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

<form action="http://example.com/edit.php" method="POST">
<textarea name="comment"></textarea>
<input type="submit" name="submit" />
</form>

can be protected by one line. 1)

<?php session_start(['csrf_rewrite'=>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

<?php session_start('csrf_rewrite'=>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 2) 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

<?php
// Rewrite and Validation is disabled by default, but explicitly disabled here.
session_start(['csrf_rewrite'=>SESSION_CSRF_NONE, 'csrf_validate'=>SESSION_CSRF_NONE]);
?>
// GET
<a href="http://example.com/delete.php?id=1234&<?php echo session_csrf_token(SESSION_CSRF_GET);?>">Delete ID:1234</a>
// POST - put this inside form tag
<?php echo session_csrf_token(SESSION_CSRF_POST);?>

then, validate manually

<?php
// Rewrite and Validation is disabled by default, but explicitly disabled here.
session_start(['csrf_rewrite'=>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.

<?php
// Disable automatic rewrite and validation explicitly - they are disabled by default
session_start(['csrf_rewrite'=>SESSION_CSRF_NONE, 'csrf_validate'=SESSION_CSRF_NONE]);
?>
<html>
<head></head>
<body>
<a href="http://example.com/delete.php?id=1234&<?php echo session_csrf_token(SESSION_CSRF_GET);?>">Delete ID:1234</a>
 
<a href="http://example.com/show.php?id=1234">Show ID:1234</a>
 
<form action="http://example.com/edit.php" method="POST">
<textarea name="comment"></textarea>
<?php echo session_csrf_token(SESSION_CSRF_POST);?>
<input type="submit" />
</form>
 
</body>
</html>

Output to browser will be something like

<html>
<head></head>
<body>
<a href="http://example.com/delete.php?id=1234&SESSCSRF=1462920523-5fd057a6ff9dc7a124fa5c814765a498e5aa024a">Remove ID:1234</a>
 
<a href="http://example.com/show.php?id=1234">Show ID:1234</a>
 
<form action="http://example.com/edit.php" method="POST">
<textarea name="comment"></textarea>
<input type="hidden" name="SESSCSRF" value="1462920523-5fd057a6ff9dc7a124fa5c814765a498e5aa024a" />
<input type="submit" />
</form>
 
</body>
</html>

delete.php

<?php
// Disable automatic rewrite ('csrf_rewrite'=>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

<?php
// Disable automatic rewrite and validation explicitly - they are disabled by default
session_start(['csrf_rewrite'=>SESSION_CSRF_NONE, 'csrf_validate'=SESSION_CSRF_NONE]);
// No CSRF validation token for this
// Show data
?>

edit.php

<?php
// Disable automatic rewrite and validation explicitly - they are disabled by default
session_start(['csrf_rewrite'=>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

<a href="http://example.com/delete.php?id=1234">Remove ID:1234</a>

to

<a href="http://example.com/delete.php?id=1234&SESSCSRF=1462920523-5fd057a6ff9dc7a124fa5c814765a498e5aa024a">Remove ID:1234</a>

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

<form action="http://example.com/edit.php" method="POST">
<textarea name="comment"></textarea>
<input type="submit" />
</form>

to

<form action="http://example.com/edit.php" method="POST">
<textarea name="comment"></textarea>
<input type="hidden" name="SESSCSRF" value="1462920523-5fd057a6ff9dc7a124fa5c814765a498e5aa024a" />
<input type="submit" />
</form>

NOTE: Something like “<input type=“hidden” name=“SESSCSRF” value=“1462920523-5fd057a6ff9dc7a124fa5c814765a498e5aa024a” />” 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.

  1. 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.
  2. Add session.csrf_token_name(string) INI.
    CSRF token name. Default “SESSCSRF”.
  3. 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
  4. 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.
  5. 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.
  6. 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
  7. Add session.csrf_domains(string) INI.
    Controls trusted domains default to “”. When empty, HTTP_HOST is used. Default: empty
  8. 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”.
  9. Add session.csrf_error(int - error level) INI.
    Controls which error is raised for CSRF token validation error. Default: E_RECOVERABLE_ERROR
  10. Extend session_start()
    Support csrf_rewrite, csrf_validate, csrf_ttl, csrf_domains, csrf_error options.
  11. 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
  12. Add int session_csrf_validate(array $input_to_validate) function
    It validates CSRF token manually, $_GET/$_POST or whatever array variable contains CSRF token.
  13. 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. '<input type=“hidden” name=“'.ini_get('session.csrf_token_name').'” value=“'. session_csrf_token().'” />';

Behaviors

Page Generation
  1. Generate random CSRF token key (csrf_token_key) if it does not exist in __PHP_SESSION__ array.
  2. 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.
  3. 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

<?php
header("Content-Type: application/json; charset=utf-8");
session_start(['csrf_protection'=>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
  1. Validation is performed according to session.csrf_validate setting.
  2. Split SESSCSRF token value by “-”
    1. Check CSRF validation mode (i.e. SESSION_CSRF_POST/GET).
    2. Check TTL value part > time(). If expired, raise error.
    3. Check CSRF token part === hash_hmac('sha256', ttl_value , csrf_token_key). If does not match, raise error.
  3. 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. 3)

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

  1. Any names (functions/constants/etc) are subject to be changed. Please comment.
  2. How it works is subject to be changed. Please comment.
  3. 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

  1. the version(s) it was merged to
  2. a link to the git commit(s)
  3. 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.

1)
INI settings can be used to set protection options.
2)
Error level is configurable
3)
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.
rfc/automatic_csrf_protection.txt · Last modified: 2021/03/27 14:54 by ilutov