Fix XSS, CSRF, input validation, and related security issues

This commit is contained in:
2026-04-14 16:00:50 -05:00
parent e0f04ea971
commit 270acc0430
7 changed files with 86 additions and 33 deletions
+12
View File
@@ -349,6 +349,18 @@
navigator.serviceWorker.register('/sw.js');
}
// Inject CSRF token into all POST forms
document.addEventListener('DOMContentLoaded', function() {
var token = '{{ csrf_token() }}';
document.querySelectorAll('form').forEach(function(form) {
if (form.method.toLowerCase() === 'post') {
var inp = document.createElement('input');
inp.type = 'hidden'; inp.name = 'csrf_token'; inp.value = token;
form.appendChild(inp);
}
});
});
(function() {
var modal = document.getElementById('confirm-modal');
var msgEl = document.getElementById('confirm-modal-msg');
+21 -12
View File
@@ -411,7 +411,7 @@ function metaSelectChanged(sel, inputId) {
(function() {
var canvas = document.getElementById('pct-chart');
if (!canvas) return;
var rawLogs = {{ pct_logs_json | safe }};
var rawLogs = {{ pct_logs_data | tojson }};
// pct_logs_json is ordered newest-first; chart wants oldest-first
var logsAsc = rawLogs.slice().reverse();
var vals = logsAsc.map(function(l) { return l.pct; });
@@ -472,14 +472,15 @@ function metaSelectChanged(sel, inputId) {
<style>
.hist-modal { display:none; position:fixed; inset:0; z-index:1000; background:rgba(0,0,0,.45); align-items:center; justify-content:center; }
.hist-modal.open { display:flex; }
.hist-modal-box { background:#fff; border-radius:8px; width:min(700px,96vw); max-height:85vh; display:flex; flex-direction:column; overflow:hidden; box-shadow:0 8px 32px rgba(0,0,0,.2); }
.hist-modal-box { background:var(--bg-card,#fff); color:var(--text-body,#222); border-radius:8px; width:min(700px,96vw); max-height:85vh; display:flex; flex-direction:column; overflow:hidden; box-shadow:0 8px 32px rgba(0,0,0,.2); }
.hist-modal-header { display:flex; align-items:center; justify-content:space-between; padding:0.9rem 1.1rem 0.7rem; border-bottom:1px solid var(--border,#e2e8f0); }
.hist-modal-header h3 { margin:0; font-size:1.05rem; }
.hist-modal-header h3 { margin:0; font-size:1.05rem; color:var(--text-h2,#334155); }
.hist-modal-close { background:none; border:none; font-size:1.2rem; cursor:pointer; color:var(--text-muted,#6b7280); line-height:1; padding:0.2rem 0.4rem; }
.hist-modal-filters { padding:0.6rem 1.1rem; border-bottom:1px solid var(--border,#e2e8f0); display:flex; gap:0.75rem; align-items:center; flex-wrap:wrap; }
.hist-modal-filters:empty { display:none; }
.hist-modal-body { overflow-y:auto; flex:1; padding:0.5rem 0.5rem; }
.hist-modal-footer { display:flex; align-items:center; justify-content:space-between; padding:0.6rem 1.1rem; border-top:1px solid var(--border,#e2e8f0); font-size:0.85rem; color:var(--text-muted,#6b7280); }
.hist-modal-filters select { background:var(--bg-input,#fff); color:var(--text-body,#222); border:1px solid var(--border-input,#d1d5db); border-radius:4px; padding:0.3rem 0.5rem; font-size:0.875rem; font-family:inherit; }
</style>
<!-- Capacity modal -->
@@ -549,6 +550,14 @@ function metaSelectChanged(sel, inputId) {
var HIST_PAGE = 20;
var _batteryId = {{ battery.id }};
function escHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function openHistModal(id) {
document.getElementById(id).classList.add('open');
}
@@ -618,7 +627,7 @@ function makeModal(cfg) {
}
// ── Capacity modal ────────────────────────────────────────────────────────
var _capAll = {{ capacity_tests_json | safe }};
var _capAll = {{ capacity_tests_data | tojson }};
var capModal = makeModal({
all: _capAll,
bodyId: 'cap-modal-body', prevId: 'cap-prev', nextId: 'cap-next', pageInfoId: 'cap-page-info',
@@ -626,9 +635,9 @@ var capModal = makeModal({
thead: '<tr><th>Date</th><th>Capacity</th><th>Notes</th><th></th></tr>',
renderRow: function(r) {
return '<tr>' +
'<td data-label="Date">' + r.date + '</td>' +
'<td data-label="Date">' + escHtml(r.date) + '</td>' +
'<td data-label="Capacity">' + r.mah + ' mAh</td>' +
'<td data-label="Notes" class="text-muted">' + (r.notes || '—') + '</td>' +
'<td data-label="Notes" class="text-muted">' + (escHtml(r.notes) || '—') + '</td>' +
'<td data-label="">' +
'<form class="inline" method="post" ' +
'action="/battery/' + _batteryId + '/capacity-test/' + r.id + '/delete" ' +
@@ -645,7 +654,7 @@ document.querySelector('[onclick="openHistModal(\'cap-modal\')"]') &&
document.querySelector('[onclick="openHistModal(\'cap-modal\')"]').addEventListener('click', function() { capModal.init(); });
// ── Charge modal ──────────────────────────────────────────────────────────
var _chgAll = {{ charge_logs_json | safe }};
var _chgAll = {{ charge_logs_data | tojson }};
var chgModal = makeModal({
all: _chgAll,
bodyId: 'chg-modal-body', prevId: 'chg-prev', nextId: 'chg-next', pageInfoId: 'chg-page-info',
@@ -653,9 +662,9 @@ var chgModal = makeModal({
thead: '<tr><th>Date</th><th>+Cycle</th><th>Notes</th><th></th></tr>',
renderRow: function(r) {
return '<tr>' +
'<td data-label="Date">' + r.date + '</td>' +
'<td data-label="Date">' + escHtml(r.date) + '</td>' +
'<td data-label="+Cycle">' + (r.cycles ? '✓' : '—') + '</td>' +
'<td data-label="Notes" class="text-muted">' + (r.notes || '—') + '</td>' +
'<td data-label="Notes" class="text-muted">' + (escHtml(r.notes) || '—') + '</td>' +
'<td data-label="">' +
'<form class="inline" method="post" ' +
'action="/battery/' + _batteryId + '/charge-log/' + r.id + '/delete" ' +
@@ -669,7 +678,7 @@ document.querySelector('[onclick="openHistModal(\'chg-modal\')"]') &&
document.querySelector('[onclick="openHistModal(\'chg-modal\')"]').addEventListener('click', function() { chgModal.init(); });
// ── Percentage modal ──────────────────────────────────────────────────────
var _pctAll = {{ pct_logs_json | safe }};
var _pctAll = {{ pct_logs_data | tojson }};
var pctModal = makeModal({
all: _pctAll,
bodyId: 'pct-modal-body', prevId: 'pct-prev', nextId: 'pct-next', pageInfoId: 'pct-page-info',
@@ -680,9 +689,9 @@ var pctModal = makeModal({
? '<span class="badge badge-warning">⚠ ' + r.pct + '%</span>'
: r.pct + '%';
return '<tr>' +
'<td data-label="Date / Time">' + r.recorded_at + '</td>' +
'<td data-label="Date / Time">' + escHtml(r.recorded_at) + '</td>' +
'<td data-label="%">' + pctHtml + '</td>' +
'<td data-label="Source" class="text-muted">' + (r.source || '—') + '</td>' +
'<td data-label="Source" class="text-muted">' + (escHtml(r.source) || '—') + '</td>' +
'</tr>';
}
});