fix: CSRF on ticket create form, DOM-safe duplicate list, audit-log param validation
- TicketController::create: validate csrf_token from POST before processing - CreateTicketView: emit hidden csrf_token field; replace innerHTML duplicate list with DOM methods to prevent any XSS path; guard checkDuplicates() with lt.api availability check - index.php audit-log: allowlist action_type; validate date_from/date_to as YYYY-MM-DD before passing to query Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,18 @@ class TicketController {
|
|||||||
|
|
||||||
// Check if form was submitted
|
// Check if form was submitted
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
// Validate CSRF token
|
||||||
|
require_once dirname(__DIR__) . '/middleware/CsrfMiddleware.php';
|
||||||
|
$csrfToken = $_POST['csrf_token'] ?? '';
|
||||||
|
if (!CsrfMiddleware::validateToken($csrfToken)) {
|
||||||
|
$error = "Invalid or expired security token. Please try again.";
|
||||||
|
$templates = $this->templateModel->getAllTemplates();
|
||||||
|
$allUsers = $this->userModel->getAllUsers();
|
||||||
|
$conn = $this->conn;
|
||||||
|
include dirname(__DIR__) . '/views/CreateTicketView.php';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle visibility groups (comes as array from checkboxes)
|
// Handle visibility groups (comes as array from checkboxes)
|
||||||
$visibilityGroups = null;
|
$visibilityGroups = null;
|
||||||
if (isset($_POST['visibility_groups']) && is_array($_POST['visibility_groups'])) {
|
if (isset($_POST['visibility_groups']) && is_array($_POST['visibility_groups'])) {
|
||||||
|
|||||||
@@ -242,7 +242,9 @@ switch (true) {
|
|||||||
$params = [];
|
$params = [];
|
||||||
$types = '';
|
$types = '';
|
||||||
|
|
||||||
if (!empty($_GET['action_type'])) {
|
$allowedActionTypes = ['create','update','delete','comment','assign','status_change','login','security',
|
||||||
|
'ticket_create','ticket_update','ticket_delete','attachment_delete','attachment_upload'];
|
||||||
|
if (!empty($_GET['action_type']) && in_array($_GET['action_type'], $allowedActionTypes, true)) {
|
||||||
$whereConditions[] = "al.action_type = ?";
|
$whereConditions[] = "al.action_type = ?";
|
||||||
$params[] = $_GET['action_type'];
|
$params[] = $_GET['action_type'];
|
||||||
$types .= 's';
|
$types .= 's';
|
||||||
@@ -252,15 +254,15 @@ switch (true) {
|
|||||||
$whereConditions[] = "al.user_id = ?";
|
$whereConditions[] = "al.user_id = ?";
|
||||||
$params[] = (int)$_GET['user_id'];
|
$params[] = (int)$_GET['user_id'];
|
||||||
$types .= 'i';
|
$types .= 'i';
|
||||||
$filters['user_id'] = $_GET['user_id'];
|
$filters['user_id'] = (int)$_GET['user_id'];
|
||||||
}
|
}
|
||||||
if (!empty($_GET['date_from'])) {
|
if (!empty($_GET['date_from']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $_GET['date_from'])) {
|
||||||
$whereConditions[] = "DATE(al.created_at) >= ?";
|
$whereConditions[] = "DATE(al.created_at) >= ?";
|
||||||
$params[] = $_GET['date_from'];
|
$params[] = $_GET['date_from'];
|
||||||
$types .= 's';
|
$types .= 's';
|
||||||
$filters['date_from'] = $_GET['date_from'];
|
$filters['date_from'] = $_GET['date_from'];
|
||||||
}
|
}
|
||||||
if (!empty($_GET['date_to'])) {
|
if (!empty($_GET['date_to']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $_GET['date_to'])) {
|
||||||
$whereConditions[] = "DATE(al.created_at) <= ?";
|
$whereConditions[] = "DATE(al.created_at) <= ?";
|
||||||
$params[] = $_GET['date_to'];
|
$params[] = $_GET['date_to'];
|
||||||
$types .= 's';
|
$types .= 's';
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ include __DIR__ . '/layout_header.php';
|
|||||||
class="create-ticket-form"
|
class="create-ticket-form"
|
||||||
novalidate>
|
novalidate>
|
||||||
|
|
||||||
|
<input type="hidden" name="csrf_token"
|
||||||
|
value="<?= htmlspecialchars(CsrfMiddleware::getToken(), ENT_QUOTES, 'UTF-8') ?>">
|
||||||
|
|
||||||
<?php if (isset($error)): ?>
|
<?php if (isset($error)): ?>
|
||||||
<div class="lt-msg lt-msg-danger lt-mb-md" role="alert">
|
<div class="lt-msg lt-msg-danger lt-mb-md" role="alert">
|
||||||
<strong>Error:</strong> <?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?>
|
<strong>Error:</strong> <?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?>
|
||||||
@@ -255,26 +258,41 @@ include __DIR__ . '/layout_header.php';
|
|||||||
});
|
});
|
||||||
|
|
||||||
function checkDuplicates(title) {
|
function checkDuplicates(title) {
|
||||||
|
if (!window.lt || typeof lt.api === 'undefined') return;
|
||||||
lt.api.get('/api/check_duplicates.php?title=' + encodeURIComponent(title))
|
lt.api.get('/api/check_duplicates.php?title=' + encodeURIComponent(title))
|
||||||
.then(function (data) {
|
.then(function (data) {
|
||||||
var warn = document.getElementById('duplicateWarning');
|
var warn = document.getElementById('duplicateWarning');
|
||||||
var list = document.getElementById('duplicatesList');
|
var list = document.getElementById('duplicatesList');
|
||||||
if (data.success && data.duplicates && data.duplicates.length > 0) {
|
if (data.success && data.duplicates && data.duplicates.length > 0) {
|
||||||
var html = '<ul class="duplicate-list lt-text-sm">';
|
var ul = document.createElement('ul');
|
||||||
|
ul.className = 'duplicate-list lt-text-sm';
|
||||||
data.duplicates.forEach(function (dup) {
|
data.duplicates.forEach(function (dup) {
|
||||||
html += '<li><a href="/ticket/' + lt.escHtml(dup.ticket_id) + '" target="_blank">#' +
|
var li = document.createElement('li');
|
||||||
lt.escHtml(dup.ticket_id) + '</a> — ' + lt.escHtml(dup.title) +
|
var a = document.createElement('a');
|
||||||
' <span class="lt-text-muted">(' + dup.similarity + '% match, ' +
|
a.href = '/ticket/' + encodeURIComponent(dup.ticket_id);
|
||||||
lt.escHtml(dup.status) + ')</span></li>';
|
a.target = '_blank';
|
||||||
|
a.textContent = '#' + dup.ticket_id;
|
||||||
|
var dash = document.createTextNode(' \u2014 ' + dup.title + ' ');
|
||||||
|
var badge = document.createElement('span');
|
||||||
|
badge.className = 'lt-text-muted';
|
||||||
|
badge.textContent = '(' + dup.similarity + '% match, ' + dup.status + ')';
|
||||||
|
li.appendChild(a);
|
||||||
|
li.appendChild(dash);
|
||||||
|
li.appendChild(badge);
|
||||||
|
ul.appendChild(li);
|
||||||
});
|
});
|
||||||
html += '</ul><p class="lt-text-xs lt-text-muted lt-mt-sm">Check these before creating a new ticket.</p>';
|
var hint = document.createElement('p');
|
||||||
list.innerHTML = html;
|
hint.className = 'lt-text-xs lt-text-muted lt-mt-sm';
|
||||||
|
hint.textContent = 'Check these before creating a new ticket.';
|
||||||
|
list.innerHTML = '';
|
||||||
|
list.appendChild(ul);
|
||||||
|
list.appendChild(hint);
|
||||||
warn.classList.remove('is-hidden');
|
warn.classList.remove('is-hidden');
|
||||||
} else {
|
} else {
|
||||||
warn.classList.add('is-hidden');
|
warn.classList.add('is-hidden');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function () { /* silent */ });
|
.catch(function () { /* silent — duplicate check is non-critical */ });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Visibility groups toggle ──────────────────────────────
|
// ── Visibility groups toggle ──────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user