Enhance search functionality with case-insensitive title search and security improvements

- Add case-insensitive search across all fields (inhalt, titel, geltungsbereich)
- Include Vorgabe.titel field in search scope for better coverage
- Implement comprehensive input validation against SQL injection and XSS
- Add German error messages for validation failures
- Escape search terms in templates to prevent XSS attacks
- Add input length limits and character validation
- Preserve user input on validation errors for better UX
This commit is contained in:
2025-11-04 12:54:44 +01:00
parent 28a1bb4b62
commit 671d259c44
2 changed files with 56 additions and 8 deletions

View File

@@ -2,6 +2,12 @@
{% block content %} {% block content %}
<h1 class="mb-4">Suche</h1> <h1 class="mb-4">Suche</h1>
{% if error_message %}
<div class="alert alert-danger">
<strong>Fehler:</strong> {{ error_message }}
</div>
{% endif %}
<!-- Search form --> <!-- Search form -->
<form action="." method="post"> <form action="." method="post">
{% csrf_token %} {% csrf_token %}
@@ -13,7 +19,9 @@
id="query" id="query"
name="q" name="q"
placeholder="Suchbegriff eingeben …" placeholder="Suchbegriff eingeben …"
required> value="{{ search_term|default:'' }}"
required
maxlength="200">
</div> </div>
<button type="submit" class="btn btn-primary">Suchen</button> <button type="submit" class="btn btn-primary">Suchen</button>
</form> </form>

View File

@@ -1,6 +1,9 @@
from django.shortcuts import render from django.shortcuts import render
from django.core.exceptions import ValidationError
from django.utils.html import escape
import re
from abschnitte.utils import render_textabschnitte from abschnitte.utils import render_textabschnitte
from dokumente.models import Dokument, VorgabeLangtext, VorgabeKurztext, Geltungsbereich from dokumente.models import Dokument, VorgabeLangtext, VorgabeKurztext, Geltungsbereich, Vorgabe
from itertools import groupby from itertools import groupby
import datetime import datetime
import pprint import pprint
@@ -9,23 +12,60 @@ def startseite(request):
standards=list(Dokument.objects.filter(aktiv=True)) standards=list(Dokument.objects.filter(aktiv=True))
return render(request, 'startseite.html', {"dokumente":standards,}) return render(request, 'startseite.html', {"dokumente":standards,})
def validate_search_input(search_term):
"""
Validate search input to prevent SQL injection and XSS
"""
if not search_term:
raise ValidationError("Suchbegriff darf nicht leer sein")
# Remove any HTML tags to prevent XSS
search_term = re.sub(r'<[^>]*>', '', search_term)
# Allow only alphanumeric characters, spaces, and basic punctuation
# This prevents SQL injection and other malicious input
if not re.match(r'^[a-zA-Z0-9äöüÄÖÜß\s\-\.\,\:\;\!\?\(\)\[\]\{\}\"\']+$', search_term):
raise ValidationError("Ungültige Zeichen im Suchbegriff")
# Limit length to prevent DoS attacks
if len(search_term) > 200:
raise ValidationError("Suchbegriff ist zu lang")
return search_term.strip()
def search(request): def search(request):
if request.method == "GET": if request.method == "GET":
return render(request, 'search.html') return render(request, 'search.html')
elif request.method == "POST": elif request.method == "POST":
suchbegriff=request.POST.get("q") raw_search_term = request.POST.get("q", "")
try:
suchbegriff = validate_search_input(raw_search_term)
except ValidationError as e:
return render(request, 'search.html', {
'error_message': str(e),
'search_term': escape(raw_search_term)
})
# Escape the search term for display in templates
safe_search_term = escape(suchbegriff)
result= {"all": {}} result= {"all": {}}
qs = VorgabeKurztext.objects.filter(inhalt__contains=suchbegriff).exclude(abschnitt__gueltigkeit_bis__lt=datetime.date.today()) qs = VorgabeKurztext.objects.filter(inhalt__icontains=suchbegriff).exclude(abschnitt__gueltigkeit_bis__lt=datetime.date.today())
result["kurztext"] = {k: [o.abschnitt for o in g] for k, g in groupby(qs, key=lambda o: o.abschnitt.dokument)} result["kurztext"] = {k: [o.abschnitt for o in g] for k, g in groupby(qs, key=lambda o: o.abschnitt.dokument)}
qs = VorgabeLangtext.objects.filter(inhalt__contains=suchbegriff).exclude(abschnitt__gueltigkeit_bis__lt=datetime.date.today()) qs = VorgabeLangtext.objects.filter(inhalt__icontains=suchbegriff).exclude(abschnitt__gueltigkeit_bis__lt=datetime.date.today())
result['langtext']= {k: [o.abschnitt for o in g] for k, g in groupby(qs, key=lambda o: o.abschnitt.dokument)} result['langtext']= {k: [o.abschnitt for o in g] for k, g in groupby(qs, key=lambda o: o.abschnitt.dokument)}
qs = Vorgabe.objects.filter(titel__icontains=suchbegriff).exclude(gueltigkeit_bis__lt=datetime.date.today())
result['titel']= {k: list(g) for k, g in groupby(qs, key=lambda o: o.dokument)}
for r in result.keys(): for r in result.keys():
for s in result[r].keys(): for s in result[r].keys():
result["all"][s] = set(result[r][s]) if r == 'titel':
result["all"][s] = set(result["all"].get(s, set()) | set(result[r][s]))
else:
result["all"][s] = set(result["all"].get(s, set()) | set(result[r][s]))
result["geltungsbereich"]={} result["geltungsbereich"]={}
geltungsbereich=set(list([x.geltungsbereich for x in Geltungsbereich.objects.filter(inhalt__contains=suchbegriff)])) geltungsbereich=set(list([x.geltungsbereich for x in Geltungsbereich.objects.filter(inhalt__icontains=suchbegriff)]))
for s in geltungsbereich: for s in geltungsbereich:
result["geltungsbereich"][s]=render_textabschnitte(s.geltungsbereich_set.order_by("order")) result["geltungsbereich"][s]=render_textabschnitte(s.geltungsbereich_set.order_by("order"))
pprint.pp (result) pprint.pp (result)
return render(request,"results.html",{"suchbegriff":suchbegriff,"resultat":result}) return render(request,"results.html",{"suchbegriff":safe_search_term,"resultat":result})