XSS protection added to comments
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 1m1s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 8s
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 1m1s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 8s
This commit is contained in:
@@ -25,7 +25,7 @@ spec:
|
|||||||
mountPath: /data
|
mountPath: /data
|
||||||
containers:
|
containers:
|
||||||
- name: web
|
- name: web
|
||||||
image: git.baumann.gr/adebaumann/vui:0.958-comments
|
image: git.baumann.gr/adebaumann/vui:0.958-comments-XSS
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8000
|
- containerPort: 8000
|
||||||
|
|||||||
@@ -219,6 +219,36 @@
|
|||||||
|
|
||||||
<!-- JavaScript for Comments -->
|
<!-- JavaScript for Comments -->
|
||||||
<script>
|
<script>
|
||||||
|
// Content Security Policy for comment system
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Prevent inline script execution in dynamically loaded content
|
||||||
|
const commentsContainer = document.getElementById('commentsContainer');
|
||||||
|
if (commentsContainer) {
|
||||||
|
// Use DOMPurify-like approach - only allow safe HTML
|
||||||
|
const allowedTags = ['br', 'small', 'div', 'span', 'button'];
|
||||||
|
const allowedAttributes = ['class', 'data-comment-id', 'aria-hidden'];
|
||||||
|
|
||||||
|
// Monitor for any script injection attempts
|
||||||
|
const observer = new MutationObserver(function(mutations) {
|
||||||
|
mutations.forEach(function(mutation) {
|
||||||
|
mutation.addedNodes.forEach(function(node) {
|
||||||
|
if (node.nodeType === 1) { // Element node
|
||||||
|
const tagName = node.tagName.toLowerCase();
|
||||||
|
if (tagName === 'script') {
|
||||||
|
console.warn('Script injection attempt blocked');
|
||||||
|
node.parentNode.removeChild(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(commentsContainer, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
let currentVorgabeId = null;
|
let currentVorgabeId = null;
|
||||||
let currentVorgabeNummer = null;
|
let currentVorgabeNummer = null;
|
||||||
@@ -270,7 +300,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
<strong>${comment.user}</strong>
|
<strong>${comment.user}</strong>
|
||||||
<small class="text-muted">(${comment.created_at})</small>
|
<small class="text-muted">(${comment.created_at})</small>
|
||||||
${comment.updated_at !== comment.created_at ? `<small class="text-muted">(bearbeitet: ${comment.updated_at})</small>` : ''}
|
${comment.updated_at !== comment.created_at ? `<small class="text-muted">(bearbeitet: ${comment.updated_at})</small>` : ''}
|
||||||
<div class="mt-1">${comment.text.replace(/\n/g, '<br>')}</div>
|
<div class="mt-1">${comment.text}</div>
|
||||||
</div>
|
</div>
|
||||||
${canDelete ? `
|
${canDelete ? `
|
||||||
<button class="btn btn-sm btn-outline-danger ml-2 delete-comment-btn" data-comment-id="${comment.id}">
|
<button class="btn btn-sm btn-outline-danger ml-2 delete-comment-btn" data-comment-id="${comment.id}">
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from django.http import JsonResponse
|
|||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.utils.html import escape, mark_safe
|
||||||
|
from django.utils.safestring import SafeString
|
||||||
import json
|
import json
|
||||||
from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage, VorgabeComment
|
from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage, VorgabeComment
|
||||||
from abschnitte.utils import render_textabschnitte
|
from abschnitte.utils import render_textabschnitte
|
||||||
@@ -264,16 +266,21 @@ def get_vorgabe_comments(request, vorgabe_id):
|
|||||||
|
|
||||||
comments_data = []
|
comments_data = []
|
||||||
for comment in comments:
|
for comment in comments:
|
||||||
|
# Escape HTML but preserve line breaks
|
||||||
|
escaped_text = escape(comment.text).replace('\n', '<br>')
|
||||||
comments_data.append({
|
comments_data.append({
|
||||||
'id': comment.id,
|
'id': comment.id,
|
||||||
'text': comment.text,
|
'text': escaped_text,
|
||||||
'user': comment.user.username,
|
'user': escape(comment.user.username),
|
||||||
'created_at': comment.created_at.strftime('%d.%m.%Y %H:%M'),
|
'created_at': comment.created_at.strftime('%d.%m.%Y %H:%M'),
|
||||||
'updated_at': comment.updated_at.strftime('%d.%m.%Y %H:%M'),
|
'updated_at': comment.updated_at.strftime('%d.%m.%Y %H:%M'),
|
||||||
'is_own': comment.user == request.user
|
'is_own': comment.user == request.user
|
||||||
})
|
})
|
||||||
|
|
||||||
return JsonResponse({'comments': comments_data})
|
response = JsonResponse({'comments': comments_data})
|
||||||
|
response['Content-Security-Policy'] = "default-src 'self'"
|
||||||
|
response['X-Content-Type-Options'] = 'nosniff'
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
@@ -286,31 +293,53 @@ def add_vorgabe_comment(request, vorgabe_id):
|
|||||||
data = json.loads(request.body)
|
data = json.loads(request.body)
|
||||||
text = data.get('text', '').strip()
|
text = data.get('text', '').strip()
|
||||||
|
|
||||||
|
# Validate input
|
||||||
if not text:
|
if not text:
|
||||||
return JsonResponse({'error': 'Kommentar darf nicht leer sein'}, status=400)
|
return JsonResponse({'error': 'Kommentar darf nicht leer sein'}, status=400)
|
||||||
|
|
||||||
|
if len(text) > 2000: # Reasonable length limit
|
||||||
|
return JsonResponse({'error': 'Kommentar ist zu lang (max 2000 Zeichen)'}, status=400)
|
||||||
|
|
||||||
|
# Additional XSS prevention - check for dangerous patterns
|
||||||
|
dangerous_patterns = ['<script', 'javascript:', 'onload=', 'onerror=', 'onclick=', 'onmouseover=']
|
||||||
|
text_lower = text.lower()
|
||||||
|
for pattern in dangerous_patterns:
|
||||||
|
if pattern in text_lower:
|
||||||
|
return JsonResponse({'error': 'Kommentar enthält ungültige Zeichen'}, status=400)
|
||||||
|
|
||||||
comment = VorgabeComment.objects.create(
|
comment = VorgabeComment.objects.create(
|
||||||
vorgabe=vorgabe,
|
vorgabe=vorgabe,
|
||||||
user=request.user,
|
user=request.user,
|
||||||
text=text
|
text=text
|
||||||
)
|
)
|
||||||
|
|
||||||
return JsonResponse({
|
# Escape HTML but preserve line breaks
|
||||||
|
escaped_text = escape(comment.text).replace('\n', '<br>')
|
||||||
|
response = JsonResponse({
|
||||||
'success': True,
|
'success': True,
|
||||||
'comment': {
|
'comment': {
|
||||||
'id': comment.id,
|
'id': comment.id,
|
||||||
'text': comment.text,
|
'text': escaped_text,
|
||||||
'user': comment.user.username,
|
'user': escape(comment.user.username),
|
||||||
'created_at': comment.created_at.strftime('%d.%m.%Y %H:%M'),
|
'created_at': comment.created_at.strftime('%d.%m.%Y %H:%M'),
|
||||||
'updated_at': comment.updated_at.strftime('%d.%m.%Y %H:%M'),
|
'updated_at': comment.updated_at.strftime('%d.%m.%Y %H:%M'),
|
||||||
'is_own': True
|
'is_own': True
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
response['Content-Security-Policy'] = "default-src 'self'"
|
||||||
|
response['X-Content-Type-Options'] = 'nosniff'
|
||||||
|
return response
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return JsonResponse({'error': 'Ungültige Daten'}, status=400)
|
response = JsonResponse({'error': 'Ungültige Daten'}, status=400)
|
||||||
|
response['Content-Security-Policy'] = "default-src 'self'"
|
||||||
|
response['X-Content-Type-Options'] = 'nosniff'
|
||||||
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({'error': str(e)}, status=500)
|
response = JsonResponse({'error': 'Serverfehler'}, status=500)
|
||||||
|
response['Content-Security-Policy'] = "default-src 'self'"
|
||||||
|
response['X-Content-Type-Options'] = 'nosniff'
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
@@ -321,10 +350,19 @@ def delete_vorgabe_comment(request, comment_id):
|
|||||||
|
|
||||||
# Check if user can delete this comment
|
# Check if user can delete this comment
|
||||||
if comment.user != request.user and not request.user.is_staff:
|
if comment.user != request.user and not request.user.is_staff:
|
||||||
return JsonResponse({'error': 'Keine Berechtigung zum Löschen dieses Kommentars'}, status=403)
|
response = JsonResponse({'error': 'Keine Berechtigung zum Löschen dieses Kommentars'}, status=403)
|
||||||
|
response['Content-Security-Policy'] = "default-src 'self'"
|
||||||
|
response['X-Content-Type-Options'] = 'nosniff'
|
||||||
|
return response
|
||||||
|
|
||||||
try:
|
try:
|
||||||
comment.delete()
|
comment.delete()
|
||||||
return JsonResponse({'success': True})
|
response = JsonResponse({'success': True})
|
||||||
|
response['Content-Security-Policy'] = "default-src 'self'"
|
||||||
|
response['X-Content-Type-Options'] = 'nosniff'
|
||||||
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({'error': str(e)}, status=500)
|
response = JsonResponse({'error': 'Serverfehler'}, status=500)
|
||||||
|
response['Content-Security-Policy'] = "default-src 'self'"
|
||||||
|
response['X-Content-Type-Options'] = 'nosniff'
|
||||||
|
return response
|
||||||
|
|||||||
@@ -215,7 +215,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6 text-right">
|
<div class="col-sm-6 text-right">
|
||||||
<p class="text-muted">Version {{ version|default:"0.957-xss" }}</p>
|
<p class="text-muted">Version {{ version|default:"0.958-comments-XSS" }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user