security: Fix critical vulnerabilities in ZOP Kaszubia module
- Fix XSS: innerHTML → textContent for modal messages - Fix XSS: Safe DOM element creation for toast notifications - Add project_id validation in admin_zopk_news_add - Add URL protocol validation (allow only http/https) - Hide exception details from API responses (log instead) - Add rate limiting (60/min) on public ZOPK routes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9a527febf3
commit
91fea3ba2c
31
app.py
31
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/<slug>')
|
||||
@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()
|
||||
|
||||
@ -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 = `<span style="font-size: 1.2em;">${icons[type] || icons.info}</span><span>${message}</span>`;
|
||||
// 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);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user