Files
dashcaddy/status/js/totp-auth.js
Sami 52577b11ed Fix 7 frontend security vulnerabilities (4 critical, 3 high)
- Escape all innerHTML assignments with user/external data across 12 JS files
- Upgrade credential encryption: per-value IV, key moved to sessionStorage
- Fix open redirect in TOTP auth via proper URL hostname validation
- Remove sensitive DNS topology data from localStorage cache
- Add security regression test suite (51 tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 01:29:04 -08:00

129 lines
4.5 KiB
JavaScript

// ===== TOTP AUTHENTICATION GATE =====
(function() {
function updateTotpLogo() {
const card = document.querySelector('.totp-card');
if (!card) return;
const bg = getComputedStyle(card).backgroundColor;
const m = bg.match(/\d+/g);
if (!m) return;
const lum = (0.299 * +m[0] + 0.587 * +m[1] + 0.114 * +m[2]) / 255;
const dark = card.querySelector('.totp-logo-dark');
const light = card.querySelector('.totp-logo-light');
if (dark) dark.style.display = lum > 0.5 ? 'none' : '';
if (light) light.style.display = lum > 0.5 ? '' : 'none';
}
function showTotpOverlay() {
const overlay = document.getElementById('totp-overlay');
if (overlay) {
overlay.classList.add('show');
setTimeout(updateTotpLogo, 50);
const firstInput = overlay.querySelector('.totp-digits input');
if (firstInput) setTimeout(() => firstInput.focus(), 100);
}
}
function hideTotpOverlay() {
const overlay = document.getElementById('totp-overlay');
if (overlay) overlay.classList.remove('show');
}
// Setup digit input UX
const container = document.getElementById('totp-digits');
if (container) {
const inputs = container.querySelectorAll('input');
inputs.forEach((input, idx) => {
input.addEventListener('input', (e) => {
const val = e.target.value.replace(/\D/g, '');
e.target.value = val.slice(0, 1);
if (val && idx < inputs.length - 1) inputs[idx + 1].focus();
const code = Array.from(inputs).map(i => i.value).join('');
if (code.length === 6) submitTotpCode(code);
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && !e.target.value && idx > 0) {
inputs[idx - 1].focus();
inputs[idx - 1].value = '';
}
});
input.addEventListener('paste', (e) => {
e.preventDefault();
const pasted = (e.clipboardData.getData('text') || '').replace(/\D/g, '');
if (pasted.length >= 6) {
inputs.forEach((inp, i) => { inp.value = pasted[i] || ''; });
inputs[5].focus();
submitTotpCode(pasted.slice(0, 6));
}
});
});
}
async function submitTotpCode(code) {
const errorEl = document.getElementById('totp-error');
errorEl.textContent = 'Verifying...';
errorEl.className = 'totp-error verifying';
try {
const res = await secureFetch('/api/v1/totp/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
});
const data = await res.json();
if (data.success) {
errorEl.textContent = '';
hideTotpOverlay();
// Check if redirected here from another service
const redirect = safeSessionGet('totp_redirect');
if (redirect) {
try { sessionStorage.removeItem('totp_redirect'); } catch (_) {}
window.location.href = redirect;
return;
}
// Initialize dashboard
if (typeof window.initializeDashboard === 'function') {
window.initializeDashboard();
}
} else {
errorEl.textContent = data.error || 'Invalid code';
errorEl.className = 'totp-error';
const inputs = document.querySelectorAll('#totp-digits input');
inputs.forEach(i => { i.value = ''; });
inputs[0]?.focus();
}
} catch (e) {
errorEl.textContent = 'Connection error';
errorEl.className = 'totp-error';
}
}
// Handle ?auth=required redirect from Caddy SSO
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('auth') === 'required') {
const returnUrl = urlParams.get('return');
if (returnUrl) {
// Validate redirect URL: must be same-origin or hostname must end with our TLD
// (prevents open redirect via includes() bypass like evil.com?q=.sami)
try {
const parsed = new URL(returnUrl, window.location.origin);
const hostname = parsed.hostname;
const isSameOrigin = parsed.origin === window.location.origin;
const tldSuffix = SITE.tld.startsWith('.') ? SITE.tld : '.' + SITE.tld;
const isOurTld = hostname.endsWith(tldSuffix) || hostname === tldSuffix.substring(1);
if (isSameOrigin || isOurTld) {
safeSessionSet('totp_redirect', returnUrl);
}
} catch (_) {
// Invalid URL — reject redirect
}
}
// Clean URL
window.history.replaceState({}, '', window.location.pathname);
}
window._showTotpOverlay = showTotpOverlay;
})();