The general approach to CSRF protection in Tiki is to validate the following for requests that change the database or user files on the server ("state-changing actions"):
The POST method is used
The request originates from the site itself
The request includes a ticket (a token) that matches a ticket on the server that is not expired
Other actions that only extract or display data (e.g., view, export, download, print) are not subject to CSRF protection validation and the GET method can be used for these actions.
The approach described above is implemented as follows:
A cryptographically secure pseudo-random sequence of bytes encoded into the base 64 character set (referred to as a ticket) is placed as a hidden input in each form from which state-changing actions can be requested
At the same time this ticket is created and placed into a form, it is stored on the server with a time stamp
Clicking on an element in a form that will trigger a state-changing action will first check that the ticket expiration period hasn't passed, and will popup a warning if it has (if javascript is enabled)
The php code that executes the state-changing request is made to be conditional on the CSRF protection validating successfully
The CSRF validation checks that (1) the request is a POST, (2) the request originates from the site itself, and (3) the ticket matches one stored on the server that is not expired.
The validation returns true if sucessful so that the action can be executed. If not successful, the action will not be performed and an error message will be displayed and environment details are written to the system php error log
The implementation also allows for the following:
Converting state-changing GET requests into POST requests through use of a confirmation form
Ability to generate a confirmation form even for POST requests that already have a ticket, for example, when desired for actions that cannot be undone (e.g., deleting an item)
For select elements, handling each option independently as to whether they require validation or a confirmation form
Ability to use popup confirmation forms even if ajax services are not being used to avoid loading a separate confirmation page, if javascript is enabled
When the bootstrap modal smarty function is used for ajax, includes form inputs into the modal popup so the parameters do not need to be separately included in the smarty function
Preferences
The security preference securityTimeout is used to set the number of seconds after which tickets and related forms expire. The session_lifetime preference is used for the default, if set, otherwise the session.gc_maxlifetime php.ini setting is used, subject to a default maximum of four hours in any case.
The old preferences for CSRF protection, feature_ticketlib and feature_ticketlib2 will be removed once the related check_ticket methods have been replaced throughout Tiki.
Ticket
The smarty ticket function is used to generate tickets for the HTML. This function creates a new ticket assuming a ticket smarty variable has not already been set (e.g., by another form on the same page) and also stores the ticket in the $_SESSION['tickets'] variable with a time stamp. A new ticket is generated for each page load. By default, tickets are deleted from the server once they are matched.
There are three ways the ticket function can be used, all of which are illustrated in the examples below:
{ticket} : returns hidden input HTML with the ticket
{ticket mode=confirm} : In addition to the ticket hidden input, returns the hidden input HTML indicating the submission is a confirmation
{ticket mode=get} : returns the ticket only
Javascript
Onclick methods are used (see examples below) to generate popup confirmation forms.
There is a listener function in tiki-confirm.js that generates a popup timeout warning the first time an input is given or a dropdown changed on any form that has the ticket input element. This way the user can refresh the page before filling out the complete form. The listener will also work for forms in a popover. It does not work for modal popups that have been left up long enough for the the ticket to expire - in this case the user will simply get an error notice once the form in the popup is submitted.
Other notes
Since the CSRF check is set by default to check that the request method is POST, there is less of a need to convert $_REQUEST to $_POST in the Php code where the actions are performed, although it is still better practice to use $_POST or $_GET rather than $_REQUEST.
The examples below cover the more common use cases. The checkCsrf() method within lib/tikiaccesslib.php provides other settings to accommodate other less common use cases.
Examples
These examples assume a smarty template is being used for the HTML.
Standard forms in a page (non-ajax)
These examples assumes ajax services are not being used for the action. They also assume a select element is not being used to submit the form - see example below if a select element is being used to submit the form.
No confirmation of action desired
Location
Implementation
Result
Form HTML
Add {ticket} to the form
Inserts hidden input with the ticket
Php
Secondarily condition the code executing the request on $access->checkCsrf()
Returns boolean depending on success of CSRF check
The jQuery Sortable Tables feature must be activated for the sort feature to work.
Here is an example of conditioning the Php code on the CSRF validition:
Copy to clipboard
<?php
if (! empty($_POST['lock']) && $access->checkCsrf()) {
//perform lock here
}
Important
The CSRF check condition should always be second to avoid unnecessary checks and false errors in case of GET requests intended for other parts of the code.
Confirmation of action desired
Location
Implementation
Result
Form HTML
Add {ticket} to the form
Inserts hidden input with the ticket
Form HTML
Add onclick="confirmPopup('{tr}Delete this item?{/tr}')" to the submit element
Pops up a confirmation form upon click. Ticket expiry is also checked so a warning preventing submitting pops up instead if tickets are expired (if javascript is enabled)
Php
Secondarily condition the code executing the request on $access->checkCsrf(true)
If request is the result of submission of the confirmation form, then a boolean is returned based on the result of the CSRF check. If not (because standard form was clicked and javascript was not enabled), then a redirect to a onfirmation page will occur.
The jQuery Sortable Tables feature must be activated for the sort feature to work.
Here is an example of conditioning the Php code on the CSRF check and submission of the confirmation form:
Copy to clipboard
<?php
if (! empty($_POST['delete']) && $access->checkCsrf(true) {
//perform delete here
}
Confirmation forms
Note that confirmation forms use {ticket mode=confirm} , which adds a hidden input in addition to the ticket. The $access->checkCsrf(true) method determines whether the request is a confirmation or not using this hidden input. If the input is missing, then the method will perform a redirect to a confirmation page. If it is there, the CSRF check will be performed and a boolean returned based on the result. If javascript is enabled and the confirmPopup() method is used, then the confirmation input will be included and there will be no redirection to a confirmation page.
Links
Change to form if possible
If a link is used for a state-changing action and ajax services are not being used, then the first choice is to change the link to a form so that the $_POST method is used rather than the $_GET method, which should not be used for state-changing actions. If the link was part of a popup list of actions (for example, the popups displayed after clicking the icon), the submit element of the form used to replace the link will be styled to look the same as a link if the button element is used and the classes btn btn-link are applied.
The same considerations regarding whether a confirmation is needed would apply as described in the standardized form section above.
Confirm link action
If it is not possible to change the link into a form, then a confirmation popup or page needs to be shown first so that the GET request is converted into a form (the confirm) that uses the POST method and a ticket.
Location
Implementation
Result
Link HTML
Add onclick="confirmPopup('{tr}Delete this item?{/tr}', '{ticket mode=get}')" to the link element
Pops up a confirmation form upon click. Ticket expiry is also checked so a warning preventing submitting pops up instead if tickets are expired (if javascript is enabled)
Php
Secondarily condition the code executing the request on $access->checkCsrf(true)
If request is the result of submission of the confirmation form, then a boolean is returned based on the result of the CSRF check. If not (because standard for was clicked and javascript was not enabled), then a redirect to a onfirmation page will occur.
The jQuery Sortable Tables feature must be activated for the sort feature to work.
Form with select element
For this example we'll assume there are two options in a "Perform action on checked items..." select element for which different treatments are desired:
One option to delete checked items, for which a confirmation is desired since the action cannot be undone
One option to lock checked items, for which no confirmation is desired since the action can easily be undone
Location
Implementation
Result
Form HTML
Add {ticket} to the form
Inserts hidden input with the ticket
Form HTML
Add onclick="confirmPopup()" to the submit element
Pops up a confirmation form upon click only for the options with the class confirm-popup. Ticket expiry is also checked so a warning preventing submitting pops up instead if tickets are expired (if javascript is enabled)
Form HTML
For the option element for which confirmation is desired, add the class confirm-popup and optionally add the confirmation text as a data attribute, e.g., data-confirm-text="{tr}Delete items?{/tr}"
Will cause a confirmation form to pop up when this option is selected and the select element is clicked
Php
For the delete action requiring confirmation, secondarily condition the code executing the request on $access->checkCsrf(true)
If request is the result of submission of the confirmation form, then a boolean is returned based on the result of the CSRF check. If not (because standard form was clicked and javascript was not enabled), then a redirect to a onfirmation page will occur.
Php
For the lock action not requiring a confirmation, secondarily condition the code executing the request on $access->checkCsrf()
Returns boolean depending on success of CSRF validation
The jQuery Sortable Tables feature must be activated for the sort feature to work.
Ajax services
When ajax services are used to perform the action, two things can be different than a non-ajax submission:
There are often two passes through the method that performs the action: the first that brings up a form, and the second that performs the action after the form is submitted
A Services_Utilities class should be used to carry out the CSRF checks. The methods referred to below are from this class.
Requiring two passes and the action code is first
Location
Implementation
Result
Original form HTML
Add {ticket} to the form
Inserts hidden input with the ticket
Original form HTML
Add onclick="confirmPopup()" to the submit element
Pops up the modal form specified in the ajax service Php code - including this method will include all inputs in the form. The method is not needed if the form inputs are not needed because all necessary parameters are set in the bootstrap modal smarty function
Php
Condition the code executing the request on isConfirmPost() from the Services_Utilities class
If request is the result of submission of the modal form, then a boolean is returned based on the result of the CSRF check. If not (because the original form was clicked and this is the first pass), then the code will be skipped and the code generating the modal form will be executed
Modal form HTML
Add {ticket mode=confirm} to the form
Inserts two hidden inputs: one with the ticket and the other with a hidden input named confirmForm with a value of y
The jQuery Sortable Tables feature must be activated for the sort feature to work.
Here is an example of the above:
Copy to clipboard
<?php
$util = new Services_Utilities();
if ($util->isConfirmPost()) {
//the isConfirmPost method also validates for CSRF if the post is a confirm submission
//perform action here
} else {
//render modal form here
}
Requiring two passes and the code to render the modal form is first
The HTML is the same as the prior example. For the Php, use the notConfirmPost() method and bring up form if true, else use checkCsrf() (from the Services_Utilities class) before performing the action. Below is an example:
Copy to clipboard
<?php
$util = new Services_Utilities();
if ($util->notConfirmPost()) {
//render modal form here
} elseif ($util->checkCsrf()) {
//perform action here
}
Requiring one pass (no modal form involved)
Location
Implementation
Result
Original form HTML
Add {ticket} to the form
Inserts hidden input with the ticket
Original form HTML
Add onclick="confirmPopup()" to the submit element
Pops up the modal form specified in the ajax service Php code.
Php
Condition the code executing the request on checkCsrf() from the Services_Utilities class
Returns boolean depending on success of CSRF check
The jQuery Sortable Tables feature must be activated for the sort feature to work.
See also ((CSRF wishes))
! General Approach
The general approach to CSRF protection in Tiki is to validate the following for requests that change the database or user files on the server ("state-changing actions"):
* The POST method is used
* The request originates from the site itself
* The request includes a ticket (a token) that matches a ticket on the server that is not expired
Other actions that only extract or display data (''e.g.'', view, export, download, print) are not subject to CSRF protection validation and the GET method can be used for these actions.
This approach is in line with [https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet|OWASP recommendations].
! Implementation
!! Overview
The approach described above is implemented as follows:
* A cryptographically secure pseudo-random sequence of bytes encoded into the base 64 character set (referred to as a __ticket__) is placed as a hidden input in each form from which state-changing actions can be requested
* At the same time this ticket is created and placed into a form, it is stored on the server with a time stamp
* Clicking on an element in a form that will trigger a state-changing action will first check that the ticket expiration period hasn't passed, and will popup a warning if it has (if javascript is enabled)
* The php code that executes the state-changing request is made to be conditional on the CSRF protection validating successfully
* The CSRF validation checks that (1) the request is a POST, (2) the request originates from the site itself, and (3) the ticket matches one stored on the server that is not expired.
* The validation returns true if sucessful so that the action can be executed. If not successful, the action will not be performed and an error message will be displayed and environment details are written to the system php error log
The implementation also allows for the following:
* Converting state-changing GET requests into POST requests through use of a confirmation form
* Ability to generate a confirmation form even for POST requests that already have a ticket, for example, when desired for actions that cannot be undone (''e.g.'', deleting an item)
* For select elements, handling each option independently as to whether they require validation or a confirmation form
* Ability to use popup confirmation forms even if ajax services are not being used to avoid loading a separate confirmation page, if javascript is enabled
* When the bootstrap modal smarty function is used for ajax, includes form inputs into the modal popup so the parameters do not need to be separately included in the smarty function
!! Preferences
The security preference -+securityTimeout+- is used to set the number of seconds after which tickets and related forms expire. The -+session_lifetime+- preference is used for the default, if set, otherwise the -+session.gc_maxlifetime+- php.ini setting is used, subject to a default maximum of four hours in any case.
The old preferences for CSRF protection, -+feature_ticketlib+- and -+feature_ticketlib2+- will be removed once the related -+check_ticket+- methods have been replaced throughout Tiki.
!! Ticket
The smarty ticket function is used to generate tickets for the HTML. This function creates a new ticket assuming a ticket smarty variable has not already been set (''e.g.'', by another form on the same page) and also stores the ticket in the -+~np~$_SESSION['tickets']~/np~+- variable with a time stamp. A new ticket is generated for each page load. By default, tickets are deleted from the server once they are matched.
There are three ways the ticket function can be used, all of which are illustrated in the examples below:
* -+~np~{ticket}~/np~+- : returns hidden input HTML with the ticket
* -+~np~{ticket mode=confirm}~/np~+- : In addition to the ticket hidden input, returns the hidden input HTML indicating the submission is a confirmation
* -+~np~{ticket mode=get}~/np~+- : returns the ticket only
!! Javascript
~np~Onclick~/np~ methods are used (see examples below) to generate popup confirmation forms.
There is a listener function in -+tiki-confirm.js+- that generates a popup timeout warning the first time an input is given or a dropdown changed on any form that has the ticket input element. This way the user can refresh the page before filling out the complete form. The listener will also work for forms in a popover. It does not work for modal popups that have been left up long enough for the the ticket to expire - in this case the user will simply get an error notice once the form in the popup is submitted.
!! Other notes
Since the CSRF check is set by default to check that the request method is POST, there is less of a need to convert -+$_REQUEST+- to -+$_POST+- in the Php code where the actions are performed, although it is still better practice to use -+$_POST+- or -+$_GET+- rather than -+$_REQUEST+-.
The examples below cover the more common use cases. The -+checkCsrf()+- method within -+lib/tikiaccesslib.php+- provides other settings to accommodate other less common use cases.
!! Examples
These examples assume a smarty template is being used for the HTML.
!!! Standard forms in a page (non-ajax)
These examples assumes ajax services are not being used for the action. They also assume a select element is not being used to submit the form - see example below if a select element is being used to submit the form.
!!!! No confirmation of action desired
{FANCYTABLE(head="Location|Implementation|Result")}
Form HTML|Add -+~np~{ticket}~/np~+- to the form | Inserts hidden input with the ticket
Php|Secondarily condition the code executing the request on -+~np~$access->checkCsrf()~/np~+- | Returns boolean depending on success of CSRF check
{FANCYTABLE}
Here is an example of conditioning the Php code on the CSRF validition:
{CODE(colors="php" theme="default")}<?php
if (! empty($_POST['lock']) && $access->checkCsrf()) {
//perform lock here
}
{CODE}
{REMARKSBOX(type="warning" title="Important")}The CSRF check condition should always be second to avoid unnecessary checks and false errors in case of GET requests intended for other parts of the code.{REMARKSBOX}
!!!! Confirmation of action desired
{FANCYTABLE(head="Location|Implementation|Result")}
Form HTML|Add -+~np~{ticket}~/np~+- to the form | Inserts hidden input with the ticket
Form HTML|Add -+~np~onclick="confirmPopup('{tr}Delete this item?{/tr}')"~/np~+- to the submit element | Pops up a confirmation form upon click. Ticket expiry is also checked so a warning preventing submitting pops up instead if tickets are expired (if javascript is enabled)
Php|Secondarily condition the code executing the request on -+~np~$access->checkCsrf(true)~/np~+- | If request is the result of submission of the confirmation form, then a boolean is returned based on the result of the CSRF check. If not (because standard form was clicked and javascript was not enabled), then a redirect to a onfirmation page will occur.
{FANCYTABLE}
Here is an example of conditioning the Php code on the CSRF check and submission of the confirmation form:
{CODE(colors="php" theme="default")}<?php
if (! empty($_POST['delete']) && $access->checkCsrf(true) {
//perform delete here
}
{CODE}
!!!!! Confirmation forms
Note that confirmation forms use -+~np~{ticket mode=confirm}~/np~+- , which adds a hidden input in addition to the ticket. The -+~np~$access->checkCsrf(true)~/np~+- method determines whether the request is a confirmation or not using this hidden input. If the input is missing, then the method will perform a redirect to a confirmation page. If it is there, the CSRF check will be performed and a boolean returned based on the result. If javascript is enabled and the -+~np~confirmPopup()~/np~+- method is used, then the confirmation input will be included and there will be no redirection to a confirmation page.
!!! Links
!!!! Change to form if possible
If a link is used for a state-changing action and ajax services are not being used, then the first choice is to change the link to a form so that the -+$_POST+- method is used rather than the -+$_GET+- method, which should not be used for state-changing actions. If the link was part of a popup list of actions (for example, the popups displayed after clicking the {icon name="settings"} icon), the submit element of the form used to replace the link will be styled to look the same as a link if the button element is used and the classes -+btn btn-link+- are applied.
For example, this link:
{CODE(colors=smarty)}
<a href="tiki-admin_feature.php?lock=1">
{icon name="lock" _menu_text='y' _menu_icon='y' alt="{tr}Lock{/tr}"}
</a>
{CODE}
Should be changed to this form:
{CODE(colors="smarty" theme="default")}<form action="tiki-admin_feature.php" method="post">
{ticket}
<button type="submit" name="lock" value="1" class="btn btn-link">
{icon name="lock"} {tr}Lock{/tr}
</button>
</form>
{CODE}
The same considerations regarding whether a confirmation is needed would apply as described in the [#Standard_forms_in_a_page|standardized form section] above.
!!!! Confirm link action
If it is not possible to change the link into a form, then a confirmation popup or page needs to be shown first so that the GET request is converted into a form (the confirm) that uses the POST method and a ticket.
{FANCYTABLE(head="Location|Implementation|Result")}
Link HTML|Add -+~np~onclick="confirmPopup('{tr}Delete this item?{/tr}', '{ticket mode=get}')"~/np~+- to the link element | Pops up a confirmation form upon click. Ticket expiry is also checked so a warning preventing submitting pops up instead if tickets are expired (if javascript is enabled)
Php|Secondarily condition the code executing the request on -+~np~$access->checkCsrf(true)~/np~+- | If request is the result of submission of the confirmation form, then a boolean is returned based on the result of the CSRF check. If not (because standard for was clicked and javascript was not enabled), then a redirect to a onfirmation page will occur.
{FANCYTABLE}
!!! Form with select element
For this example we'll assume there are two options in a "Perform action on checked items..." select element for which different treatments are desired:
* One option to delete checked items, for which a confirmation is desired since the action cannot be undone
* One option to lock checked items, for which no confirmation is desired since the action can easily be undone
{FANCYTABLE(head="Location|Implementation|Result")}
Form HTML|Add -+~np~{ticket}~/np~+- to the form | Inserts hidden input with the ticket
Form HTML|Add -+~np~onclick="confirmPopup()"~/np~+- to the submit element | Pops up a confirmation form upon click only for the options with the class -+~np~confirm-popup~/np~+-. Ticket expiry is also checked so a warning preventing submitting pops up instead if tickets are expired (if javascript is enabled)
Form HTML|For the option element for which confirmation is desired, add the class -+~np~confirm-popup~/np~+- and optionally add the confirmation text as a data attribute, e.g., -+~np~data-confirm-text="{tr}Delete items?{/tr}"~/np~+-|Will cause a confirmation form to pop up when this option is selected and the select element is clicked
Php|For the delete action requiring confirmation, secondarily condition the code executing the request on -+~np~$access->checkCsrf(true)~/np~+- | If request is the result of submission of the confirmation form, then a boolean is returned based on the result of the CSRF check. If not (because standard form was clicked and javascript was not enabled), then a redirect to a onfirmation page will occur.
Php|For the lock action not requiring a confirmation, secondarily condition the code executing the request on -+~np~$access->checkCsrf()~/np~+- | Returns boolean depending on success of CSRF validation
{FANCYTABLE}
!!! Ajax services
When ajax services are used to perform the action, two things can be different than a non-ajax submission:
* There are often two passes through the method that performs the action: the first that brings up a form, and the second that performs the action after the form is submitted
* A -+Services_Utilities+- class should be used to carry out the CSRF checks. The methods referred to below are from this class.
!!!! Requiring two passes and the action code is first
{FANCYTABLE(head="Location|Implementation|Result")}
Original form HTML|Add -+~np~{ticket}~/np~+- to the form | Inserts hidden input with the ticket
Original form HTML|Add -+~np~onclick="confirmPopup()"~/np~+- to the submit element | Pops up the modal form specified in the ajax service Php code - including this method will include all inputs in the form. The method is not needed if the form inputs are not needed because all necessary parameters are set in the bootstrap modal smarty function
Php| Condition the code executing the request on -+~np~isConfirmPost()~/np~+- from the -+Services_Utilities+- class | If request is the result of submission of the modal form, then a boolean is returned based on the result of the CSRF check. If not (because the original form was clicked and this is the first pass), then the code will be skipped and the code generating the modal form will be executed
Modal form HTML|Add -+~np~{ticket mode=confirm}~/np~+- to the form | Inserts two hidden inputs: one with the ticket and the other with a hidden input named -+confirmForm+- with a value of -+y+-
{FANCYTABLE}
Here is an example of the above:
{CODE(colors="php")}<?php
$util = new Services_Utilities();
if ($util->isConfirmPost()) {
//the isConfirmPost method also validates for CSRF if the post is a confirm submission
//perform action here
} else {
//render modal form here
}
{CODE}
!!!! Requiring two passes and the code to render the modal form is first
The HTML is the same as the prior example. For the Php, use the -+notConfirmPost()+- method and bring up form if true, else use -+checkCsrf()+- (from the -+Services_Utilities+- class) before performing the action. Below is an example:
{CODE(colors="php")}<?php
$util = new Services_Utilities();
if ($util->notConfirmPost()) {
//render modal form here
} elseif ($util->checkCsrf()) {
//perform action here
}
{CODE}
!!!! Requiring one pass (no modal form involved)
{FANCYTABLE(head="Location|Implementation|Result")}
Original form HTML|Add -+~np~{ticket}~/np~+- to the form | Inserts hidden input with the ticket
Original form HTML|Add -+~np~onclick="confirmPopup()"~/np~+- to the submit element | Pops up the modal form specified in the ajax service Php code.
Php| Condition the code executing the request on -+~np~checkCsrf()~/np~+- from the -+Services_Utilities+- class | Returns boolean depending on success of CSRF check
{FANCYTABLE}
The following is a list of keywords that should serve as hubs for navigation within the Tiki development and should correspond to documentation keywords.
Each feature in Tiki has a wiki page which regroups all the bugs, requests for enhancements, etc. It is somewhat a form of wiki-based project management. You can also express your interest in a feature by adding it to your profile. You can also try out the Dynamic filter.