fix(cms): implement v2 security and stability review findings
CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Blocked by required conditions
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Blocked by required conditions
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Waiting to run
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Blocked by required conditions

Resolves:
- CSRF false positive checked (global POST protection)
- Publish mutex to prevent git lock / double deploy
- Basic Auth rate limit checked before credential evaluation
- Memory leak in rate limiter (added GC interval)
- XSS in Toast messages
- XSS in data-path attribute
- CI healthcheck port mismatch (3000 -> 8080)
- Added security headers (X-Frame-Options, X-Content-Type-Options)
This commit is contained in:
Do Siki
2026-08-20 11:35:01 +02:00
parent 768297031d
commit c5d5198fbf
12 changed files with 132 additions and 61 deletions
+43 -30
View File
@@ -29,13 +29,13 @@ function renderPrimitive(path, val, container) {
const type = val === null ? 'null' : typeof val;
let control;
if (type === 'boolean') {
control = `<input type="checkbox" data-path="${path}" data-type="boolean" ${val ? 'checked' : ''}>`;
control = `<input type="checkbox" data-path="${esc(path)}" data-type="boolean" ${val ? 'checked' : ''}>`;
} else if (type === 'number') {
control = `<input type="number" data-path="${path}" data-type="number" value="${esc(val)}">`;
control = `<input type="number" data-path="${esc(path)}" data-type="number" value="${esc(val)}">`;
} else {
control = isLong
? `<textarea data-path="${path}" data-type="${type}" rows="${Math.min(8,Math.max(2,Math.ceil(String(val).length/80)))}">${esc(val ?? '')}<\/textarea>`
: `<input type="text" data-path="${path}" data-type="${type}" value="${esc(val ?? '')}">`;
? `<textarea data-path="${esc(path)}" data-type="${type}" rows="${Math.min(8,Math.max(2,Math.ceil(String(val).length/80)))}">${esc(val ?? '')}<\/textarea>`
: `<input type="text" data-path="${esc(path)}" data-type="${type}" value="${esc(val ?? '')}">`;
}
div.innerHTML = `
<label>${path}</label>
@@ -242,41 +242,54 @@ function parsePath(path) {
}
async function save() {
const data = collect();
const res = await fetch('/save?file=' + FILE, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': CSRF_TOKEN, 'X-Content-Hash': CONTENT_HASH },
body: JSON.stringify(data, null, 2)
});
if (res.status === 401) { location.href = '/login'; return; }
if (res.status === 409) {
if (confirm('A tartalom megváltozott, mióta ez a lap megnyílt (pl. deploy vagy másik fül mentett).\n\nOK = lap frissítése az új tartalommal (a szerkesztésed elvész)\nMégse = maradsz ezen a lapon, a mentés nem történt meg.')) {
location.reload();
}
return;
}
const json = await res.json();
const status = document.getElementById('saveStatus');
if (json.ok) {
// Refresh the optimistic-lock fingerprint with the server-computed hash of
// the saved content, so the user's own subsequent saves don't trip 409.
if (json.contentHash) CONTENT_HASH = json.contentHash;
status.textContent = '✅ Mentve!';
status.style.color = '#10b981';
} else {
status.textContent = '❌ Hiba: ' + json.error;
try {
const data = collect();
const res = await fetch('/save?file=' + FILE, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': CSRF_TOKEN, 'X-Content-Hash': CONTENT_HASH },
body: JSON.stringify(data, null, 2)
});
if (res.status === 401) { location.href = '/login'; return false; }
if (res.status === 409) {
if (confirm('A tartalom megváltozott, mióta ez a lap megnyílt (pl. deploy vagy másik fül mentett).\n\nOK = lap frissítése az új tartalommal (a szerkesztésed elvész)\nMégse = maradsz ezen a lapon, a mentés nem történt meg.')) {
location.reload();
}
return false;
}
const json = await res.json();
if (json.ok) {
// Refresh the optimistic-lock fingerprint with the server-computed hash of
// the saved content, so the user's own subsequent saves don't trip 409.
if (json.contentHash) CONTENT_HASH = json.contentHash;
status.textContent = '✅ Mentve!';
status.style.color = '#10b981';
status.style.visibility = 'visible';
setTimeout(() => status.style.visibility = 'hidden', 3000);
return true;
} else {
status.textContent = '❌ Hiba: ' + json.error;
status.style.color = '#f87171';
status.style.visibility = 'visible';
setTimeout(() => status.style.visibility = 'hidden', 5000);
return false;
}
} catch (e) {
status.textContent = '❌ Hálózati hiba mentéskor';
status.style.color = '#f87171';
status.style.visibility = 'visible';
setTimeout(() => status.style.visibility = 'hidden', 5000);
return false;
}
status.style.visibility = 'visible';
setTimeout(() => status.style.visibility = 'hidden', 3000);
}
async function publish() {
const btn = document.getElementById('publishBtn');
const status = document.getElementById('saveStatus');
// Save first
await save();
// Save first — abort publish if save failed (e.g. 409 conflict, validation error)
const saved = await save();
if (!saved) return;
// WHY: lock the button width and remember the label so the running state
// neither resizes the bottom bar nor permanently swaps the env-specific label.