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 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 $_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 |
Here is an example of conditioning the Php code on the CSRF validition:
<?php if (! empty($_POST['lock']) && $access->checkCsrf()) { //perform lock here }
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. |
Here is an example of conditioning the Php code on the CSRF check and submission of the confirmation form:
<?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.
For example, this link:
<a href="tiki-admin_feature.php?lock=1"> {icon name="lock" _menu_text='y' _menu_icon='y' alt="{tr}Lock{/tr}"} </a>
Should be changed to this form:
<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>
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. |
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 |
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 |
Here is an example of the above:
<?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:
<?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 |