Merge pull request #6959 from JediKev/security/latest-patches-06-2026

Reviewed-By: JediKev <kevin@enhancesoft.com>
This commit is contained in:
JediKev
2026-06-17 16:48:29 -05:00
committed by GitHub
32 changed files with 368 additions and 66 deletions

View File

@@ -67,8 +67,9 @@ if($thisclient && $thisclient->getId() && $thisclient->isValid()){
}
/******* CSRF Protectin *************/
// Enforce CSRF protection for POSTS
if ($_SERVER['REQUEST_METHOD'] == 'POST' && !$ost->checkCSRFToken()) {
// Enforce CSRF protection for state-changing methods
if (in_array($_SERVER['REQUEST_METHOD'], ['POST', 'PUT', 'PATCH', 'DELETE'])
&& !$ost->checkCSRFToken()) {
Http::redirect('index.php');
//just incase redirect fails
die('Action denied (400)!');

View File

@@ -45,7 +45,7 @@ if ($cfg->isAuthRequiredForFiles()
) {
// Try and determine if an agent is viewing the page / file
if (strpos($_SERVER['HTTP_REFERRER'], ROOT_PATH . 'scp/') !== false) {
if (strpos($_SERVER['HTTP_REFERER'], ROOT_PATH . 'scp/') !== false) {
$_SESSION['_staff']['auth']['dest'] = Http::refresh_url();
Http::redirect(ROOT_PATH.'scp/login.php');
} else {
@@ -58,7 +58,7 @@ if ($cfg->isAuthRequiredForFiles()
// and the user has access to the parent ticket!!
if ($file->verifySignature($_GET['signature'], $_GET['expires'])) {
try {
if (($s = @$_GET['s']) && strpos($file->getType(), 'image/') === 0)
if (($s = @$_GET['s']) && $file->isInlineSafeImage())
return $file->display($s);
// Download the file..
@@ -67,7 +67,8 @@ if ($file->verifySignature($_GET['signature'], $_GET['expires'])) {
$file->download($filename, $disposition, @$_GET['expires']);
}
catch (Exception $ex) {
Http::response(500, 'Unable to find that file: '.$ex->getMessage());
$ost->logError(__('File download error'), $ex->getMessage());
Http::response(500, __('Unable to retrieve the requested file'));
}
}
// else

View File

@@ -3,6 +3,7 @@
if(!defined('INCLUDE_DIR')) die('!');
require_once(INCLUDE_DIR.'class.draft.php');
require_once(INCLUDE_DIR.'class.file.php');
class DraftAjaxAPI extends AjaxController {
@@ -84,7 +85,7 @@ class DraftAjaxAPI extends AjaxController {
// Check file type to ensure image
$type = $file[0]['type'];
if (strpos($file[0]['type'], 'image/') !== 0)
if (!AttachmentFile::isInlineSafeImageType($file[0]['type']))
return Http::response(403,
JsonDataEncoder::encode(array(
'error' => 'File type is not allowed',
@@ -367,6 +368,9 @@ class DraftAjaxAPI extends AjaxController {
$files = array();
foreach ($images as $f) {
if (!$f->isInlineSafeImage())
continue;
$url = $f->getDownloadUrl();
$files[] = array(
// Don't send special sizing for thread items 'cause they

View File

@@ -13,6 +13,9 @@ class NoteAjaxAPI extends AjaxController {
Http::response(403, "Login required");
elseif (!($note = QuickNote::lookup($id)))
Http::response(205, "Note not found");
elseif (($note->staff_id !== $thisstaff->getId())
&& !$thisstaff->isAdmin())
Http::response(403, "Access Denied");
Http::response(200, $note->display());
}
@@ -24,6 +27,9 @@ class NoteAjaxAPI extends AjaxController {
Http::response(403, "Login required");
elseif (!($note = QuickNote::lookup($id)))
Http::response(205, "Note not found");
elseif (($note->staff_id !== $thisstaff->getId())
&& !$thisstaff->isAdmin())
Http::response(403, "Access Denied");
elseif (!isset($_POST['note']) || !$_POST['note'])
Http::response(422, "Send `note` parameter");
@@ -41,6 +47,9 @@ class NoteAjaxAPI extends AjaxController {
Http::response(403, "Login required");
elseif (!($note = QuickNote::lookup($id)))
Http::response(205, "Note not found");
elseif (($note->staff_id !== $thisstaff->getId())
&& !$thisstaff->isAdmin())
Http::response(403, "Access Denied");
elseif (!$note->delete())
Http::response(500, "Unable to remove note");
@@ -52,6 +61,9 @@ class NoteAjaxAPI extends AjaxController {
if (!$thisstaff)
Http::response(403, "Login required");
elseif (!in_array(substr($ext_id, 0, 1), ['O', 'U'])
&& !$thisstaff->isAdmin())
Http::response(403, "Access Denied");
elseif (!isset($_POST['note']) || !$_POST['note'])
Http::response(422, "Send `note` parameter");

View File

@@ -59,10 +59,17 @@ class TasksAjaxAPI extends AjaxController {
}
function triggerThreadAction($task_id, $thread_id, $action) {
global $thisstaff;
if (!($task = Task::lookup($task_id))
|| !$task->checkStaffPerm($thisstaff))
Http::response(404, 'No such task thread entry');
$thread = ThreadEntry::lookup($thread_id);
if (!$thread)
Http::response(404, 'No such task thread entry');
if ($thread->getThread()->getObjectId() != $task_id)
if ($thread->getThread()->getObjectType() != 'A'
|| $thread->getThread()->getObjectId() != $task_id)
Http::response(404, 'No such task thread entry');
$valid = false;

View File

@@ -23,7 +23,7 @@ include_once INCLUDE_DIR . 'class.thread_actions.php';
class TicketsAjaxAPI extends AjaxController {
function lookup() {
function lookup($wildcard=false) {
global $thisstaff;
$limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
@@ -53,7 +53,7 @@ class TicketsAjaxAPI extends AjaxController {
return $this->encode(array());
global $ost;
$hits = $ost->searcher->find($q, $hits, false);
$hits = $ost->searcher->find($q, $hits, false, ['boolean' => $wildcard]);
$T = array();
if (preg_match('/\d{2,}[^*]/', $q, $T)) {
@@ -62,10 +62,10 @@ class TicketsAjaxAPI extends AjaxController {
elseif (preg_match('/(.*@.{2,})|(.{2,}@.*)/', $q, $T)) {
$hits = $this->lookupByEmail($limit, $visibility, $hits);
}
elseif (!count($hits) && preg_match('`\w$`u', $q)) {
elseif (!$wildcard && !count($hits) && preg_match('`^[a-zA-Z0-9]+$`u', $q)) {
// Do wild-card fulltext search
$_REQUEST['q'] = $q.'*';
return $this->lookup();
return $this->lookup(true);
}
// TODO: Why not aggregate based on relationship?
@@ -777,6 +777,8 @@ class TicketsAjaxAPI extends AjaxController {
if (!($ticket=Ticket::lookup($tid)))
Http::response(404, __('No such ticket'));
elseif (!$ticket->checkStaffPerm($thisstaff))
Http::response(403, __('Permission denied'));
elseif (!($field=$ticket->getField($fid)))
Http::response(404, __('No such field'));
@@ -1702,10 +1704,17 @@ class TicketsAjaxAPI extends AjaxController {
}
function triggerThreadAction($ticket_id, $thread_id, $action) {
global $thisstaff;
if (!($ticket = Ticket::lookup($ticket_id))
|| !$ticket->checkStaffPerm($thisstaff))
Http::response(404, 'No such ticket thread entry');
$thread = ThreadEntry::lookup($thread_id);
if (!$thread)
Http::response(404, 'No such ticket thread entry');
if ($thread->getThread()->getObjectId() != $ticket_id)
if ($thread->getThread()->getObjectType() != 'T'
|| $thread->getThread()->getObjectId() != $ticket_id)
Http::response(404, 'No such ticket thread entry');
$valid = false;

View File

@@ -121,7 +121,12 @@ class UsersAjaxAPI extends AjaxController {
if(!$thisstaff)
Http::response(403, 'Login Required');
elseif(!($user = User::lookup($id)))
if (!$thisstaff->hasPerm(User::PERM_DIRECTORY)
&& !$thisstaff->hasPerm(User::PERM_EDIT))
Http::response(403, 'Permission Denied');
if(!($user = User::lookup($id)))
Http::response(404, 'Unknown user');
$info = array(
@@ -275,6 +280,14 @@ class UsersAjaxAPI extends AjaxController {
}
function getUser($id=false) {
global $thisstaff;
if (!$thisstaff)
Http::response(403, 'Login Required');
if (!$thisstaff->hasPerm(User::PERM_DIRECTORY)
&& !$thisstaff->hasPerm(User::PERM_EDIT))
Http::response(403, 'Permission Denied');
if(($user=User::lookup(($id) ? $id : $_REQUEST['id'])))
Http::response(201, $user->to_json(), 'application/json');
@@ -422,6 +435,8 @@ class UsersAjaxAPI extends AjaxController {
Http::response(403, 'Login Required');
elseif (!($user = User::lookup($id)))
Http::response(404, 'Unknown user');
elseif (!$thisstaff->hasPerm(User::PERM_EDIT))
Http::response(403, 'Access Denied');
$info = array();
$info['title'] = sprintf(__('Organization for %s'),
@@ -434,6 +449,9 @@ class UsersAjaxAPI extends AjaxController {
if (!($org = Organization::lookup($_POST['orgid'])))
$info['error'] = __('Unknown organization selected');
} else { //Creating new org.
if (!$thisstaff->hasPerm(Organization::PERM_CREATE))
Http::response(403, 'Permission Denied');
$form = OrganizationForm::getDefaultForm()->getForm($_POST);
if (!($org = Organization::fromForm($form)))
$info['error'] = __('Unable to create organization.')

View File

@@ -146,7 +146,7 @@ class API {
$sql='INSERT INTO '.API_KEY_TABLE.' SET '.$sql
.',created=NOW() '
.',ipaddr='.db_input($vars['ipaddr'])
.',apikey='.db_input(strtoupper(md5(time().$vars['ipaddr'].md5(Misc::randCode(16)))));
.',apikey='.db_input(strtoupper(Misc::randCode(48, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')));
if(db_query($sql) && ($id=db_insert_id()))
return $id;
@@ -353,7 +353,7 @@ class ApiController extends Controller {
// TODO: Only include API Key when in debug mode to avoid
// potentialialy leaking a valid key in system logs
if (($key=$this->getApiKey()))
$msg .= "\n[$key]\n";
$msg .= sprintf("\n[%s]\n", Format::shroud($key, 8, 16));
$title = sprintf('%s (%s)', $title ?: __('API Error'), $code);
$this->logError($title, $msg, $logOnly);

View File

@@ -1255,7 +1255,7 @@ class PasswordResetTokenBackend extends StaffAuthenticationBackend {
|| $id != $staff->getId())
$errors['msg'] = __('Invalid reset token');
elseif (!($ts = $_config->lastModified($_POST['token']))
&& ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
|| ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
$errors['msg'] = __('Invalid reset token');
elseif (!$staff->forcePasswdRest())
$errors['msg'] = __('Unable to reset password');
@@ -1482,7 +1482,7 @@ class ClientPasswordResetTokenBackend extends UserAuthenticationBackend {
|| $id != 'c'.$client->getId())
$errors['msg'] = __('Invalid reset token');
elseif (!($ts = $_config->lastModified($_POST['token']))
&& ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
|| ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
$errors['msg'] = __('Invalid reset token');
elseif (!$acct->forcePasswdReset())
$errors['msg'] = __('Unable to reset password');

View File

@@ -29,9 +29,11 @@ class Dispatcher {
function resolve($url, $args=null) {
if ($this->file) { $this->lazy_load(); }
# Support HTTP method emulation with the _method GET argument
if (isset($_GET['_method'])) {
$_SERVER['REQUEST_METHOD'] = strtoupper($_GET['_method']);
unset($_GET['_method']);
if ($_SERVER['REQUEST_METHOD'] == 'POST'
&& isset($_POST['_method'])
&& in_array(strtoupper($_POST['_method']), array('PUT', 'PATCH', 'DELETE'))) {
$_SERVER['REQUEST_METHOD'] = strtoupper($_POST['_method']);
unset($_POST['_method']);
}
// Decode URL for accurate matching
$url = urldecode($url);

View File

@@ -175,6 +175,10 @@ class AttachmentFile extends VerySimpleModel
}
function display($scale=false, $ttl=86400) {
// Force download if not safe viewable image
if (!$this->isInlineSafeImage())
return $this->download(false, 'attachment');
$this->makeCacheable($ttl);
if ($scale && extension_loaded('gd')
@@ -214,6 +218,12 @@ class AttachmentFile extends VerySimpleModel
'type' => $options['type']))))
$options['id'] = $a->getId();
// Force attachment disposition if not inline-safe image
if (isset($options['disposition'])
&& strcasecmp($options['disposition'], 'inline') == 0
&& !$this->isInlineSafeImage())
$options['disposition'] = 'attachment';
return static::generateDownloadUrl($this->getId(),
strtolower($this->getKey()), $this->getSignature(),
$options);
@@ -267,7 +277,8 @@ class AttachmentFile extends VerySimpleModel
$check = static::_genUrlSignature($this->getId(), $this->getKey(),
$this->getSignature(), $expires);
return $signature == $check;
return hash_equals($check, $signature);
}
static function _genUrlSignature($id, $key, $signature, $expires) {
@@ -287,7 +298,7 @@ class AttachmentFile extends VerySimpleModel
$inline = ($thisstaff ? ($thisstaff->getImageAttachmentView() === 'inline') : false);
$disposition = ((($disposition && strcasecmp($disposition, 'inline') == 0)
|| $inline)
&& strpos($this->getType(), 'image/') !== false)
&& $this->isInlineSafeImage())
? 'inline' : 'attachment';
$ttl = ($expires) ? $expires - Misc::gmtime() : false;
$bk = $this->open();
@@ -721,6 +732,29 @@ class AttachmentFile extends VerySimpleModel
->filter(array('ft' => 'B'))
->order_by('created');
}
/**
* Determines whether a MIME type is safe to render inline as a passive image.
*
* @param string $type
* @return bool
*/
static function isInlineSafeImageType($type) {
$type = strtolower(trim((string) $type));
return strpos($type, 'image/') === 0
&& $type !== 'image/svg+xml'
&& $type !== 'image/svg';
}
/**
* Determines whether this file is safe to render inline as a passive image.
*
* @return bool
*/
function isInlineSafeImage() {
return static::isInlineSafeImageType($this->getType());
}
}
class FileStorageBackend {

View File

@@ -339,11 +339,10 @@ class Format {
':<html[^>]+:i', # drop html attributes
':<(a|span) (name|style)="(mso-bookmark\:)?_MailEndCompose">(.+)?<\/(a|span)>:', # Drop _MailEndCompose
':<div dir=(3D)?"ltr">(.*?)<\/div>(.*):is', # drop Gmail "ltr" attributes
':data-cid="[^"]*":', # drop image cid attributes
'(position: ?(-webkit-)?(static|relative|fixed|absolute|sticky|initial|inherit);?)', # Position styling
':[\x{2002}-\x{200B}]+:u', # unicode spaces
),
array('', '', '', '', '<html', '$4', '$2 $3', '', '', ' '),
array('', '', '', '', '<html', '$4', '$2 $3', '', ' '),
$html);
// HtmLawed specific config only
@@ -353,7 +352,7 @@ class Format {
'comment' => 1, //Remove html comments (OUTLOOK LOVE THEM)
'tidy' => -1,
'elements' => '*-form-input-button',
'deny_attribute' => 'id, formaction, action, srcset, on*',
'deny_attribute' => 'id, formaction, action, srcset, data*, on*',
'schemes' => 'href: aim, feed, file, ftp, gopher, http, https, irc, mailto, news, nntp, sftp, ssh, telnet; *:file, http, https; src: cid, http, https, data',
'hook_tag' => function($e, $a=0) { return Format::__html_cleanup($e, $a); },
);
@@ -605,6 +604,9 @@ class Format {
if (!($file = AttachmentFile::lookup($match[2])))
return $match[0];
if (!$file->isInlineSafeImage())
return $match[0];
return sprintf('"%s" data-cid="%s"',
$file->getDownloadUrl($options), $match[2]);
}, $html);

View File

@@ -973,7 +973,7 @@ class CustomQueue extends VerySimpleModel {
// Consider keyword searching
if ($name === ':keywords') {
global $ost;
$qs = $ost->searcher->find($value, $qs, false);
$qs = $ost->searcher->find($value, $qs, false, ['boolean' => false]);
}
else {
$nullable = ($method === 'nset') ? false : null;

View File

@@ -44,7 +44,7 @@ abstract class SearchBackend {
);
abstract function update($model, $id, $content, $new=false, $attrs=array());
abstract function find($query, QuerySet $criteria, $addRelevance=true);
abstract function find($query, QuerySet $criteria, $addRelevance=true, $options=array());
static function register($backend=false) {
$backend = $backend ?: get_called_class();
@@ -79,9 +79,9 @@ class SearchInterface {
$this->bootstrap();
}
function find($query, QuerySet $criteria, $addRelevance=true) {
function find($query, QuerySet $criteria, $addRelevance=true, $options=array()) {
$query = Format::searchable($query);
return $this->backend->find($query, $criteria, $addRelevance);
return $this->backend->find($query, $criteria, $addRelevance, $options);
}
function update($model, $id, $content, $new=false, $attrs=array()) {
@@ -327,7 +327,7 @@ class MysqlSearchBackend extends SearchBackend {
return implode('', $results);
}
function find($query, QuerySet $criteria, $addRelevance=true) {
function find($query, QuerySet $criteria, $addRelevance=true, $options=array()) {
global $thisstaff;
// MySQL usually doesn't handle words shorter than three letters
@@ -339,6 +339,10 @@ class MysqlSearchBackend extends SearchBackend {
$mode = ' IN NATURAL LANGUAGE MODE';
// Allow boolean full-text syntax to be disabled
$allow_boolean = !array_key_exists('boolean', $options)
|| $options['boolean'];
// According to the MySQL full text boolean mode, this grammar is
// assumed:
// see http://dev.mysql.com/doc/refman/5.6/en/fulltext-boolean.html
@@ -359,7 +363,8 @@ class MysqlSearchBackend extends SearchBackend {
// Require the use of at least one operator and conform to the
// boolean mode grammar
$T = array();
if (preg_match('`(^|\s)["()<>~+-]`u', $query, $T)
if ($allow_boolean
&& preg_match('`(^|\s)["()<>~+-]`u', $query, $T)
&& preg_match("`^{$BOOLEAN}$`u", $query, $T)
) {
// If using boolean operators, search in boolean mode. This regex

View File

@@ -1672,7 +1672,7 @@ implements TemplateVariable {
'format' => $vars['body']->getType(),
'staff_id' => $vars['staffId'],
'user_id' => $vars['userId'],
'poster' => $poster,
'poster' => Format::sanitize($poster),
'source' => $vars['source'],
'flags' => $vars['flags'] ?: 0,
));
@@ -1757,6 +1757,10 @@ implements TemplateVariable {
$files[$i]['inline'] = true;
}
foreach ($entry->normalizeFileInfo($files) as $F) {
if (!empty($F['inline'])
&& (!isset($F['file']) || !$F['file']->isInlineSafeImage()))
$F['inline'] = false;
// Deduplicate on the `key` attribute. The key is
// necessary for the CID rewrite below
$attached_files[$F['key']] = $F;
@@ -1769,6 +1773,9 @@ implements TemplateVariable {
// discarded, only the unique hash-code (key) will be available to
// retrieve the image later
foreach ($attached_files as $key => $a) {
if (empty($a['inline']))
continue;
if (isset($a['cid']) && $a['cid']) {
$body = preg_replace('/src=("|\'|\b)(?:cid:)?'
. preg_quote($a['cid'], '/').'\1/i',

View File

@@ -4651,7 +4651,6 @@ implements RestrictedAccess, Threadable, Searchable {
// post response - if any
$response = null;
if ($vars['response'] && $role->hasPerm(Ticket::PERM_REPLY)) {
$vars['response'] = $ticket->replaceVars($vars['response']);
// $vars['cannedatachments'] contains the attachments placed on
// the response form.
$response = $ticket->postReply($vars, $errors, ($alert &&

View File

@@ -88,7 +88,7 @@ class UserSession {
list($hash, $expire, $ip) = explode(':', $token);
// Make sure the session hash is valid
if ((md5($expire . SESSION_SECRET . $this->userID) != $hash))
if (!hash_equals(md5($expire . SESSION_SECRET . $this->userID), $hash))
return false;
// is it expired??

View File

@@ -71,6 +71,15 @@ class VariableReplacer {
$tag = mb_strtolower($tag);
$rv = null;
// Deny sensitive attributes outright
$blacklist = [
'passwd',
'password',
'authkey',
];
if (in_array($tag, $blacklist))
return "";
if (!is_object($obj)) {
if ($tag && is_array($obj) && array_key_exists($tag, $obj))
$rv = $obj[$tag];

View File

@@ -30,7 +30,7 @@ if ($cfg->isAvatarsEnabled() && $user)
</span>
</div>
<?php
echo sprintf(__('<b>%s</b> posted %s'), $name,
echo sprintf(__('<b>%s</b> posted %s'), Format::htmlchars($name),
sprintf('<time datetime="%s" title="%s">%s</time>',
date(DateTime::W3C, Misc::db2gmtime($entry->created)),
Format::daydatetime($entry->created),
@@ -38,7 +38,7 @@ if ($cfg->isAvatarsEnabled() && $user)
)
); ?>
<span style="max-width:500px" class="faded title truncate"><?php
echo $entry->title; ?>
echo Format::htmlchars($entry->title); ?>
</span>
</div>
<div class="thread-body" id="thread-id-<?php echo $entry->getId(); ?>">

View File

@@ -42,9 +42,9 @@ $agentmasking = $cfg->hideStaffName();
<td style=" border-top: 1px dashed #999;">
<div style="background-color:#f7f7f7; padding:10px 20px;">
<p style="font-family: sans-serif; padding:0; margin:0; color:<?php echo $color; ?>;">
<strong><?php echo $name; ?></strong>
<strong><?php echo Format::htmlchars($name); ?></strong>
<span style="color:#888; font-size:12px; padding-left: 20px;"><?php
echo $entry->title;
echo Format::htmlchars($entry->title);
?>
</span>
</p>

View File

@@ -101,7 +101,7 @@ if ($settings['keywords']) {
$tickets->filter(array('number__startswith'=>$q));
} elseif (strlen($q) > 2) { //Deep search!
// Use the search engine to perform the search
$tickets = $ost->searcher->find($q, $tickets);
$tickets = $ost->searcher->find($q, $tickets, true, ['boolean' => false]);
}
}

View File

@@ -106,7 +106,9 @@ else
<td align="center">
<input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['id']; ?>"
<?php echo $sel?'checked="checked"':''; ?>> </td>
<td>&nbsp;<a href="apikeys.php?id=<?php echo $row['id']; ?>"><?php echo Format::htmlchars($row['apikey']); ?></a></td>
<td>&nbsp;<a href="apikeys.php?id=<?php echo $row['id']; ?>">
<?php echo Format::shroud(Format::htmlchars($row['apikey']), 12, 26); ?>
</a></td>
<td><?php echo $row['ipaddr']; ?></td>
<td><?php echo $row['isactive']?__('Active'):'<b>'.__('Disabled').'</b>'; ?></td>
<td>&nbsp;<?php echo Format::date($row['created']); ?></td>

View File

@@ -84,7 +84,7 @@ if ($_POST && $thread) {
$c->getUserId(),
$c->isActive() ? 'selected="selected"' : '',
$c->isActive() ? 'active' : 'disabled',
$c->getName());
Format::htmlchars($c->getName()));
}
$recipients = sprintf(__('(%d of %d)'),
$thread->getNumActiveCollaborators(),
@@ -92,9 +92,9 @@ if ($_POST && $thread) {
?>
<script type="text/javascript">
$(function() {
$('#t<?php echo $thread->getId(); ?>-recipients').html('<?php echo $recipients; ?>');
$('#t<?php echo $thread->getId(); ?>-recipients').html('<?php echo JsonDataEncoder::encode($recipients); ?>');
$('#t<?php echo $thread->getId(); ?>-collaborators').html('<?php echo $thread->getNumCollaborators(); ?>');
$('#collabselection').html('<?php echo $options; ?>');
$('#collabselection').html('<?php echo JsonDataEncoder::encode($options); ?>');
});
</script>
<?php

View File

@@ -8,5 +8,5 @@
<br/>
<div class="thread-body bleed">
<?php echo $faq->getLocalAnswer(); ?>
<?php echo Format::stripExternalImages($faq->getLocalAnswer(), true); ?>
</div>

View File

@@ -15,7 +15,7 @@ foreach ($entries as $entry) {
<div class="header">
<div class="thread-info">
<div class="thread-name">
<span><?php echo $name; ?></span>&nbsp;<span><?php echo Format::datetime($entry->created) ?></span>
<span><?php echo Format::htmlchars($name); ?></span>&nbsp;<span><?php echo Format::datetime($entry->created); ?></span>
<?php if ($entry->number) { ?>
&nbsp;<span data-toggle="tooltip" title="<?php echo sprintf(__('Ticket #%s'), $entry->number); ?>" class="label label-bare">
<i class="icon-code-fork"></i></span>

View File

@@ -27,7 +27,7 @@ do {
echo Format::htmlchars($E->title).' — '; ?></strong>
<em><?php if (strpos($E->updated, '0000-') === false)
echo sprintf(__('Edited on %s by %s'), Format::datetime($E->updated),
($editor = $E->getEditor()) ? $editor->getName() : '');
($editor = $E->getEditor()) ? Format::htmlchars($editor->getName()) : '');
else
echo __('Original'); ?></em>
</a>

View File

@@ -71,7 +71,7 @@ if ($user && $cfg->isAvatarsEnabled())
</span>
</div>
<?php
echo sprintf(__('<b>%s</b> posted %s'), $name,
echo sprintf(__('<b>%s</b> posted %s'), Format::htmlchars($name),
sprintf('<a name="entry-%d" href="#entry-%1$s"><time %s
datetime="%s" data-toggle="tooltip" title="%s">%s</time></a>',
$entry->id,
@@ -82,7 +82,7 @@ if ($user && $cfg->isAvatarsEnabled())
)
); ?>
<span style="max-width:400px" class="faded title truncate"><?php
echo $entry->title; ?>
echo Format::htmlchars($entry->title); ?>
</span>
</div>
<div class="thread-body no-pjax">

View File

@@ -11,11 +11,21 @@ if ($_SESSION[':form-data'] && !$_GET['tid'])
// Use thread entry to seed the ticket
if (!$user && $_GET['tid'] && ($entry = ThreadEntry::lookup($_GET['tid']))) {
if ($entry->getThread()->getObjectType() == 'T')
if ($entry->getThread()->getObjectType() == 'T') {
$oldTicketId = $entry->getThread()->getObjectId();
if ($entry->getThread()->getObjectType() == 'A')
if (!($oldTicket = Ticket::lookup($oldTicketId))
|| !$oldTicket->checkStaffPerm($thisstaff))
Http::response(403, 'Access Denied');
}
if ($entry->getThread()->getObjectType() == 'A') {
$oldTaskId = $entry->getThread()->getObjectId();
if (!($oldTask = Task::lookup($oldTaskId))
|| !$oldTask->checkStaffPerm($thisstaff))
Http::response(403, 'Access Denied');
}
$_SESSION[':form-data']['message'] = Format::htmlchars($entry->getBody());
$_SESSION[':form-data']['ticketId'] = $oldTicketId;
$_SESSION[':form-data']['taskId'] = $oldTaskId;

View File

@@ -18,6 +18,12 @@ define('SOURCE','Web'); //Ticket source.
$ticket = null;
$errors=array();
if ($_POST) {
// If login is required and there is no user or if guest user then
// banish to the shadow realm
if ($cfg->isClientLoginRequired()
&& (!$thisclient || $thisclient->isGuest()))
require_once 'secure.inc.php';
$vars = $_POST;
$vars['deptId']=$vars['emailId']=0; //Just Making sure we don't accept crap...only topicId is expected.
if ($thisclient) {

View File

@@ -1,16 +1,146 @@
/* ========================================================================
* Bootstrap: tooltip.js v3.3.4
* http://getbootstrap.com/javascript/#tooltip
* Bootstrap: tooltip.js v3.4.1
* https://getbootstrap.com/docs/3.4/javascript/#tooltip
* Inspired by the original jQuery.tipsy by Jason Frame
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Copyright 2011-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
var DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']
var uriAttrs = [
'background',
'cite',
'href',
'itemtype',
'longdesc',
'poster',
'src',
'xlink:href'
]
var ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i
var DefaultWhitelist = {
// Global attributes allowed on any supplied element below.
'*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],
a: ['target', 'href', 'title', 'rel'],
area: [],
b: [],
br: [],
col: [],
code: [],
div: [],
em: [],
hr: [],
h1: [],
h2: [],
h3: [],
h4: [],
h5: [],
h6: [],
i: [],
img: ['src', 'alt', 'title', 'width', 'height'],
li: [],
ol: [],
p: [],
pre: [],
s: [],
small: [],
span: [],
sub: [],
sup: [],
strong: [],
u: [],
ul: []
}
/**
* A pattern that recognizes a commonly useful subset of URLs that are safe.
*
* Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
*/
var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi
/**
* A pattern that matches safe data URLs. Only matches image, video and audio types.
*
* Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
*/
var DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i
function allowedAttribute(attr, allowedAttributeList) {
var attrName = attr.nodeName.toLowerCase()
if ($.inArray(attrName, allowedAttributeList) !== -1) {
if ($.inArray(attrName, uriAttrs) !== -1) {
return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN))
}
return true
}
var regExp = $(allowedAttributeList).filter(function (index, value) {
return value instanceof RegExp
})
// Check if a regular expression validates the attribute.
for (var i = 0, l = regExp.length; i < l; i++) {
if (attrName.match(regExp[i])) {
return true
}
}
return false
}
function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) {
if (unsafeHtml.length === 0) {
return unsafeHtml
}
if (sanitizeFn && typeof sanitizeFn === 'function') {
return sanitizeFn(unsafeHtml)
}
// IE 8 and below don't support createHTMLDocument
if (!document.implementation || !document.implementation.createHTMLDocument) {
return unsafeHtml
}
var createdDocument = document.implementation.createHTMLDocument('sanitization')
createdDocument.body.innerHTML = unsafeHtml
var whitelistKeys = $.map(whiteList, function (el, i) { return i })
var elements = $(createdDocument.body).find('*')
for (var i = 0, len = elements.length; i < len; i++) {
var el = elements[i]
var elName = el.nodeName.toLowerCase()
if ($.inArray(elName, whitelistKeys) === -1) {
el.parentNode.removeChild(el)
continue
}
var attributeList = $.map(el.attributes, function (el) { return el })
var whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || [])
for (var j = 0, len2 = attributeList.length; j < len2; j++) {
if (!allowedAttribute(attributeList[j], whitelistedAttributes)) {
el.removeAttribute(attributeList[j].nodeName)
}
}
}
return createdDocument.body.innerHTML
}
// TOOLTIP PUBLIC CLASS DEFINITION
// ===============================
@@ -26,7 +156,7 @@
this.init('tooltip', element, options)
}
Tooltip.VERSION = '3.3.4'
Tooltip.VERSION = '3.4.1'
Tooltip.TRANSITION_DURATION = 150
@@ -43,7 +173,10 @@
viewport: {
selector: 'body',
padding: 0
}
},
sanitize : true,
sanitizeFn : null,
whiteList : DefaultWhitelist
}
Tooltip.prototype.init = function (type, element, options) {
@@ -51,7 +184,7 @@
this.type = type
this.$element = $(element)
this.options = this.getOptions(options)
this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport))
this.$viewport = this.options.viewport && $(document).find($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport))
this.inState = { click: false, hover: false, focus: false }
if (this.$element[0] instanceof document.constructor && !this.options.selector) {
@@ -84,7 +217,15 @@
}
Tooltip.prototype.getOptions = function (options) {
options = $.extend({}, this.getDefaults(), this.$element.data(), options)
var dataAttributes = this.$element.data()
for (var dataAttr in dataAttributes) {
if (dataAttributes.hasOwnProperty(dataAttr) && $.inArray(dataAttr, DISALLOWED_ATTRIBUTES) !== -1) {
delete dataAttributes[dataAttr]
}
}
options = $.extend({}, this.getDefaults(), dataAttributes, options)
if (options.delay && typeof options.delay == 'number') {
options.delay = {
@@ -93,6 +234,10 @@
}
}
if (options.sanitize) {
options.template = sanitizeHtml(options.template, options.whiteList, options.sanitizeFn)
}
return options
}
@@ -204,7 +349,7 @@
.addClass(placement)
.data('bs.' + this.type, this)
this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
this.options.container ? $tip.appendTo($(document).find(this.options.container)) : $tip.insertAfter(this.$element)
this.$element.trigger('inserted.bs.' + this.type)
var pos = this.getPosition()
@@ -306,7 +451,16 @@
var $tip = this.tip()
var title = this.getTitle()
$tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
if (this.options.html) {
if (this.options.sanitize) {
title = sanitizeHtml(title, this.options.whiteList, this.options.sanitizeFn)
}
$tip.find('.tooltip-inner').html(title)
} else {
$tip.find('.tooltip-inner').text(title)
}
$tip.removeClass('fade in top bottom left right')
}
@@ -317,9 +471,11 @@
function complete() {
if (that.hoverState != 'in') $tip.detach()
that.$element
.removeAttr('aria-describedby')
.trigger('hidden.bs.' + that.type)
if (that.$element) { // TODO: Check whether guarding this code with this `if` is really necessary.
that.$element
.removeAttr('aria-describedby')
.trigger('hidden.bs.' + that.type)
}
callback && callback()
}
@@ -362,7 +518,10 @@
// width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093
elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top })
}
var elOffset = isBody ? { top: 0, left: 0 } : $element.offset()
var isSvg = window.SVGElement && el instanceof window.SVGElement
// Avoid using $.offset() on SVGs since it gives incorrect results in jQuery 3.
// See https://github.com/twbs/bootstrap/issues/20280
var elOffset = isBody ? { top: 0, left: 0 } : (isSvg ? null : $element.offset())
var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }
var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null
@@ -478,9 +637,13 @@
that.$tip = null
that.$arrow = null
that.$viewport = null
that.$element = null
})
}
Tooltip.prototype.sanitizeHtml = function (unsafeHtml) {
return sanitizeHtml(unsafeHtml, this.options.whiteList, this.options.sanitizeFn)
}
// TOOLTIP PLUGIN DEFINITION
// =========================

View File

@@ -39,6 +39,7 @@ if($_POST) {
}
switch ($_POST['do']) {
case 'sendmail':
$start = microtime(true);
$userid = (string) $_POST['userid'];
if (Validator::is_userid($userid)
&& ($staff=Staff::lookup($userid))) {
@@ -50,6 +51,15 @@ if($_POST) {
}
else
$tpl = 'pwreset.sent.php';
$min = 1.4;
$jitter = random_int(0, 250) / 1000;
$target = $min + $jitter;
$elapsed = microtime(true) - $start;
if ($elapsed < $target)
usleep((int)(($target - $elapsed) * 1_000_000));
break;
case 'newpasswd':
// TODO: Compare passwords

View File

@@ -104,8 +104,9 @@ define('SESSION_MAXLIFE', $thisstaff->getMaxIdleTime());
$thisstaff->refreshSession();
/******* CSRF Protectin *************/
// Enforce CSRF protection for POSTS
if ($_POST && !$ost->checkCSRFToken()) {
// Enforce CSRF protection for state-changing methods
if (in_array($_SERVER['REQUEST_METHOD'], ['POST', 'PUT', 'PATCH', 'DELETE'])
&& !$ost->checkCSRFToken()) {
Http::response(400, __('Valid CSRF Token Required'));
exit;
}