Prechádzať zdrojové kódy

security: Implement clipboard monitoring consent dialog (M5 - MEDIUM priority)

Implemented final MEDIUM priority security fix - requires explicit user consent
before enabling clipboard monitoring to address privacy concerns.

Components Created:
- ClipboardConsentDialog: Beautiful modal with clear privacy disclosure
  • Explains what data is monitored (only YouTube/Vimeo URLs)
  • Promises data stays local (never sent to servers)
  • Shows monitoring can be disabled anytime
  • "Remember my choice" checkbox for convenience
  • Allow / Don't Allow buttons with proper focus management

State Management:
- Added clipboardConsent property to AppState (null/true/false)
- hasClipboardConsent() - check if user allowed monitoring
- hasClipboardConsentAnswer() - check if user was asked
- setClipboardConsent(allowed) - save consent with persistence
- resetClipboardConsent() - clear saved preference

Consent Flow:
1. User toggles clipboard monitoring ON
2. If not asked before → Show consent dialog
3. User clicks "Allow" or "Don't Allow"
4. Optional: Check "Remember my choice"
5. Monitoring only starts if consent given
6. Future toggles respect saved preference

Security Enhancements:
✅ Explicit consent required before monitoring starts
✅ Main process validates userConsented parameter
✅ Preload bridge passes consent flag
✅ Clear privacy disclosure in dialog
✅ Consent preference persisted to localStorage
✅ Info icon with tooltip for transparency

UI Improvements:
✅ Info icon (ℹ️) next to clipboard toggle
✅ Hover tooltip: "Auto-detect Video URLs - Your privacy is protected"
✅ Beautiful dialog with checkmarks and clear messaging
✅ Keyboard accessible (ESC to deny, Tab navigation, focus on Allow)

All 259 tests passing. No regressions.

Issue: #11 (Security: M5 - Clipboard Monitoring Consent)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
jopa79 3 mesiacov pred
rodič
commit
a2d859b944

+ 17 - 3
index.html

@@ -93,13 +93,25 @@
                 </select>
 
                 <!-- Clipboard Monitoring -->
-                <div class="flex items-center gap-2 ml-4">
+                <div class="flex items-center gap-2 ml-4 group relative">
                     <span class="text-[#90a1b9] text-sm tracking-[-0.1504px]">Clipboard:</span>
                     <label class="relative inline-flex items-center cursor-pointer">
                         <input type="checkbox" id="clipboardMonitorToggle" class="sr-only peer">
                         <div class="w-9 h-5 bg-[#45556c] peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-[#155dfc]"></div>
                         <span class="ml-2 text-xs text-[#cad5e2]">Monitor</span>
                     </label>
+                    <!-- Info Icon with Tooltip -->
+                    <div class="relative">
+                        <svg class="w-4 h-4 text-[#90a1b9] hover:text-[#155dfc] transition-colors cursor-help" fill="currentColor" viewBox="0 0 20 20">
+                            <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
+                        </svg>
+                        <!-- Tooltip -->
+                        <div class="absolute hidden group-hover:block bottom-full right-0 mb-2 w-64 px-3 py-2 text-xs text-white bg-[#1d293d] border border-[#45556c] rounded-lg shadow-lg z-50">
+                            <div class="font-semibold mb-1">Auto-detect Video URLs</div>
+                            <div class="text-[#cad5e2]">Automatically detects when you copy YouTube or Vimeo links. Your privacy is protected - only video URLs are checked.</div>
+                            <div class="absolute top-full right-4 -mt-1 border-4 border-transparent border-t-[#45556c]"></div>
+                        </div>
+                    </div>
                 </div>
             </div>
 
@@ -1053,8 +1065,9 @@
             loadScript('scripts/utils/url-validator.js', () => {
                 loadScript('scripts/core/event-bus.js', () => {
                     loadScript('scripts/models/Video.js', () => {
-                        loadScript('scripts/models/AppState.js', () => {
-                            loadScript('scripts/utils/error-handler.js', () => {
+                        loadScript('scripts/components/clipboard-consent-dialog.js', () => {
+                            loadScript('scripts/models/AppState.js', () => {
+                                loadScript('scripts/utils/error-handler.js', () => {
                                 loadScript('scripts/utils/desktop-notifications.js', () => {
                                     loadScript('scripts/utils/live-region-manager.js', () => {
                                         loadScript('scripts/utils/accessibility-manager.js', () => {
@@ -1091,6 +1104,7 @@
                 });
             });
         });
+    });
     </script>
 
     <!-- Debug script -->

+ 28 - 2
scripts/app.js

@@ -1416,7 +1416,32 @@ class GrabZillaApp {
 
         try {
             if (enabled) {
-                const result = await window.electronAPI.startClipboardMonitor();
+                // Check if consent has been given
+                if (!this.state.hasClipboardConsentAnswer()) {
+                    // Show consent dialog for first time
+                    const consentDialog = new window.ClipboardConsentDialog();
+                    const { allowed, rememberChoice } = await consentDialog.show();
+
+                    if (rememberChoice) {
+                        // Save consent preference
+                        this.state.setClipboardConsent(allowed);
+                    }
+
+                    if (!allowed) {
+                        // User denied consent
+                        document.getElementById('clipboardMonitorToggle').checked = false;
+                        this.updateStatusMessage('Clipboard monitoring requires your permission');
+                        return;
+                    }
+                } else if (!this.state.hasClipboardConsent()) {
+                    // User previously denied consent
+                    document.getElementById('clipboardMonitorToggle').checked = false;
+                    this.showError('You have previously denied clipboard monitoring permission');
+                    return;
+                }
+
+                // User has consented - start monitoring
+                const result = await window.electronAPI.startClipboardMonitor(true);
                 if (result.success) {
                     // Set up listener for detected URLs
                     window.electronAPI.onClipboardUrlDetected((event, url) => {
@@ -1424,7 +1449,7 @@ class GrabZillaApp {
                     });
                     this.updateStatusMessage('Clipboard monitoring enabled');
                 } else {
-                    this.showError('Failed to start clipboard monitoring');
+                    this.showError(result.error || 'Failed to start clipboard monitoring');
                     document.getElementById('clipboardMonitorToggle').checked = false;
                 }
             } else {
@@ -1434,6 +1459,7 @@ class GrabZillaApp {
         } catch (error) {
             logger.error('Error toggling clipboard monitor:', error.message);
             this.showError(`Clipboard monitoring error: ${error.message}`);
+            document.getElementById('clipboardMonitorToggle').checked = false;
         }
     }
 

+ 165 - 0
scripts/components/clipboard-consent-dialog.js

@@ -0,0 +1,165 @@
+/**
+ * Clipboard Monitoring Consent Dialog
+ *
+ * Displays a modal dialog requesting user consent before enabling clipboard monitoring.
+ * Complies with privacy best practices by providing clear disclosure.
+ */
+
+class ClipboardConsentDialog {
+    constructor() {
+        this.dialog = null;
+        this.overlay = null;
+        this.onAllow = null;
+        this.onDeny = null;
+    }
+
+    /**
+     * Show the consent dialog
+     * @returns {Promise<{allowed: boolean, rememberChoice: boolean}>}
+     */
+    show() {
+        return new Promise((resolve) => {
+            this.createDialog();
+
+            // Set up button handlers
+            const allowBtn = this.dialog.querySelector('#clipboard-consent-allow');
+            const denyBtn = this.dialog.querySelector('#clipboard-consent-deny');
+            const rememberCheckbox = this.dialog.querySelector('#clipboard-consent-remember');
+
+            const handleResponse = (allowed) => {
+                const rememberChoice = rememberCheckbox.checked;
+                this.remove();
+                resolve({ allowed, rememberChoice });
+            };
+
+            allowBtn.addEventListener('click', () => handleResponse(true));
+            denyBtn.addEventListener('click', () => handleResponse(false));
+
+            // ESC key to deny
+            const handleEscape = (e) => {
+                if (e.key === 'Escape') {
+                    handleResponse(false);
+                    document.removeEventListener('keydown', handleEscape);
+                }
+            };
+            document.addEventListener('keydown', handleEscape);
+
+            // Show dialog
+            document.body.appendChild(this.overlay);
+            document.body.appendChild(this.dialog);
+
+            // Focus allow button
+            setTimeout(() => allowBtn.focus(), 100);
+        });
+    }
+
+    /**
+     * Create the dialog HTML
+     */
+    createDialog() {
+        // Create overlay
+        this.overlay = document.createElement('div');
+        this.overlay.className = 'fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center';
+        this.overlay.style.backdropFilter = 'blur(4px)';
+
+        // Create dialog
+        this.dialog = document.createElement('div');
+        this.dialog.className = 'bg-[#1d293d] border border-[#45556c] rounded-lg shadow-xl max-w-md w-full mx-4 z-50';
+        this.dialog.setAttribute('role', 'dialog');
+        this.dialog.setAttribute('aria-labelledby', 'clipboard-consent-title');
+        this.dialog.setAttribute('aria-describedby', 'clipboard-consent-description');
+
+        this.dialog.innerHTML = `
+            <div class="p-6">
+                <!-- Header -->
+                <div class="flex items-start gap-3 mb-4">
+                    <svg class="w-6 h-6 text-[#155dfc] flex-shrink-0 mt-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
+                    </svg>
+                    <div class="flex-1">
+                        <h2 id="clipboard-consent-title" class="text-lg font-semibold text-white mb-1">
+                            Enable Clipboard Monitoring?
+                        </h2>
+                        <p class="text-sm text-[#90a1b9]">
+                            GrabZilla can automatically detect video URLs when you copy them
+                        </p>
+                    </div>
+                </div>
+
+                <!-- Privacy Disclosure -->
+                <div id="clipboard-consent-description" class="bg-[#314158] border border-[#45556c] rounded-lg p-4 mb-4">
+                    <div class="space-y-3">
+                        <div class="flex items-start gap-2">
+                            <svg class="w-4 h-4 text-[#00a63e] flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
+                                <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
+                            </svg>
+                            <p class="text-sm text-[#cad5e2]">
+                                <strong class="text-white">Only checks for video URLs</strong> from YouTube and Vimeo
+                            </p>
+                        </div>
+                        <div class="flex items-start gap-2">
+                            <svg class="w-4 h-4 text-[#00a63e] flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
+                                <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
+                            </svg>
+                            <p class="text-sm text-[#cad5e2]">
+                                <strong class="text-white">Never sent to external servers</strong> - stays on your device
+                            </p>
+                        </div>
+                        <div class="flex items-start gap-2">
+                            <svg class="w-4 h-4 text-[#00a63e] flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
+                                <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
+                            </svg>
+                            <p class="text-sm text-[#cad5e2]">
+                                <strong class="text-white">Can be disabled anytime</strong> using the toggle switch
+                            </p>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- Remember Choice -->
+                <label class="flex items-center gap-2 mb-6 cursor-pointer group">
+                    <input type="checkbox" id="clipboard-consent-remember"
+                           class="w-4 h-4 text-[#155dfc] bg-[#314158] border-[#45556c] rounded focus:ring-2 focus:ring-[#155dfc]">
+                    <span class="text-sm text-[#cad5e2] group-hover:text-white transition-colors">
+                        Remember my choice (don't ask again)
+                    </span>
+                </label>
+
+                <!-- Action Buttons -->
+                <div class="flex gap-3">
+                    <button id="clipboard-consent-deny"
+                            class="flex-1 px-4 py-2.5 bg-[#314158] hover:bg-[#3d4f66] border border-[#45556c] text-white rounded-lg font-medium transition-colors focus:ring-2 focus:ring-[#155dfc] focus:outline-none">
+                        Don't Allow
+                    </button>
+                    <button id="clipboard-consent-allow"
+                            class="flex-1 px-4 py-2.5 bg-[#155dfc] hover:bg-[#1250e3] text-white rounded-lg font-medium transition-colors focus:ring-2 focus:ring-[#155dfc] focus:outline-none">
+                        Allow Clipboard Monitoring
+                    </button>
+                </div>
+            </div>
+        `;
+    }
+
+    /**
+     * Remove the dialog from DOM
+     */
+    remove() {
+        if (this.dialog) {
+            this.dialog.remove();
+            this.dialog = null;
+        }
+        if (this.overlay) {
+            this.overlay.remove();
+            this.overlay = null;
+        }
+    }
+}
+
+// Export for use in main app
+if (typeof window !== 'undefined') {
+    window.ClipboardConsentDialog = ClipboardConsentDialog;
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+    module.exports = ClipboardConsentDialog;
+}

+ 24 - 1
scripts/models/AppState.js

@@ -11,7 +11,8 @@ class AppState {
             defaultFormat: window.AppConfig?.APP_CONFIG?.DEFAULT_FORMAT || 'None',
             filenamePattern: window.AppConfig?.APP_CONFIG?.DEFAULT_FILENAME_PATTERN || '%(title)s.%(ext)s',
             cookieFile: null,
-            maxHistoryEntries: 100 // Maximum number of history entries to keep
+            maxHistoryEntries: 100, // Maximum number of history entries to keep
+            clipboardConsent: null // null = not asked, true = allowed, false = denied
         };
         this.ui = {
             isDownloading: false,
@@ -318,6 +319,28 @@ class AppState {
         this.emit('configUpdated', { config: this.config, oldConfig });
     }
 
+    // Clipboard consent management
+    hasClipboardConsent() {
+        return this.config.clipboardConsent === true;
+    }
+
+    hasClipboardConsentAnswer() {
+        return this.config.clipboardConsent !== null;
+    }
+
+    setClipboardConsent(allowed) {
+        this.config.clipboardConsent = allowed;
+        this.emit('clipboardConsentChanged', { allowed });
+        // Persist to localStorage
+        this.saveState();
+    }
+
+    resetClipboardConsent() {
+        this.config.clipboardConsent = null;
+        this.emit('clipboardConsentChanged', { allowed: null });
+        this.saveState();
+    }
+
     // UI state management
     updateUI(newUIState) {
         const oldUIState = { ...this.ui };

+ 15 - 3
src/main.js

@@ -227,8 +227,16 @@ ipcMain.handle('check-file-exists', async (event, filePath) => {
 let clipboardMonitorInterval = null
 let lastClipboardText = ''
 
-ipcMain.handle('start-clipboard-monitor', async (event) => {
+ipcMain.handle('start-clipboard-monitor', async (event, userConsented = false) => {
   try {
+    // SECURITY: Require explicit user consent for clipboard monitoring
+    if (!userConsented) {
+      return {
+        success: false,
+        error: 'User consent required for clipboard monitoring'
+      }
+    }
+
     if (clipboardMonitorInterval) {
       return { success: false, message: 'Already monitoring' }
     }
@@ -241,20 +249,24 @@ ipcMain.handle('start-clipboard-monitor', async (event) => {
       if (currentText && currentText !== lastClipboardText) {
         lastClipboardText = currentText
 
-        // Check if it contains a video URL
+        // SECURITY: Only check for video URLs, don't process other clipboard content
+        // This prevents accidental exposure of passwords, API keys, etc.
         const youtubeMatch = currentText.match(/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/)
         const vimeoMatch = currentText.match(/(?:https?:\/\/)?(?:www\.)?(?:vimeo\.com\/|player\.vimeo\.com\/video\/)(\d+)/)
 
         if (youtubeMatch || vimeoMatch) {
           event.sender.send('clipboard-url-detected', currentText)
         }
+        // Don't log or process non-URL clipboard content
       }
     }, 1000) // Check every second
 
+    logger.info('Clipboard monitoring started with user consent')
     return { success: true }
   } catch (error) {
+    // SECURITY: Don't expose clipboard content in error logs
     logger.error('Error starting clipboard monitor:', error.message)
-    return { success: false, error: error.message }
+    return { success: false, error: 'Failed to start clipboard monitoring' }
   }
 })
 

+ 1 - 1
src/preload.js

@@ -11,7 +11,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
   checkFileExists: (filePath) => ipcRenderer.invoke('check-file-exists', filePath),
   
   // Clipboard monitoring
-  startClipboardMonitor: () => ipcRenderer.invoke('start-clipboard-monitor'),
+  startClipboardMonitor: (userConsented) => ipcRenderer.invoke('start-clipboard-monitor', userConsented),
   stopClipboardMonitor: () => ipcRenderer.invoke('stop-clipboard-monitor'),
   onClipboardUrlDetected: (callback) => {
     ipcRenderer.on('clipboard-url-detected', callback)