Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add page for viewing slow/error queries to admin area.
Fix CSRF vulnerability in admin/pageDeleteGroup.php, admin/pageDeleteMember.php and admin/pageDeleteRecord.php
  • Loading branch information
Ahmad Gneady committed Jun 28, 2021
1 parent 2301934 commit e822cb1
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 509 deletions.
2 changes: 2 additions & 0 deletions app/admin/pageDeleteGroup.php
Expand Up @@ -5,6 +5,8 @@
// validate input
$groupID = intval($_GET['groupID']);

if(!csrf_token(true)) die($Translation['csrf token expired or invalid']);

// make sure group has no members
if(sqlValue("select count(1) from membership_users where groupID='{$groupID}'")) {
errorMsg($Translation["can not delete group remove members"]);
Expand Down
2 changes: 2 additions & 0 deletions app/admin/pageDeleteMember.php
Expand Up @@ -5,6 +5,8 @@
// validate input
$memberID=makeSafe(strtolower($_GET['memberID']));

if(!csrf_token(true)) die($Translation['csrf token expired or invalid']);

sql("delete from membership_users where lcase(memberID)='$memberID'", $eo);
sql("update membership_userrecords set memberID='' where lcase(memberID)='$memberID'", $eo);

Expand Down
2 changes: 2 additions & 0 deletions app/admin/pageDeleteRecord.php
Expand Up @@ -5,6 +5,8 @@
// validate input
$recID = intval($_GET['recID']);

if(!csrf_token(true)) die($Translation['csrf token expired or invalid']);

$res = sql("SELECT `tableName`, `pkValue` FROM `membership_userrecords` WHERE `recID`='{$recID}'", $eo);
if($row = db_fetch_row($res)) {
sql("DELETE FROM `membership_userrecords` WHERE `recID`='{$recID}'", $eo);
Expand Down
177 changes: 177 additions & 0 deletions app/admin/pageQueryLogs.php
@@ -0,0 +1,177 @@
<?php
require(__DIR__ . "/incCommon.php");

// Retrieve request (type, page)
$queryTypes = [
'slow' => 'COALESCE(`duration`, 0) > 0',
'error' => 'CHAR_LENGTH(COALESCE(`error`, \'\')) > 0',
];

$type = $_REQUEST['type'];
if(!in_array($type, array_keys($queryTypes))) $type = 'slow';

$page = intval($_REQUEST['page']);
if($page < 1) $page = 1;

// Starting record from $page (page is 1-based, while firstRecord is 0-based)
$recordsPerPage = config('adminConfig')['recordsPerPage'];
// $firstRecord is calculated below after retrieving $lastPage

// set up log table if necessary
if(!createQueryLogTable())
dieErrorPage($Translation['Query log table does not exist']);

// only keep queries that are at most 2 months old
$eo = ['silentErrors' => true];
sql("DELETE FROM `appgini_query_log` WHERE `datetime` < DATE_SUB(NOW(), INTERVAL 2 MONTH)", $eo);

// build the WHERE clause
$where = "WHERE {$queryTypes[$type]}";
/* future work: allow filtering by user, date period, ... etc, and allow sorting */

// how many records and pages in total do we have?
$totalRecords = sqlValue("SELECT COUNT(1) FROM `appgini_query_log` $where");
$lastPage = ceil($totalRecords / $recordsPerPage);

// adjust $page and $firstRecord if needed
$page = max(1, min($page, $lastPage));
$firstRecord = ($page - 1) * $recordsPerPage;

// get app uri to strip from stored uri
$appURI = config('appURI');

// retrieve requested logs
$records = [];
$res = sql("SELECT * FROM `appgini_query_log`
$where
ORDER BY `datetime` DESC
LIMIT $firstRecord, $recordsPerPage", $eo);
while($row = db_fetch_assoc($res)) {
if($appURI) $row['uri'] = ltrim(str_replace($appURI, '', $row['uri']), '/');
$records[] = $row;
}

include(__DIR__ . "/incHeader.php");
?>

<!-- Page content -->
<div class="page-header"><h1><?php echo $Translation['Query logs']; ?></h1></div>

<div class="btn-group">
<button type="button" class="btn btn-warning btn-lg query-type slow"><?php echo $Translation['slow queries']; ?></button>
<button type="button" class="btn btn-danger btn-lg query-type error"><?php echo $Translation['error queries']; ?></button>
</div>

<hr>

<div class="alert alert-success hidden" id="noMatches">
<i class="glyphicon glyphicon-ok"></i>
<?php echo $Translation['no matching results found']; ?>
</div>

<div class="table-responsive">
<table class="table table-striped table-hover table-bordered" id="queryLogs">
<thead>
<tr>
<th><?php echo $Translation['date/time']; ?> <i class="glyphicon glyphicon-sort-by-attributes-alt"></i> </th>
<th><?php echo $Translation['username']; ?></th>
<th><?php echo $Translation['page address']; ?></th>
<th><?php echo $Translation['query']; ?></th>
</tr>
</thead>

<tbody>
<?php foreach($records as $rec) { ?>
<tr>
<td><?php echo app_datetime($rec['datetime'], 'dt'); ?></td>
<td><?php echo $rec['memberID']; ?></td>
<td><?php echo $rec['uri']; ?></td>
<td>
<pre><?php echo htmlspecialchars($rec['statement']); ?></pre>
<div class="error text-danger text-bold"><?php echo htmlspecialchars($rec['error']); ?></div>
<div class="duration text-warning text-bold">
<?php echo $Translation['duration (sec)']; ?>:
<?php echo $rec['duration']; ?>
</div>
</td>
</tr>
<?php } ?>
</tbody>

<tfoot>
<tr class="records-pagination">
<th colspan="4">
<div class="row">
<div class="col-xs-4 text-left">
<button type="button" class="btn btn-default btn-previous"><i class="glyphicon glyphicon-chevron-left"></i> <?php echo $Translation['previous']; ?></button>
</div>
<div class="col-xs-4 text-center">
<?php echo str_replace(
['#', '<x>', '<y>'],
[$totalRecords, $page, $lastPage],
$Translation['total # queries'] . ' ' . $Translation['page x of y']
); ?>
</div>
<div class="col-xs-4 text-right">
<button type="button" class="btn btn-default btn-next"><?php echo $Translation['next']; ?> <i class="glyphicon glyphicon-chevron-right"></i></button>
</div>
</div>
</th>
</tr>
</tfoot>
</table>
</div>

<style>
#queryLogs tr > td:first-child { min-width: 10em; }
#queryLogs pre {
white-space: pre-wrap;
max-height: 8em;
overflow-y: auto;
}
</style>

<script>
$j(function() {
var type = '<?php echo $type; ?>';
var page = <?php echo $page; ?>, lastPage = <?php echo $lastPage; ?>;

// set active query-type button based on type
$j('.query-type.' + type).addClass('active text-bold');

// hide table and show no matches if 0 records
var noMatches = !$j('#queryLogs > tbody > tr').length;
$j('#queryLogs').toggleClass('hidden', noMatches);
$j('#noMatches').toggleClass('hidden', !noMatches);

// hide non relevent parts based on type
$j('#queryLogs .duration').toggleClass('hidden', type == 'error');
$j('#queryLogs .error').toggleClass('hidden', type == 'slow');

// handle clicking query-type buttons
$j('.query-type:not(.active)').click(function() {
location.href = 'pageQueryLogs.php?type=' + (type == 'slow' ? 'error' : 'slow');
})

// toogle next/previous links
var prevPage = page > 1 ? page - 1 : 1;
var nextPage = page < lastPage ? page + 1 : lastPage;
$j('.btn-previous').toggleClass('hidden', page == 1);
$j('.btn-next').toggleClass('hidden', page == lastPage);

// clone pagination on top of table
$j('tr.records-pagination').clone().prependTo('#queryLogs thead');

// handle clicking prev link
$j('.btn-previous').on('click', function() {
location.href = 'pageQueryLogs.php?type=' + type + '&page=' + prevPage;
})

// handle clicking next link
$j('.btn-next').on('click', function() {
location.href = 'pageQueryLogs.php?type=' + type + '&page=' + nextPage;
})
})
</script>

<?php include(__DIR__ . "/incFooter.php");
2 changes: 1 addition & 1 deletion app/admin/pageViewGroups.php
Expand Up @@ -80,7 +80,7 @@
<td class="text-center">
<a href="pageEditGroup.php?groupID=<?php echo $row[0]; ?>" title="<?php echo $Translation['Edit group']; ?>"><i class="glyphicon glyphicon-pencil"></i></a>
<?php if(!$groupMembersCount) { ?>
<a href="pageDeleteGroup.php?groupID=<?php echo $row[0]; ?>"
<a href="pageDeleteGroup.php?groupID=<?php echo $row[0]; ?>&csrf_token=<?php echo urlencode(csrf_token(false, true)); ?>"
title="<?php echo $Translation['delete group'] ; ?>"
onClick="return confirm('<?php echo addslashes($Translation['confirm delete group']); ?>');">
<i class="glyphicon glyphicon-trash text-danger"></i>
Expand Down
2 changes: 1 addition & 1 deletion app/admin/pageViewMembers.php
Expand Up @@ -180,7 +180,7 @@
<i class="glyphicon glyphicon-trash text-muted"></i>
<i class="glyphicon glyphicon-ban-circle text-muted"></i>
<?php } else { ?>
<a href="pageDeleteMember.php?memberID=<?php echo $row[0]; ?>" onClick="return confirm('<?php echo addslashes(str_replace('<USERNAME>', $row[0], $Translation['sure delete user'])); ?>');"><i class="glyphicon glyphicon-trash text-danger" title="<?php echo $Translation['delete member'] ; ?>"></i></a>
<a href="pageDeleteMember.php?memberID=<?php echo $row[0]; ?>&csrf_token=<?php echo urlencode(csrf_token(false, true)); ?>" onClick="return confirm('<?php echo addslashes(str_replace('<USERNAME>', $row[0], $Translation['sure delete user'])); ?>');"><i class="glyphicon glyphicon-trash text-danger" title="<?php echo $Translation['delete member'] ; ?>"></i></a>
<?php
if(!$row[8]) { // if member is not approved, display approve link
?><a href="pageChangeMemberStatus.php?memberID=<?php echo $row[0]; ?>&approve=1"><i class="glyphicon glyphicon-ok text-success" title="<?php echo $Translation["unban this member"] ; ?>" title="<?php echo $Translation["approve this member"] ; ?>"></i></a><?php
Expand Down
2 changes: 1 addition & 1 deletion app/admin/pageViewRecords.php
Expand Up @@ -120,7 +120,7 @@
<tr>
<td class="text-center">
<a href="pageEditOwnership.php?recID=<?php echo $row[0]; ?>" title="<?php echo $Translation['change record ownership'] ; ?>"><i class="glyphicon glyphicon-user"></i></a>
<a href="pageDeleteRecord.php?recID=<?php echo $row[0]; ?>" onClick="return confirm('<?php echo addslashes($Translation['sure delete record']); ?>');" title="<?php echo $Translation['delete record']; ?>"><i class="glyphicon glyphicon-trash text-danger"></i></a>
<a href="pageDeleteRecord.php?recID=<?php echo $row[0]; ?>&csrf_token=<?php echo urlencode(csrf_token(false, true)); ?>" onClick="return confirm('<?php echo addslashes($Translation['sure delete record']); ?>');" title="<?php echo $Translation['delete record']; ?>"><i class="glyphicon glyphicon-trash text-danger"></i></a>
</td>
<td><?php echo $row[1]; ?></td>
<td><?php echo $row[2]; ?></td>
Expand Down

0 comments on commit e822cb1

Please sign in to comment.