04/06 remediate some issues

This commit is contained in:
2026-04-06 16:31:50 -04:00
parent d7293f2747
commit 3b17705911
8 changed files with 211 additions and 28 deletions
+16 -4
View File
@@ -9,7 +9,7 @@ from flask import Blueprint, render_template, redirect, url_for, flash, request,
from flask_login import login_required, current_user
from werkzeug.utils import secure_filename
import bleach
from app import db
from app import db, limiter
from app.models import (User, Ticket, Comment, ActivityLog, KnowledgeBase,
KBAttachment, UserRole, TicketStatus)
from app.services.log_service import log_action
@@ -404,8 +404,10 @@ def all_tickets():
if search:
from app.models import Comment
from sqlalchemy import func
submitter_alias = db.aliased(User)
assignee_alias = db.aliased(User)
stripped_body = func.regexp_replace(Comment.body, r'<[^>]+>', '', 'g')
q = (
q
.outerjoin(submitter_alias, submitter_alias.id == Ticket.created_by_id)
@@ -417,7 +419,7 @@ def all_tickets():
Ticket.description.ilike(f'%{search}%') |
submitter_alias.full_name.ilike(f'%{search}%') |
assignee_alias.full_name.ilike(f'%{search}%') |
Comment.body.ilike(f'%{search}%')
stripped_body.ilike(f'%{search}%')
)
.distinct()
)
@@ -540,6 +542,7 @@ def kb_upload_image():
# ── File-serve route (images embedded in articles + attachment downloads) ─────
@admin_bp.route('/kb/files/<string:stored_name>')
@login_required
def kb_serve_file(stored_name):
"""Serve a KB attachment file. Login required — no public access.
@@ -551,6 +554,12 @@ def kb_serve_file(stored_name):
traverse outside the upload directory. <string:> disallows slashes,
restricting the value to a flat filename — matching the UUID-based
stored_name format (e.g. 'a1b2c3d4e5f6....png') used by all upload helpers.
@login_required is applied here because the upload directory is shared
across KB files, ticket attachments, comment images, and user avatars.
Without authentication, an unauthenticated caller who knows or guesses
any stored_name (UUID-based) could retrieve arbitrary files from the
shared uploads folder — including confidential ticket attachments.
"""
upload_dir = current_app.config['UPLOAD_FOLDER']
return send_from_directory(upload_dir, stored_name)
@@ -572,7 +581,6 @@ def kb_delete_attachment(article_id, att_id):
logger.info(f'[KB ATTACHMENT DELETE] att_id={att.id} article_id={article_id} by user_id={current_user.id}')
db.session.delete(att)
db.session.commit()
logger.info(f'[KB ATTACHMENT DELETE] att_id={att.id} article_id={article_id} completed')
# Return JSON so the edit page can remove the row without a full reload
return jsonify({'ok': True, 'att_id': att.id})
@@ -766,6 +774,7 @@ def activity_logs():
@admin_bp.route('/tickets/export')
@login_required
@admin_required
@limiter.limit('10 per hour')
def export_tickets():
"""Stream a CSV of tickets matching the current filter params."""
status = request.args.get('status', '')
@@ -780,8 +789,10 @@ def export_tickets():
elif assigned == 'unassigned': q = q.filter_by(assigned_to_id=None)
if search:
from sqlalchemy import func
submitter_alias = db.aliased(User)
assignee_alias = db.aliased(User)
stripped_body = func.regexp_replace(Comment.body, r'<[^>]+>', '', 'g')
q = (
q
.outerjoin(submitter_alias, submitter_alias.id == Ticket.created_by_id)
@@ -793,7 +804,7 @@ def export_tickets():
Ticket.description.ilike(f'%{search}%') |
submitter_alias.full_name.ilike(f'%{search}%') |
assignee_alias.full_name.ilike(f'%{search}%') |
Comment.body.ilike(f'%{search}%')
stripped_body.ilike(f'%{search}%')
)
.distinct()
)
@@ -845,6 +856,7 @@ def _roles():
@admin_bp.route('/settings', methods=['GET', 'POST'])
@login_required
@admin_required
def settings():
from app.models import SystemSetting