diff --git a/app.py b/app.py index 4f02958..4c0e494 100644 --- a/app.py +++ b/app.py @@ -7780,6 +7780,7 @@ def release_notes(): # ============================================================ @app.route('/zopk') +@limiter.limit("60 per minute") # SECURITY: Rate limit public ZOPK page def zopk_index(): """ Public knowledge base page for ZOPK. @@ -7830,6 +7831,7 @@ def zopk_index(): @app.route('/zopk/projekty/') +@limiter.limit("60 per minute") # SECURITY: Rate limit public ZOPK project pages def zopk_project_detail(slug): """Project detail page""" from database import ZOPKProject, ZOPKNews, ZOPKResource, ZOPKCompanyLink @@ -7869,6 +7871,7 @@ def zopk_project_detail(slug): @app.route('/zopk/aktualnosci') +@limiter.limit("60 per minute") # SECURITY: Rate limit public ZOPK news list def zopk_news_list(): """All ZOPK news - paginated""" from database import ZOPKProject, ZOPKNews @@ -8153,7 +8156,7 @@ def admin_zopk_news_add(): if not current_user.is_admin: return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403 - from database import ZOPKNews + from database import ZOPKNews, ZOPKProject import hashlib db = SessionLocal() @@ -8169,6 +8172,25 @@ def admin_zopk_news_add(): if not title or not url: return jsonify({'success': False, 'error': 'Tytuł i URL są wymagane'}), 400 + # SECURITY: Validate URL protocol (block javascript:, data:, etc.) + from urllib.parse import urlparse + parsed = urlparse(url) + allowed_protocols = ('http', 'https') + if parsed.scheme.lower() not in allowed_protocols: + return jsonify({'success': False, 'error': 'Nieprawidłowy protokół URL. Dozwolone: http, https'}), 400 + + # SECURITY: Validate project_id if provided + if project_id: + try: + project_id = int(project_id) + project = db.query(ZOPKProject).filter(ZOPKProject.id == project_id).first() + if not project: + return jsonify({'success': False, 'error': 'Nieprawidłowy ID projektu'}), 400 + except (ValueError, TypeError): + return jsonify({'success': False, 'error': 'ID projektu musi być liczbą'}), 400 + else: + project_id = None + # Generate URL hash for deduplication url_hash = hashlib.sha256(url.encode()).hexdigest() @@ -8178,8 +8200,6 @@ def admin_zopk_news_add(): return jsonify({'success': False, 'error': 'Ten artykuł już istnieje w bazie'}), 400 # Extract domain from URL - from urllib.parse import urlparse - parsed = urlparse(url) source_domain = parsed.netloc.replace('www.', '') news = ZOPKNews( @@ -8194,7 +8214,7 @@ def admin_zopk_news_add(): moderated_by=current_user.id, moderated_at=datetime.now(), published_at=datetime.now(), - project_id=project_id if project_id else None + project_id=project_id ) db.add(news) db.commit() @@ -8207,7 +8227,8 @@ def admin_zopk_news_add(): except Exception as e: db.rollback() - return jsonify({'success': False, 'error': str(e)}), 500 + logger.error(f"Error adding ZOPK news: {e}") + return jsonify({'success': False, 'error': 'Wystąpił błąd podczas dodawania newsa'}), 500 finally: db.close() diff --git a/templates/admin/zopk_news.html b/templates/admin/zopk_news.html index 876cbca..3d177cd 100644 --- a/templates/admin/zopk_news.html +++ b/templates/admin/zopk_news.html @@ -443,7 +443,7 @@ function showConfirm(message, options = {}) { const modal = document.getElementById('confirmModal'); document.getElementById('confirmModalIcon').textContent = options.icon || '❓'; document.getElementById('confirmModalTitle').textContent = options.title || 'Potwierdzenie'; - document.getElementById('confirmModalMessage').innerHTML = message; + document.getElementById('confirmModalMessage').textContent = message; document.getElementById('confirmModalCancel').textContent = options.cancelText || 'Anuluj'; document.getElementById('confirmModalOk').textContent = options.okText || 'OK'; document.getElementById('confirmModalOk').className = 'btn ' + (options.okClass || 'btn-primary'); @@ -479,7 +479,14 @@ function showToast(message, type = 'info', duration = 4000) { const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' }; const toast = document.createElement('div'); toast.className = `toast ${type}`; - toast.innerHTML = `${icons[type] || icons.info}${message}`; + // Bezpieczne tworzenie elementów - unikamy innerHTML z danymi użytkownika + const iconSpan = document.createElement('span'); + iconSpan.style.fontSize = '1.2em'; + iconSpan.textContent = icons[type] || icons.info; + const msgSpan = document.createElement('span'); + msgSpan.textContent = message; + toast.appendChild(iconSpan); + toast.appendChild(msgSpan); container.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration); }