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:
2026-03-28 21:26:52 -04:00
parent b40c404828
commit 6b76496640
3 changed files with 44 additions and 12 deletions
+26 -8
View File
@@ -35,6 +35,9 @@ include __DIR__ . '/layout_header.php';
class="create-ticket-form"
novalidate>
<input type="hidden" name="csrf_token"
value="<?= htmlspecialchars(CsrfMiddleware::getToken(), ENT_QUOTES, 'UTF-8') ?>">
<?php if (isset($error)): ?>
<div class="lt-msg lt-msg-danger lt-mb-md" role="alert">
<strong>Error:</strong> <?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?>
@@ -255,26 +258,41 @@ include __DIR__ . '/layout_header.php';
});
function checkDuplicates(title) {
if (!window.lt || typeof lt.api === 'undefined') return;
lt.api.get('/api/check_duplicates.php?title=' + encodeURIComponent(title))
.then(function (data) {
var warn = document.getElementById('duplicateWarning');
var list = document.getElementById('duplicatesList');
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) {
html += '<li><a href="/ticket/' + lt.escHtml(dup.ticket_id) + '" target="_blank">#' +
lt.escHtml(dup.ticket_id) + '</a> &mdash; ' + lt.escHtml(dup.title) +
' <span class="lt-text-muted">(' + dup.similarity + '% match, ' +
lt.escHtml(dup.status) + ')</span></li>';
var li = document.createElement('li');
var a = document.createElement('a');
a.href = '/ticket/' + encodeURIComponent(dup.ticket_id);
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>';
list.innerHTML = html;
var hint = document.createElement('p');
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');
} else {
warn.classList.add('is-hidden');
}
})
.catch(function () { /* silent */ });
.catch(function () { /* silent — duplicate check is non-critical */ });
}
// ── Visibility groups toggle ──────────────────────────────