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
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:
+28
-13
@@ -30,6 +30,7 @@ const GUIDE_FILE = process.env.CONTENT_EDITOR_GUIDE_FILE || path.join(__dirname,
|
||||
const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000;
|
||||
const AUTH_MAX_ATTEMPTS = 5;
|
||||
const PUBLISH_MAX_ATTEMPTS = 3;
|
||||
let isPublishing = false;
|
||||
|
||||
const FILES = {
|
||||
common: path.join(CONTENT_DIR, 'common.json'),
|
||||
@@ -65,6 +66,8 @@ const clientJs = fs.readFileSync(path.join(__dirname, 'scripts', 'cms-editor-cli
|
||||
const core = require('./scripts/cms-core');
|
||||
const { CMS_USER, CMS_PASS, CMS_DEPLOY_ENV, CSRF_TOKEN, securityConfigIsValid, getClientAddress, hasValidCsrfToken, backupAndWriteAtomically } = core;
|
||||
const exceedsRateLimit = (key, limit) => core.exceedsRateLimit(key, limit, RATE_LIMIT_WINDOW_MS);
|
||||
const isRateLimited = (key, limit) => core.isRateLimited(key, limit, RATE_LIMIT_WINDOW_MS);
|
||||
const recordRateLimitAttempt = key => core.recordRateLimitAttempt(key, RATE_LIMIT_WINDOW_MS);
|
||||
const hasValidCredentials = req => core.hasValidCredentials(req, validateLogin);
|
||||
const isAuthenticated = core.makeIsAuthenticated(hasValidSession, validateLogin);
|
||||
const isBrowserNavigation = core.isBrowserNavigation;
|
||||
@@ -82,6 +85,8 @@ function readDeployVersion() {
|
||||
const DEPLOY_VERSION = readDeployVersion();
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
res.setHeader('X-Frame-Options', 'DENY');
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
const clientAddress = getClientAddress(req);
|
||||
if (!securityConfigIsValid()) {
|
||||
res.writeHead(503, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
@@ -141,6 +146,12 @@ const server = http.createServer(async (req, res) => {
|
||||
body += c;
|
||||
});
|
||||
req.on('end', () => {
|
||||
if (isRateLimited(`auth:${clientAddress}`, AUTH_MAX_ATTEMPTS)) {
|
||||
writeAudit('login_failed', { clientAddress, result: 'rate_limited' });
|
||||
res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': String(RATE_LIMIT_WINDOW_MS / 1000) });
|
||||
res.end(JSON.stringify({ ok: false, error: 'Túl sok belépési kísérlet — próbáld újra később.' }));
|
||||
return;
|
||||
}
|
||||
let user = '';
|
||||
let pass = '';
|
||||
try {
|
||||
@@ -157,12 +168,7 @@ const server = http.createServer(async (req, res) => {
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
return;
|
||||
}
|
||||
if (exceedsRateLimit(`auth:${clientAddress}`, AUTH_MAX_ATTEMPTS)) {
|
||||
writeAudit('login_failed', { clientAddress, result: 'rate_limited' });
|
||||
res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': String(RATE_LIMIT_WINDOW_MS / 1000) });
|
||||
res.end(JSON.stringify({ ok: false, error: 'Túl sok belépési kísérlet — próbáld újra később.' }));
|
||||
return;
|
||||
}
|
||||
recordRateLimitAttempt(`auth:${clientAddress}`);
|
||||
writeAudit('login_failed', { clientAddress, result: bodyTooLarge ? 'request_too_large' : 'invalid_credentials' });
|
||||
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, error: 'Hibás felhasználónév vagy jelszó.' }));
|
||||
@@ -170,14 +176,15 @@ const server = http.createServer(async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRateLimited(`auth:${clientAddress}`, AUTH_MAX_ATTEMPTS)) {
|
||||
writeAudit('authentication_failed', { clientAddress, limited: true });
|
||||
res.writeHead(429, { 'Retry-After': String(RATE_LIMIT_WINDOW_MS / 1000) });
|
||||
res.end('Too many authentication attempts');
|
||||
return;
|
||||
}
|
||||
if (!isAuthenticated(req)) {
|
||||
const limited = exceedsRateLimit(`auth:${clientAddress}`, AUTH_MAX_ATTEMPTS);
|
||||
writeAudit('authentication_failed', { clientAddress, limited });
|
||||
if (limited) {
|
||||
res.writeHead(429, { 'Retry-After': String(RATE_LIMIT_WINDOW_MS / 1000) });
|
||||
res.end('Too many authentication attempts');
|
||||
return;
|
||||
}
|
||||
recordRateLimitAttempt(`auth:${clientAddress}`);
|
||||
writeAudit('authentication_failed', { clientAddress, limited: false });
|
||||
// Browser navigations land on the styled login page; API/curl gets a plain 401.
|
||||
// WHY no WWW-Authenticate: Safari pops its native auth dialog on fetch() calls
|
||||
// that receive a Basic challenge — the styled /login page handles browsers.
|
||||
@@ -283,7 +290,14 @@ const server = http.createServer(async (req, res) => {
|
||||
|
||||
// POST /publish — Git Commit, Pull Rebase & Push
|
||||
if (req.method === 'POST' && u.pathname === '/publish') {
|
||||
if (isPublishing) {
|
||||
res.writeHead(423, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, error: 'Már folyamatban van egy publikálás. Kérlek, várj.' }));
|
||||
return;
|
||||
}
|
||||
isPublishing = true;
|
||||
if (exceedsRateLimit(`publish:${clientAddress}`, PUBLISH_MAX_ATTEMPTS)) {
|
||||
isPublishing = false;
|
||||
writeAudit('publish_rate_limited', { clientAddress, user: CMS_USER });
|
||||
res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': String(RATE_LIMIT_WINDOW_MS / 1000) });
|
||||
res.end(JSON.stringify({ ok: false, error: 'Túl sok publikálási kísérlet' }));
|
||||
@@ -294,6 +308,7 @@ const server = http.createServer(async (req, res) => {
|
||||
// (WHY comments there): commit only when staged changes exist, rebase with
|
||||
// abort-on-failure, deterministic no-changes marker instead of output matching.
|
||||
exec(buildPublishCommand('content: frissítve a CMS-ből'), { cwd: CONTENT_DIR }, (error, stdout, stderr) => {
|
||||
isPublishing = false;
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
const outcome = interpretPublishResult(error, stdout, stderr);
|
||||
writeAudit('publish_finished', { clientAddress, user: CMS_USER, result: outcome.result });
|
||||
|
||||
Reference in New Issue
Block a user