fix(security): patch 5 vulnerabilities from security audit
- mask API keys in usage stats/history responses (apiKeyMasked) - validate proxy URL scheme + reject shell metachars before env write - escape HTML in OAuth callback page to prevent XSS - atomic O_EXCL lock file to prevent TOCTOU race in MITM startServer - set mitmIsRestarting guard synchronously before any await Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -41,6 +41,7 @@ async function resolveMitmRouterBaseUrl() {
|
||||
const MITM_PORT = 443;
|
||||
const MITM_WIN_NODE_PORT = 8443;
|
||||
const PID_FILE = path.join(MITM_DIR, ".mitm.pid");
|
||||
const LOCK_FILE = path.join(MITM_DIR, ".mitm.lock");
|
||||
|
||||
const MITM_MAX_RESTARTS = 5;
|
||||
const MITM_RESTART_DELAYS_MS = [5000, 10000, 20000, 30000, 60000];
|
||||
@@ -400,19 +401,22 @@ async function getMitmStatus() {
|
||||
|
||||
async function scheduleMitmRestart(apiKey) {
|
||||
if (mitmIsRestarting) return;
|
||||
// Set guard synchronously before any await to prevent concurrent calls
|
||||
// from passing the check above.
|
||||
mitmIsRestarting = true;
|
||||
|
||||
const aliveMs = Date.now() - mitmLastStartTime;
|
||||
if (aliveMs >= MITM_RESTART_RESET_MS) mitmRestartCount = 0;
|
||||
|
||||
if (mitmRestartCount >= MITM_MAX_RESTARTS) {
|
||||
err("Max restart attempts reached. Giving up.");
|
||||
mitmIsRestarting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const attempt = mitmRestartCount;
|
||||
const delay = MITM_RESTART_DELAYS_MS[Math.min(attempt, MITM_RESTART_DELAYS_MS.length - 1)];
|
||||
mitmRestartCount++;
|
||||
mitmIsRestarting = true;
|
||||
|
||||
log(`Restarting in ${delay / 1000}s... (${mitmRestartCount}/${MITM_MAX_RESTARTS})`);
|
||||
await new Promise((r) => setTimeout(r, delay));
|
||||
@@ -486,7 +490,19 @@ async function startServer(apiKey, sudoPassword, forceKillPort443 = false) {
|
||||
throw new Error("MITM server is already running");
|
||||
}
|
||||
|
||||
await killLeftoverMitm(sudoPassword);
|
||||
// Atomically claim lock to prevent concurrent startServer across processes.
|
||||
// O_EXCL (flag: "wx") fails with EEXIST if the file already exists.
|
||||
try {
|
||||
fs.writeFileSync(LOCK_FILE, String(process.pid), { flag: "wx" });
|
||||
} catch (e) {
|
||||
if (e.code === "EEXIST") {
|
||||
throw new Error("MITM server is already starting (lock contention)");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
await killLeftoverMitm(sudoPassword);
|
||||
|
||||
if (!IS_WIN) {
|
||||
const portStatus = await checkPort443Free();
|
||||
@@ -679,6 +695,7 @@ async function startServer(apiKey, sudoPassword, forceKillPort443 = false) {
|
||||
serverProcess = null;
|
||||
serverPid = null;
|
||||
try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
|
||||
try { fs.unlinkSync(LOCK_FILE); } catch { /* ignore */ }
|
||||
// Auto-restart on unexpected exit
|
||||
if (code !== 0 && !mitmIsRestarting) scheduleMitmRestart(apiKey);
|
||||
});
|
||||
@@ -706,7 +723,15 @@ async function startServer(apiKey, sudoPassword, forceKillPort443 = false) {
|
||||
await saveMitmSettings(true, sudoPassword);
|
||||
if (sudoPassword) setCachedPassword(sudoPassword);
|
||||
|
||||
// Server is healthy — remove lock file (PID file persists as the marker)
|
||||
try { fs.unlinkSync(LOCK_FILE); } catch { /* ignore */ }
|
||||
|
||||
return { running: true, pid: serverPid };
|
||||
} catch (e) {
|
||||
// Clean up lock on any failure
|
||||
try { fs.unlinkSync(LOCK_FILE); } catch { /* ignore */ }
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -779,6 +804,7 @@ async function stopServer(sudoPassword) {
|
||||
}
|
||||
|
||||
try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
|
||||
try { fs.unlinkSync(LOCK_FILE); } catch { /* ignore */ }
|
||||
await saveMitmSettings(false, null);
|
||||
mitmIsRestarting = false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user