diff --git a/Remote/home-modern.html b/Remote/home-modern.html new file mode 100644 index 00000000..9bf151ce --- /dev/null +++ b/Remote/home-modern.html @@ -0,0 +1,115 @@ + +
+

<%= text_welcome || 'Welcome to Envy Remote' %>

+ +
+

<%= text_homeinfo || 'This modern web interface allows you to control searches, downloads, uploads and network connections with enhanced features and real-time updates.' %>

+
+ + +
+ +
+
+

<%= text_homestart || 'Quick Actions' %>

+
+
+
+
+ + +
+
+ + +
+ +
+
+
+ + +
+
+

<%= text_homedirect || 'Direct Download' %>

+
+
+
+
+ + +
+ +
+
+
+
+ + +
+

Network Statistics

+
+
+
+

0

+

Active Downloads

+
+
+
+
+

0

+

Active Uploads

+
+
+
+
+

0

+

Connected Peers

+
+
+
+
+ + +
+

Recent Activity

+
+
+
+

Loading recent activity...

+
+
+
+ + +
+

<%= text_homelinks || 'Useful Links' %>

+ +
+
diff --git a/Remote/login-modern.html b/Remote/login-modern.html new file mode 100644 index 00000000..16f7e199 --- /dev/null +++ b/Remote/login-modern.html @@ -0,0 +1,73 @@ + +
+ +
diff --git a/Remote/modern-inline.css b/Remote/modern-inline.css new file mode 100644 index 00000000..05c8843b --- /dev/null +++ b/Remote/modern-inline.css @@ -0,0 +1,8 @@ +.loading-screen{position:fixed;top:0;left:0;width:100%;height:100%;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);display:flex;align-items:center;justify-content:center;z-index:9999;transition:opacity .3s ease}.loading-screen.hidden{opacity:0;pointer-events:none}.loading-content{text-align:center;color:#fff}.loading-spinner{width:40px;height:40px;border:4px solid rgba(255,255,255,.3);border-top:4px solid #fff;border-radius:50%;animation:spin 1s linear infinite;margin:0 auto 1rem}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}} +.footer{background:#2c3e50;color:#fff;padding:2rem 0 1rem;margin-top:3rem}.footer-content{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:2rem;margin-bottom:2rem}.footer-section h4{color:#3498db;margin-bottom:1rem;font-size:1.1rem}.footer-section ul{list-style:none;padding:0;margin:0}.footer-section li{margin-bottom:.5rem}.footer-section a{color:#ecf0f1;text-decoration:none;transition:color .3s ease}.footer-section a:hover{color:#3498db}.footer-bottom{border-top:1px solid #34495e;padding-top:1rem;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem}.footer-bottom p{margin:0;color:#95a5a6;font-size:.9rem}.footer-status{display:flex;align-items:center;gap:1rem;font-size:.9rem}.status-indicator{display:flex;align-items:center;gap:.5rem}.status-dot{width:8px;height:8px;border-radius:50%;background:#27ae60}.status-offline .status-dot{background:#e74c3c}.last-updated{color:#95a5a6} +.modal{display:none;position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000}.modal[aria-hidden="false"]{display:block}.modal-backdrop{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5)}.modal-content{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,.3);max-width:500px;width:90%;max-height:90vh;overflow-y:auto}.modal-header{padding:1.5rem;border-bottom:1px solid #dee2e6;display:flex;justify-content:space-between;align-items:center}.modal-close{background:none;border:none;font-size:1.5rem;cursor:pointer;width:30px;height:30px}.modal-body{padding:1.5rem}.modal-footer{padding:1rem 1.5rem;border-top:1px solid #dee2e6;display:flex;gap:.5rem;justify-content:flex-end} +.login-container{max-width:500px;margin:2rem auto;display:grid;gap:2rem;grid-template-columns:1fr}.login-card{background:#fff;border-radius:12px;box-shadow:0 10px 30px rgba(0,0,0,.1);overflow:hidden}.login-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;padding:2rem;text-align:center}.login-header h1{margin:0 0 1rem;font-size:1.8rem}.login-form{padding:2rem}.btn-block{width:100%;margin-top:1rem}.checkbox-label{display:flex;align-items:center;gap:.5rem;cursor:pointer}.form-text{color:#6c757d;font-size:.8rem;margin-top:.25rem}.login-footer{padding:1.5rem 2rem;background:#f8f9fa;border-top:1px solid #dee2e6;text-align:center}.login-info{margin:1rem 0 0;font-size:.8rem;color:#6c757d}.security-notice{background:#fff3cd;border:1px solid #ffeaa7;border-radius:8px;padding:1.5rem;margin-top:1rem}.login-form.submitting{position:relative}.login-form.submitting::after{content:'';position:absolute;inset:0;background:rgba(255,255,255,.8);z-index:10}.login-form.submitting .btn{pointer-events:none} +.link-list{list-style:none;padding:0;margin:0}.link-list li{margin-bottom:.5rem}.link-list a{color:#007bff;text-decoration:none;padding:.5rem;display:block;border-radius:4px;transition:background-color .3s ease}.link-list a:hover{background-color:#f8f9fa;text-decoration:underline}.activity-item{padding:.75rem;border-bottom:1px solid #dee2e6;display:flex;justify-content:space-between;align-items:center}.activity-text{flex:1;margin-left:1rem} +.nav-toggle{display:none;background:none;border:none;font-size:1.5rem;color:#007bff;cursor:pointer}.nav-toggle-visible{display:block}.nav-tabs-mobile-hidden{display:none}.nav-tabs-mobile-open{display:flex!important;flex-direction:column;position:absolute;top:100%;left:0;right:0;background:#fff;box-shadow:0 2px 10px rgba(0,0,0,.1)} +.loading-indicator{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,.8);color:#fff;padding:1rem;border-radius:8px;z-index:10000;display:none}.envy-toast{position:fixed;top:20px;right:20px;max-width:400px;z-index:10001;animation:slideIn .3s ease;display:flex;justify-content:space-between;gap:.5rem} +@media (max-width:768px){.login-container{margin:1rem;gap:1rem}.login-header,.login-form,.login-footer{padding:1.5rem}.activity-item{flex-direction:column;align-items:flex-start}} diff --git a/Remote/modern-page-handlers.js b/Remote/modern-page-handlers.js new file mode 100644 index 00000000..913152e0 --- /dev/null +++ b/Remote/modern-page-handlers.js @@ -0,0 +1,195 @@ +(function() { + 'use strict'; + + function closeModal(modalId) { + const modal = document.getElementById(modalId); + if (!modal) { + return; + } + + modal.setAttribute('aria-hidden', 'true'); + } + + + function showModal(modalId) { + const modal = document.getElementById(modalId); + if (modal) { + modal.setAttribute('aria-hidden', 'false'); + } + } + + function showConfirm(message, callback) { + const content = document.getElementById('confirm-content'); + const okBtn = document.getElementById('confirm-ok-btn'); + if (!content || !okBtn) { + return; + } + + content.textContent = String(message || 'Are you sure?'); + okBtn.onclick = function() { + callback(); + closeModal('confirm-modal'); + }; + + showModal('confirm-modal'); + } + + function updateConnectionStatus() { + const statusElement = document.getElementById('connection-status'); + const timestamp = document.querySelector('#last-updated [data-timestamp]'); + + if (!statusElement) { + return; + } + + const label = navigator.onLine ? 'Online' : 'Offline'; + const statusDot = statusElement.querySelector('.status-dot'); + + statusElement.className = `status-indicator ${navigator.onLine ? 'status-online' : 'status-offline'}`; + + if (statusDot) { + statusElement.replaceChildren(statusDot, document.createTextNode(` ${label}`)); + } else { + statusElement.textContent = label; + } + + if (timestamp) { + timestamp.setAttribute('data-timestamp', Date.now()); + timestamp.textContent = 'Just now'; + } + } + + function wireModalButtons() { + document.querySelectorAll('[data-close-modal]').forEach(button => { + button.addEventListener('click', () => { + closeModal(button.getAttribute('data-close-modal')); + }); + }); + } + + function wireExternalLinks() { + document.querySelectorAll('a.external-link').forEach(link => { + link.addEventListener('click', event => { + event.preventDefault(); + window.open(link.href, '_blank', 'noopener,noreferrer'); + }); + }); + } + + function wireForgotPassword() { + const forgotLink = document.querySelector('[data-forgot-password]'); + if (!forgotLink) { + return; + } + + forgotLink.addEventListener('click', event => { + event.preventDefault(); + if (window.EnvyRemote) { + window.EnvyRemote.showNotification('Password recovery is not implemented in this demo. Please contact your administrator.', 'info'); + } + }); + } + + function wireLoginForm() { + const form = document.querySelector('.login-form'); + if (!form) { + return; + } + + const submitBtn = form.querySelector('.btn'); + form.addEventListener('submit', function(event) { + const username = document.getElementById('username'); + const password = document.getElementById('password'); + const usernameValue = username ? username.value.trim() : ''; + const passwordValue = password ? password.value : ''; + + if (!usernameValue || !passwordValue) { + event.preventDefault(); + if (window.EnvyRemote) { + window.EnvyRemote.showNotification('Please enter both username and password.', 'error'); + } + return; + } + + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.textContent = 'Logging in...'; + } + form.classList.add('submitting'); + }); + } + + function wireActivityFeed() { + const container = document.getElementById('recent-activity'); + if (!container || !window.EnvyRemote) { + return; + } + + function formatTimeAgo(diff) { + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + if (days > 0) return `${days}d ago`; + if (hours > 0) return `${hours}h ago`; + if (minutes > 0) return `${minutes}m ago`; + return `${seconds}s ago`; + } + + function renderActivities(activities) { + const body = document.createElement('div'); + body.className = 'card-body'; + + if (!Array.isArray(activities) || activities.length === 0) { + const none = document.createElement('p'); + none.textContent = 'No recent activity.'; + body.appendChild(none); + } else { + activities.forEach(activity => { + const item = document.createElement('div'); + item.className = 'activity-item'; + + const time = document.createElement('span'); + time.className = 'activity-time'; + time.textContent = formatTimeAgo(Date.now() - Number(activity.timestamp || 0)); + + const text = document.createElement('span'); + text.className = 'activity-text'; + text.textContent = String(activity.description || ''); + + item.appendChild(time); + item.appendChild(text); + body.appendChild(item); + }); + } + + container.replaceChildren(body); + } + + function loadRecentActivity() { + window.EnvyRemote.ajaxRequest('/api/recent-activity') + .then(data => renderActivities(data.activities)) + .catch(() => renderActivities([])); + } + + loadRecentActivity(); + const interval = setInterval(loadRecentActivity, 30000); + window.addEventListener('beforeunload', () => clearInterval(interval)); + } + + window.closeModal = closeModal; + window.showModal = showModal; + window.showConfirm = showConfirm; + + document.addEventListener('DOMContentLoaded', function() { + wireModalButtons(); + wireExternalLinks(); + wireForgotPassword(); + wireLoginForm(); + wireActivityFeed(); + updateConnectionStatus(); + setInterval(updateConnectionStatus, 30000); + window.addEventListener('online', updateConnectionStatus); + window.addEventListener('offline', updateConnectionStatus); + }); +})(); diff --git a/Remote/security-config.js b/Remote/security-config.js new file mode 100644 index 00000000..15c54663 --- /dev/null +++ b/Remote/security-config.js @@ -0,0 +1,535 @@ +/** + * Envy Remote Security Configuration + * Version 2026 - Enhanced Security Features + * + * This file contains security settings and utilities for the Envy remote interface. + * It implements modern security practices including CSRF protection, secure headers, + * session management, and input validation. + */ + +(function() { + 'use strict'; + + // Security Configuration + const SECURITY_CONFIG = { + // CSRF Protection + csrf: { + enabled: true, + tokenLength: 32, + tokenLifetime: 3600000, // 1 hour + headerName: 'X-CSRF-Token', + cookieName: 'envy_csrf_token' + }, + + // Session Management + session: { + timeout: 1800000, // 30 minutes + maxConcurrentSessions: 3, + rememberMeMaxAge: 604800000, // 7 days + secureCookies: true, + httpOnlyCookies: true, + sameSiteCookies: 'strict' + }, + + // Content Security Policy + csp: { + enabled: true, + defaultSrc: ["'self'"], + scriptSrc: ["'self'"], + styleSrc: ["'self'"], + imgSrc: ["'self'", "data:", "https:"], + fontSrc: ["'self'", "https:"], + connectSrc: ["'self'"], + frameSrc: ["'none'"], + objectSrc: ["'none'"] + }, + + // HTTPS Enforcement + https: { + enabled: true, + hstsMaxAge: 31536000, // 1 year + hstsIncludeSubdomains: true, + hstsPreload: false + }, + + // Rate Limiting + rateLimit: { + enabled: true, + windowMs: 900000, // 15 minutes + maxRequests: 100, // per window + skipSuccessfulRequests: false, + skipFailedRequests: false + }, + + // Input Validation + validation: { + maxFileNameLength: 255, + maxPathLength: 4096, + allowedFileTypes: ['torrent', 'magnet', 'http', 'https', 'ftp'], + sanitizeHtml: true + }, + + // Security Headers + headers: { + 'X-Frame-Options': 'DENY', + 'X-Content-Type-Options': 'nosniff', + 'X-XSS-Protection': '1; mode=block', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + 'Permissions-Policy': 'geolocation=(), microphone=(), camera=()' + } + }; + + // CSRF Protection Class + class CSRFProtection { + constructor(config) { + this.config = config.csrf; + this.token = null; + this.tokenExpiry = null; + } + + /** + * Generate a new CSRF token + */ + generateToken() { + this.token = window.EnvySecurityUtils + ? window.EnvySecurityUtils.generateSecureToken(this.config.tokenLength) + : this.generateFallbackToken(); + this.tokenExpiry = Date.now() + this.config.tokenLifetime; + this.storeToken(); + return this.token; + } + + generateFallbackToken() { + const array = new Uint8Array(this.config.tokenLength); + crypto.getRandomValues(array); + return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); + } + + /** + * Validate a CSRF token + */ + validateToken(token) { + if (!this.token || !token) return false; + + // Check if token matches + if (token !== this.token) return false; + + // Check if token has expired + if (Date.now() > this.tokenExpiry) { + this.clearToken(); + return false; + } + + return true; + } + + /** + * Get current token or generate new one + */ + getToken() { + if (!this.token || Date.now() > this.tokenExpiry) { + return this.generateToken(); + } + return this.token; + } + + /** + * Store token in cookie + */ + storeToken() { + if (typeof document !== 'undefined') { + const cookieOptions = `path=/; max-age=${this.config.tokenLifetime / 1000}; SameSite=${SECURITY_CONFIG.session.sameSiteCookies}`; + document.cookie = `${this.config.cookieName}=${this.token}; ${cookieOptions}`; + } + } + + /** + * Clear stored token + */ + clearToken() { + this.token = null; + this.tokenExpiry = null; + if (typeof document !== 'undefined') { + document.cookie = `${this.config.cookieName}=; path=/; max-age=0`; + } + } + + /** + * Add CSRF token to form + */ + addToForm(form) { + const token = this.getToken(); + let csrfInput = form.querySelector('input[name="csrf_token"]'); + + if (!csrfInput) { + csrfInput = document.createElement('input'); + csrfInput.type = 'hidden'; + csrfInput.name = 'csrf_token'; + form.appendChild(csrfInput); + } + + csrfInput.value = token; + } + } + + // Session Management Class + class SessionManager { + constructor(config) { + this.config = config.session; + this.sessionId = null; + this.lastActivity = Date.now(); + this.heartbeatInterval = null; + } + + /** + * Initialize session + */ + initialize() { + this.sessionId = this.getStoredSessionId() || this.generateSessionId(); + this.storeSessionId(); + this.startHeartbeat(); + this.setupActivityTracking(); + } + + /** + * Generate new session ID + */ + generateSessionId() { + const array = new Uint8Array(16); + crypto.getRandomValues(array); + return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); + } + + /** + * Get stored session ID + */ + getStoredSessionId() { + const cookieName = 'envy_session_id'; + const cookies = document.cookie.split(';'); + for (let cookie of cookies) { + const [name, value] = cookie.trim().split('='); + if (name === cookieName) { + return value; + } + } + return null; + } + + /** + * Store session ID + */ + storeSessionId() { + const cookieOptions = `path=/; max-age=${this.config.timeout / 1000}; SameSite=${this.config.sameSiteCookies}`; + document.cookie = `envy_session_id=${this.sessionId}; ${cookieOptions}`; + } + + /** + * Start heartbeat to keep session alive + */ + startHeartbeat() { + this.heartbeatInterval = setInterval(() => { + this.sendHeartbeat(); + }, 60000); // Every minute + } + + /** + * Send heartbeat to server + */ + sendHeartbeat() { + if (window.EnvyRemote) { + EnvyRemote.ajaxRequest('/api/session/heartbeat', { + method: 'POST', + headers: { + 'X-Session-ID': this.sessionId + } + }).catch(error => { + console.warn('Heartbeat failed:', error); + }); + } + } + + /** + * Setup activity tracking + */ + setupActivityTracking() { + const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart']; + const updateActivity = () => { + this.lastActivity = Date.now(); + }; + + events.forEach(event => { + document.addEventListener(event, updateActivity, { passive: true }); + }); + + // Check for inactivity every minute + setInterval(() => { + const inactiveTime = Date.now() - this.lastActivity; + if (inactiveTime > this.config.timeout) { + this.handleSessionTimeout(); + } + }, 60000); + } + + /** + * Handle session timeout + */ + handleSessionTimeout() { + if (window.EnvyRemote) { + EnvyRemote.showNotification('Session expired. Please login again.', 'error'); + setTimeout(() => { + window.location.href = '/remote/login'; + }, 3000); + } + } + + /** + * Logout and clear session + */ + logout() { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + } + + // Clear session cookie + document.cookie = 'envy_session_id=; path=/; max-age=0'; + + // Clear CSRF token + if (window.csrfProtection) { + window.csrfProtection.clearToken(); + } + + // Redirect to login + window.location.href = '/remote/logout'; + } + + /** + * Get session info + */ + getSessionInfo() { + return { + sessionId: this.sessionId, + lastActivity: this.lastActivity, + timeRemaining: this.config.timeout - (Date.now() - this.lastActivity) + }; + } + } + + // Input Validation Class + class InputValidator { + constructor(config) { + this.config = config.validation; + } + + /** + * Validate filename + */ + validateFileName(filename) { + if (!filename || typeof filename !== 'string') return false; + if (filename.length > this.config.maxFileNameLength) return false; + if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) return false; + + // Check for dangerous characters + const dangerousChars = /[<>\"\'\|\?\*\x00-\x1f]/; + return !dangerousChars.test(filename); + } + + /** + * Validate URL + */ + validateUrl(url) { + if (!url || typeof url !== 'string') return false; + + try { + const parsedUrl = new URL(url); + + // Check protocol + if (!this.config.allowedFileTypes.includes(parsedUrl.protocol.replace(':', ''))) { + return false; + } + + // Check for localhost/private IPs (basic protection) + const hostname = parsedUrl.hostname; + if (hostname === 'localhost' || hostname === '127.0.0.1' || + hostname.startsWith('192.168.') || hostname.startsWith('10.') || + hostname.startsWith('172.')) { + return false; + } + + return true; + } catch (e) { + return false; + } + } + + /** + * Sanitize HTML input + */ + sanitizeHtml(input) { + if (!this.config.sanitizeHtml || !input) return input; + + // Basic HTML sanitization + return input + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g, '/'); + } + + /** + * Validate search query + */ + validateSearchQuery(query) { + if (!query || typeof query !== 'string') return false; + if (query.length > 500) return false; // Reasonable limit + + // Check for SQL injection patterns (basic) + const sqlPatterns = /(\bUNION\b|\bSELECT\b|\bINSERT\b|\bDELETE\b|\bUPDATE\b|\bDROP\b)/i; + return !sqlPatterns.test(query); + } + } + + // Security Headers Helper + class SecurityHeaders { + constructor(config) { + this.config = config; + } + + /** + * Set security headers + */ + setHeaders() { + if (typeof document === 'undefined') return; + + // Set meta tags for additional security + const metaTags = [ + { name: 'referrer', content: 'strict-origin-when-cross-origin' }, + { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge' } + ]; + + metaTags.forEach(tag => { + const meta = document.createElement('meta'); + Object.keys(tag).forEach(key => { + meta.setAttribute(key, tag[key]); + }); + document.head.appendChild(meta); + }); + } + + /** + * Generate CSP header + */ + generateCSP() { + if (!this.config.csp.enabled) return ''; + + const csp = this.config.csp; + const directives = []; + + if (csp.defaultSrc) directives.push(`default-src ${csp.defaultSrc.join(' ')}`); + if (csp.scriptSrc) directives.push(`script-src ${csp.scriptSrc.join(' ')}`); + if (csp.styleSrc) directives.push(`style-src ${csp.styleSrc.join(' ')}`); + if (csp.imgSrc) directives.push(`img-src ${csp.imgSrc.join(' ')}`); + if (csp.connectSrc) directives.push(`connect-src ${csp.connectSrc.join(' ')}`); + if (csp.frameSrc) directives.push(`frame-src ${csp.frameSrc.join(' ')}`); + if (csp.objectSrc) directives.push(`object-src ${csp.objectSrc.join(' ')}`); + + return directives.join('; '); + } + } + + // Rate Limiting Class + class RateLimiter { + constructor(config) { + this.config = config.rateLimit; + this.requests = []; + } + + /** + * Check if request should be allowed + */ + checkLimit() { + if (!this.config.enabled) return true; + + const now = Date.now(); + const windowStart = now - this.config.windowMs; + + // Remove old requests + this.requests = this.requests.filter(time => time > windowStart); + + // Check if under limit + if (this.requests.length >= this.config.maxRequests) { + return false; + } + + // Add current request + this.requests.push(now); + return true; + } + + /** + * Get remaining requests in current window + */ + getRemainingRequests() { + const now = Date.now(); + const windowStart = now - this.config.windowMs; + const recentRequests = this.requests.filter(time => time > windowStart); + return Math.max(0, this.config.maxRequests - recentRequests.length); + } + + /** + * Get time until reset + */ + getTimeUntilReset() { + if (this.requests.length === 0) return 0; + const oldestRequest = Math.min(...this.requests); + const resetTime = oldestRequest + this.config.windowMs; + return Math.max(0, resetTime - Date.now()); + } + } + + // Initialize Security Components + const csrfProtection = new CSRFProtection(SECURITY_CONFIG); + const sessionManager = new SessionManager(SECURITY_CONFIG); + const inputValidator = new InputValidator(SECURITY_CONFIG); + const securityHeaders = new SecurityHeaders(SECURITY_CONFIG); + const rateLimiter = new RateLimiter(SECURITY_CONFIG); + + // Global Security API + window.EnvySecurity = { + // CSRF Protection + getCSRFToken: () => csrfProtection.getToken(), + validateCSRFToken: (token) => csrfProtection.validateToken(token), + + // Session Management + initializeSession: () => sessionManager.initialize(), + logout: () => sessionManager.logout(), + getSessionInfo: () => sessionManager.getSessionInfo(), + + // Input Validation + validateFileName: (name) => inputValidator.validateFileName(name), + validateUrl: (url) => inputValidator.validateUrl(url), + validateSearchQuery: (query) => inputValidator.validateSearchQuery(query), + sanitizeHtml: (input) => inputValidator.sanitizeHtml(input), + + // Rate Limiting + checkRateLimit: () => rateLimiter.checkLimit(), + getRemainingRequests: () => rateLimiter.getRemainingRequests(), + getTimeUntilReset: () => rateLimiter.getTimeUntilReset(), + + // Security Headers + generateCSP: () => securityHeaders.generateCSP(), + setSecurityHeaders: () => securityHeaders.setHeaders(), + + // Configuration + config: SECURITY_CONFIG + }; + + // Auto-initialize on page load + if (typeof document !== 'undefined') { + document.addEventListener('DOMContentLoaded', function() { + window.EnvySecurity.initializeSession(); + window.EnvySecurity.setSecurityHeaders(); + }); + } + +})(); diff --git a/Remote/security-fix-checklist.md b/Remote/security-fix-checklist.md new file mode 100644 index 00000000..4e01ed55 --- /dev/null +++ b/Remote/security-fix-checklist.md @@ -0,0 +1,10 @@ +# Remote Security Fix Checklist (2026-04-22) + +- `Remote/envy-modern.js:134,172` → Validate redirect targets against internal allowlist before navigation/fallback. +- `Remote/envy-modern.js:238,282,323,560` → Remove unsafe DOM sinks (`innerHTML`) or sanitize via DOMPurify allowlist. +- `Remote/envy-modern.js:652` → Replace `Math.random()` CSRF generation with `crypto.getRandomValues()` base64url tokens and rotate safely. +- `Remote/envy-modern.js:686-696` → Validate dynamic API params and response shape with typed validation errors. +- `Remote/security-config.js:38-39` → Remove `'unsafe-inline'` from CSP directives. +- `Remote/security-config.js:466-470` → Fix rate limiter config path references to `this.config.windowMs` / `this.config.maxRequests`. +- `Remote/head-modern.html`, `Remote/tail-modern.html`, `Remote/login-modern.html`, `Remote/home-modern.html` → Remove inline handlers/scripts/styles and wire external JS/CSS compatible with strict CSP. +- `Remote/tests/*` → Add Jest regression tests for CSRF, sanitization, redirects, rate limiter, input validation. diff --git a/Remote/security-utils.js b/Remote/security-utils.js new file mode 100644 index 00000000..e97835d8 --- /dev/null +++ b/Remote/security-utils.js @@ -0,0 +1,143 @@ +(function(global) { + 'use strict'; + + class ValidationError extends Error { + constructor(code, message) { + super(message); + this.name = 'ValidationError'; + this.code = code; + } + } + + function getCrypto() { + if (global.crypto && typeof global.crypto.getRandomValues === 'function') { + return global.crypto; + } + + if (typeof require === 'function') { + const nodeCrypto = require('crypto'); + if (nodeCrypto.webcrypto && typeof nodeCrypto.webcrypto.getRandomValues === 'function') { + return nodeCrypto.webcrypto; + } + } + + throw new Error('Secure crypto API is unavailable'); + } + + function toBase64Url(uint8Array) { + let binary = ''; + for (let i = 0; i < uint8Array.length; i += 1) { + binary += String.fromCharCode(uint8Array[i]); + } + + const encoded = typeof btoa === 'function' + ? btoa(binary) + : Buffer.from(binary, 'binary').toString('base64'); + return encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); + } + + function generateSecureToken(byteLength) { + const length = Number.isInteger(byteLength) ? byteLength : 16; + if (length < 16) { + throw new ValidationError('TOKEN_LENGTH', 'Token byte length must be at least 16 bytes'); + } + + const bytes = new Uint8Array(length); + getCrypto().getRandomValues(bytes); + return toBase64Url(bytes); + } + + function validateRedirectTarget(target, allowlist) { + if (typeof target !== 'string') { + return null; + } + + const value = target.trim(); + if (!value || value.startsWith('//')) { + return null; + } + + if (/^(?:[a-z][a-z0-9+.-]*:)/i.test(value)) { + return null; + } + + if (!value.startsWith('/')) { + return null; + } + + const validPrefixes = allowlist || ['/remote', '/api']; + const isAllowed = validPrefixes.some(prefix => value === prefix || value.startsWith(prefix + '/') || (prefix === '/' && value.startsWith('/'))); + + return isAllowed ? value : null; + } + + function validateApiSegment(segment, fieldName) { + if (typeof segment !== 'string') { + throw new ValidationError('INVALID_TYPE', `${fieldName} must be a string`); + } + + if (!segment || segment.length > 64) { + throw new ValidationError('INVALID_LENGTH', `${fieldName} length is out of bounds`); + } + + if (!/^[A-Za-z0-9_-]+$/.test(segment)) { + throw new ValidationError('INVALID_CHARS', `${fieldName} contains invalid characters`); + } + + return segment; + } + + + function validateHttpMethod(method) { + const normalized = String(method || '').toUpperCase(); + if (normalized !== 'GET' && normalized !== 'POST') { + throw new ValidationError('INVALID_METHOD', 'Only GET/POST are supported'); + } + return normalized; + } + + function sanitizeHTML(html) { + if (typeof html !== 'string') { + return ''; + } + + if (global.DOMPurify && typeof global.DOMPurify.sanitize === 'function') { + return global.DOMPurify.sanitize(html, { + ALLOWED_TAGS: [ + 'div', 'span', 'p', 'br', 'hr', 'small', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', + 'strong', 'em', 'b', 'i', 'u', 'a', 'table', 'caption', 'thead', 'tbody', 'tfoot', + 'tr', 'th', 'td', 'button', 'input', 'select', 'option', 'optgroup', 'textarea', + 'form', 'label', 'fieldset', 'legend', 'section', 'article', 'header', 'footer', + 'main', 'nav', 'aside', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' + ], + ALLOWED_ATTR: [ + 'class', 'id', 'href', 'title', 'target', 'rel', + 'aria-label', 'aria-labelledby', 'aria-describedby', 'aria-current', 'aria-hidden', + 'type', 'name', 'value', 'placeholder', 'checked', 'selected', 'disabled', + 'readonly', 'required', 'multiple', 'for', 'role', 'method', 'action' + ], + ALLOW_DATA_ATTR: true + }); + } + + return html + .replace(/&/g, '&') + .replace(//g, '>'); + } + + const api = { + ValidationError, + generateSecureToken, + validateRedirectTarget, + validateApiSegment, + sanitizeHTML, + validateHttpMethod + }; + + global.EnvySecurityUtils = api; + + if (typeof module !== 'undefined' && module.exports) { + module.exports = api; + } +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/Remote/tail-modern.html b/Remote/tail-modern.html new file mode 100644 index 00000000..4443e279 --- /dev/null +++ b/Remote/tail-modern.html @@ -0,0 +1,91 @@ +