Added CSRF to the application (took in account backwards compatibility)

Mitigated the XSS vulnerabilities reported by HackDefense
Advisories for said vulnerabilities can be found here:
https://hackdefense.com/publications/cve-2019-18345-davical-caldav-server-vulnerability
https://hackdefense.com/publications/cve-2019-18346-davical-caldav-server-vulnerability
https://hackdefense.com/publications/cve-2019-18347-davical-caldav-server-vulnerability
This commit is contained in:
nielsvangijzen 2019-10-28 11:55:11 +01:00
parent 710bc6cccd
commit 86a8ec5302
6 changed files with 195 additions and 2 deletions

View File

@ -1,4 +1,5 @@
<?php
require_once('./always.php');
require_once('classEditor.php');
require_once('classBrowser.php');
@ -25,7 +26,12 @@ require_once('interactive-page.php');
$page_elements = array();
$code_file = sprintf( 'ui/%s-%s.php', $component, $action );
if ( ! @include_once( $code_file ) ) {
$c->messages[] = sprintf('No page found to %s %s%s%s', $action, ($action == 'browse' ? '' : 'a '), $component, ($action == 'browse' ? 's' : ''));
$c->messages[] = sprintf(
'No page found to %s %s%s%s',
htmlspecialchars($action),
($action == 'browse' ? '' : 'a '), $component,
($action == 'browse' ? 's' : '')
);
include('page-header.php');
include('page-footer.php');
@ob_flush(); exit(0);

View File

@ -8,6 +8,47 @@
if ( preg_match('{/always.php$}', $_SERVER['SCRIPT_NAME'] ) ) header('Location: index.php');
// XSS Protection
function filter_post(&$val, $index) {
if(in_array($index, ["newpass1", "newpass2"])) return;
switch (gettype($val)) {
case "string":
$val = htmlspecialchars($val);
break;
case "array":
array_walk_recursive($val, function(&$v) {
if (gettype($v) == "string") {
$v = htmlspecialchars($v);
}
});
break;
}
}
function clean_get() {
$temp = [];
foreach($_GET as $key => $value) {
// XSS is possible in both key and values
$k = htmlspecialchars($key);
$v = htmlspecialchars($value);
$temp[$k] = $v;
}
return $temp;
}
// Before anything else is executed we filter all the user input, a lot of code in this project
// relies on variables that are easily manipulated by the user. These lines and functions filter all those variables.
if(isset($_POST)) array_walk($_POST, 'filter_post');
$_GET = clean_get();
$_SERVER['REQUEST_URI'] = str_replace("&amp;", "&", htmlspecialchars($_SERVER['REQUEST_URI']));
$_SERVER['HTTP_REFERER'] = htmlspecialchars($_SERVER['HTTP_REFERER']);
// Ensure the configuration starts out as an empty object.
$c = (object) array();
$c->script_start_time = microtime(true);

119
inc/csrf_tokens.php Normal file
View File

@ -0,0 +1,119 @@
<?php
/**
* Update the CSRF token
*/
function updateCsrf() {
if(!sessionExists()) {
session_start();
}
$_SESSION['csrf_token'] = generateCsrf();
}
/**
* Check whether a session is currently active
* @return bool
*/
function sessionExists() {
if (version_compare(phpversion(), '5.4.0', '>')) {
return session_id() !== '';
} else {
return session_status() === PHP_SESSION_ACTIVE;
}
}
/**
* Generate a CSRF token, it chooses from 3 different functions based on PHP version and modules
* @return bool|string
*/
function generateCsrf() {
if (version_compare(phpversion(), '7.0.0', '>=')) {
$random = generateRandom();
if($random !== false) return $random;
}
if (function_exists('mcrypt_create_iv')) {
return generateMcrypt();
}
return generateOpenssl();
}
/**
* Generate a random string using the PHP built in function random_bytes
* @version 7.0.0
* @return bool|string
*/
function generateRandom() {
try {
return bin2hex(random_bytes(32));
} catch (Exception $e) {
return false;
}
}
/**
* Generate a random string using MCRYPT
* @return string
*/
function generateMcrypt() {
return bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
}
/**
* Generate a random string using OpenSSL
* @return string
*/
function generateOpenssl() {
return bin2hex(openssl_random_pseudo_bytes(32));
}
/**
* Checks for session and the existence of a key
* after ensuring both are present it returns the
* current CSRF token
* @return string
*/
function getCsrf() {
if(!sessionExists()) {
session_start();
}
if(!array_key_exists('csrf_token', $_SESSION)) {
updateCsrf();
}
return $_SESSION['csrf_token'];
}
/**
* Get a hidden CSRF input field to be used in forms
* @return string
*/
function getCsrfField() {
return sprintf("<input type=\"hidden\" name=\"csrf_token\" value=\"%s\">", getCsrf());
}
/**
* Verify a given CSRF token
* @param $csrf_token
* @return bool
*/
function verifyCsrf($csrf_token) {
$current_csrf = getCsrf();
// Prefer hash_equals over === because the latter is vulnerable to timing attacks
if(function_exists('hash_equals')) {
return hash_equals($current_csrf, $csrf_token);
}
return false;
}
/**
* Uses the global $_POST variable to check if the CSRF token is valid
* @return bool
*/
function verifyCsrfPost() {
return (isset($_POST['csrf_token']) && verifyCsrf($_POST['csrf_token']));
}

View File

@ -20,6 +20,9 @@ if ( isset($_SERVER['SCRIPT_NAME']) ) {
if ( $wiki_help == 'admin' ) {
$wiki_help .= '/' . $_GET['t'] . '/' . $_GET['action'];
}
$wiki_help = htmlspecialchars($wiki_help);
$wiki_help = 'w/Help/'.$wiki_help;
}

View File

@ -1,4 +1,5 @@
<?php
require_once("csrf_tokens.php");
// Editor component for collections
$editor = new Editor(translate('Collection'), 'collection');
@ -273,6 +274,7 @@ EOPRIV;
$submit_row = '';
}
$csrf_field = getCsrfField();
$id = $editor->Value('collection_id');
$template = <<<EOTEMPLATE
##form##
@ -384,6 +386,7 @@ label.privilege {
<tr> <th class="right">$prompt_description:</th> <td class="left">##description.textarea.78x6##</td> </tr>
$submit_row
</table>
$csrf_field
</form>
<script language="javascript">
toggle_enabled('fld_is_calendar','=fld_timezone','=fld_schedule_transp','!fld_is_addressbook');
@ -453,9 +456,11 @@ if ( $editor->Available() ) {
$orig_to_id = $row_data->to_principal;
$form_id = $grantrow->Id();
$form_url = preg_replace( '#&(edit|delete)_grant=\d+#', '', $_SERVER['REQUEST_URI'] );
$csrf_field = getCsrfField();
$template = <<<EOTEMPLATE
<form method="POST" enctype="multipart/form-data" id="form_$form_id" action="$form_url">
$csrf_field
<td class="left" colspan="2"><input type="hidden" name="id" value="$id"><input type="hidden" name="orig_to_id" value="$orig_to_id">##to_principal.select##</td>
<td class="left" colspan="2">
<input type="button" value="$btn_all" class="submit" title="$btn_all_title" onclick="toggle_privileges('grant_privileges', 'all', 'form_$form_id');">

View File

@ -1,4 +1,5 @@
<?php
require_once("csrf_tokens.php");
param_to_global('id', 'int', 'old_id', 'principal_id' );
@ -181,6 +182,13 @@ function principal_editor() {
$editor->AddAttribute( 'email', 'title', translate("The email address identifies principals when processing invitations and freebusy lookups. It should be set to a unique value.") );
$editor->SetWhere( 'principal_id='.$id );
if($_SERVER['REQUEST_METHOD'] === "POST" && !verifyCsrfPost()) {
$c->messages[] = i18n("A valid CSRF token must be provided");
$can_write_principal = false;
}
$csrf_field = getCsrfField();
$editor->AddField('is_admin', 'EXISTS( SELECT 1 FROM role_member WHERE role_no = 1 AND role_member.user_no = dav_principal.user_no )' );
$editor->AddAttribute('is_admin', 'title', translate('An "Administrator" user has full rights to the whole DAViCal System'));
@ -237,7 +245,7 @@ function principal_editor() {
$c->messages[] = i18n("Updating Principal record.");
}
$editor->Write();
if ( $_POST['type_id'] != 3 && $editor->IsCreate() ) {
if ( $_POST['type_id'] != 3 && $editor->IsCreate() ) {
/** We only add the default calendar if it isn't a group, and this is a create action */
require_once('auth-functions.php');
CreateHomeCollections($editor->Value('username'));
@ -396,6 +404,7 @@ label.privilege {
<tr> <th class="right" style="white-space:normal;">$prompt_privileges:</th><td class="left">$privs_html</td> </tr>
$submit_row
</table>
$csrf_field
</form>
EOTEMPLATE;
@ -545,9 +554,11 @@ function edit_group_row( $row_data ) {
global $id, $grouprow;
$form_url = preg_replace( '#&(edit|delete)_group=\d+#', '', $_SERVER['REQUEST_URI'] );
$csrf_field = getCsrfField();
$template = <<<EOTEMPLATE
<form method="POST" enctype="multipart/form-data" id="add_group" action="$form_url">
$csrf_field
<td class="left"><input type="hidden" name="id" value="$id"></td>
<td class="left" colspan="3">##member_id.select## &nbsp; ##Add.submit##</td>
<td class="center"></td>
@ -660,8 +671,11 @@ function edit_grant_row_principal( $row_data ) {
$form_id = $grantrow->Id();
$form_url = preg_replace( '#&(edit|delete)_grant=\d+#', '', $_SERVER['REQUEST_URI'] );
$csrf_field = getCsrfField();
$template = <<<EOTEMPLATE
<form method="POST" enctype="multipart/form-data" id="form_$form_id" action="$form_url">
$csrf_field
<td class="left" colspan="2"><input type="hidden" name="id" value="$id"><input type="hidden" name="orig_to_id" value="$orig_to_id">##to_principal.select##</td>
<td class="left" colspan="2">$privs_html</td>
<td class="center">##submit##</td>
@ -788,9 +802,11 @@ function edit_ticket_row( $row_data ) {
$form_id = $ticketrow->Id();
$ticket_id = $row_data->ticket_id;
$form_url = preg_replace( '#&(edit|delete)_[a-z]+=\d+#', '', $_SERVER['REQUEST_URI'] );
$csrf_field = getCsrfField();
$template = <<<EOTEMPLATE
<form method="POST" enctype="multipart/form-data" id="form_$form_id" action="$form_url">
$csrf_field
<td class="left">$ticket_id<input type="hidden" name="id" value="$id"><input type="hidden" name="ticket_id" value="$ticket_id"></td>
<td class="left"><input type="text" name="target" value="$row_data->target"></td>
<td class="left"><input type="text" name="expires" value="$row_data->expires" size="10"></td>
@ -1011,8 +1027,11 @@ function edit_binding_row( $row_data ) {
$source_title = translate('Path to collection you wish to bind, like /user1/calendar/ or https://cal.example.com/user2/cal/');
$access_title = translate('optional');
$csrf_field = getCsrfField();
$template = <<<EOTEMPLATE
<form method="POST" enctype="multipart/form-data" id="form_$form_id" action="$form_url">
$csrf_field
<td class="left">&nbsp;<input type="hidden" name="id" value="$id"></td>
<td class="left"><input type="text" name="dav_name" value="$row_data->dav_name" size="25"></td>
<td class="left"><input type="text" name="dav_displayname" size="20"></td>