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')
|
@app.route('/zopk')
|
||||||
|
@limiter.limit("60 per minute") # SECURITY: Rate limit public ZOPK page
|
||||||
def zopk_index():
|
def zopk_index():
|
||||||
"""
|
"""
|
||||||
Public knowledge base page for ZOPK.
|
Public knowledge base page for ZOPK.
|
||||||
@ -7830,6 +7831,7 @@ def zopk_index():
|
|||||||
|
|
||||||
|
|
||||||
@app.route('/zopk/projekty/<slug>')
|
@app.route('/zopk/projekty/<slug>')
|
||||||
|
@limiter.limit("60 per minute") # SECURITY: Rate limit public ZOPK project pages
|
||||||
def zopk_project_detail(slug):
|
def zopk_project_detail(slug):
|
||||||
"""Project detail page"""
|
"""Project detail page"""
|
||||||
from database import ZOPKProject, ZOPKNews, ZOPKResource, ZOPKCompanyLink
|
from database import ZOPKProject, ZOPKNews, ZOPKResource, ZOPKCompanyLink
|
||||||
@ -7869,6 +7871,7 @@ def zopk_project_detail(slug):
|
|||||||
|
|
||||||
|
|
||||||
@app.route('/zopk/aktualnosci')
|
@app.route('/zopk/aktualnosci')
|
||||||
|
@limiter.limit("60 per minute") # SECURITY: Rate limit public ZOPK news list
|
||||||
def zopk_news_list():
|
def zopk_news_list():
|
||||||
"""All ZOPK news - paginated"""
|
"""All ZOPK news - paginated"""
|
||||||
from database import ZOPKProject, ZOPKNews
|
from database import ZOPKProject, ZOPKNews
|
||||||
@ -8153,7 +8156,7 @@ def admin_zopk_news_add():
|
|||||||
if not current_user.is_admin:
|
if not current_user.is_admin:
|
||||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||||
|
|
||||||
from database import ZOPKNews
|
from database import ZOPKNews, ZOPKProject
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@ -8169,6 +8172,25 @@ def admin_zopk_news_add():
|
|||||||
if not title or not url:
|
if not title or not url:
|
||||||
return jsonify({'success': False, 'error': 'Tytuł i URL są wymagane'}), 400
|
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
|
# Generate URL hash for deduplication
|
||||||
url_hash = hashlib.sha256(url.encode()).hexdigest()
|
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
|
return jsonify({'success': False, 'error': 'Ten artykuł już istnieje w bazie'}), 400
|
||||||
|
|
||||||
# Extract domain from URL
|
# Extract domain from URL
|
||||||
from urllib.parse import urlparse
|
|
||||||
parsed = urlparse(url)
|
|
||||||
source_domain = parsed.netloc.replace('www.', '')
|
source_domain = parsed.netloc.replace('www.', '')
|
||||||
|
|
||||||
news = ZOPKNews(
|
news = ZOPKNews(
|
||||||
@ -8194,7 +8214,7 @@ def admin_zopk_news_add():
|
|||||||
moderated_by=current_user.id,
|
moderated_by=current_user.id,
|
||||||
moderated_at=datetime.now(),
|
moderated_at=datetime.now(),
|
||||||
published_at=datetime.now(),
|
published_at=datetime.now(),
|
||||||
project_id=project_id if project_id else None
|
project_id=project_id
|
||||||
)
|
)
|
||||||
db.add(news)
|
db.add(news)
|
||||||
db.commit()
|
db.commit()
|
||||||
@ -8207,7 +8227,8 @@ def admin_zopk_news_add():
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
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:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|||||||
@ -443,7 +443,7 @@ function showConfirm(message, options = {}) {
|
|||||||
const modal = document.getElementById('confirmModal');
|
const modal = document.getElementById('confirmModal');
|
||||||
document.getElementById('confirmModalIcon').textContent = options.icon || '❓';
|
document.getElementById('confirmModalIcon').textContent = options.icon || '❓';
|
||||||
document.getElementById('confirmModalTitle').textContent = options.title || 'Potwierdzenie';
|
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('confirmModalCancel').textContent = options.cancelText || 'Anuluj';
|
||||||
document.getElementById('confirmModalOk').textContent = options.okText || 'OK';
|
document.getElementById('confirmModalOk').textContent = options.okText || 'OK';
|
||||||
document.getElementById('confirmModalOk').className = 'btn ' + (options.okClass || 'btn-primary');
|
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 icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' };
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
toast.className = `toast ${type}`;
|
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);
|
container.appendChild(toast);
|
||||||
setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration);
|
setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user