From df9085f479b8de522c3f827895aa5023837552cb Mon Sep 17 00:00:00 2001 From: Bernd Date: Wed, 23 Jul 2025 20:42:35 +0500 Subject: [PATCH] improving safety, Sanitizing input, Validating and sanitizing data, Ensuring proper HTML escaping --- anime_backlog.db-shm | Bin 0 -> 32768 bytes anime_backlog.db-wal | Bin 0 -> 37112 bytes backend.py | 63 +++++++++++++++++++++++++++++++++++++++---- frontend.py | 30 ++++++++++++++------- 4 files changed, 79 insertions(+), 14 deletions(-) create mode 100644 anime_backlog.db-shm create mode 100644 anime_backlog.db-wal diff --git a/anime_backlog.db-shm b/anime_backlog.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..74bfcc8016857ca1c3faf8856cff6f1f309f261a GIT binary patch literal 32768 zcmeI*JrV&i6ae5||2l)h6%;yZm(Zx3N3B#jfFn486KE7hsZwZ+A=$wwG+S)Jd~YW6 zvf1Qq@*QB)I}DN-mC({8X*EOc#^=Rmd0fr+r|E8Xn+(%+|8hN#N84V``Jq%6vF`mN zp2P1{4!!u@S1yHqJG9PoJiZeI2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oU%ef$F~;#5oA$ z7D%&y4dq^D0RaL82oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ rfB*pk1PTzS6;M-20>2?p|BY?hC4mA2O5sNDMiOt`76b?o_%84OI-Dy8 literal 0 HcmV?d00001 diff --git a/anime_backlog.db-wal b/anime_backlog.db-wal new file mode 100644 index 0000000000000000000000000000000000000000..32f06fb83330f2879aa8d69951838bee83b215e5 GIT binary patch literal 37112 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 literal 0 HcmV?d00001 diff --git a/backend.py b/backend.py index a981f05..e8575bc 100644 --- a/backend.py +++ b/backend.py @@ -1,6 +1,7 @@ import sqlite3 import csv import logging +import html # Set up logging logging.basicConfig(filename='anime_tracker.log', level=logging.ERROR, @@ -8,7 +9,8 @@ logging.basicConfig(filename='anime_tracker.log', level=logging.ERROR, class AnimeBackend: def __init__(self): - self.db = sqlite3.connect('anime_backlog.db') + self.db = sqlite3.connect('anime_backlog.db', isolation_level=None) # Autocommit mode to prevent locks + self.db.execute('PRAGMA journal_mode=WAL') # Use WAL mode for better concurrency self.create_table() def create_table(self): @@ -68,25 +70,64 @@ class AnimeBackend: def add_anime(self, data): try: + # Sanitize string inputs + sanitized_data = { + 'name': html.escape(data['name'].strip()) if data['name'] else '', + 'year': data['year'], + 'season': data['season'].strip() if data['season'] else '', + 'status': data['status'].strip() if data['status'] else 'unwatched', + 'type': data['type'].strip() if data['type'] else '', + 'comment': html.escape(data['comment'].strip()) if data['comment'] else '', + 'url': data['url'].strip() if data['url'] else '' + } cursor = self.db.cursor() cursor.execute( "INSERT INTO anime (name, year, season, status, type, comment, url) VALUES (?, ?, ?, ?, ?, ?, ?)", - (data['name'], data['year'], data['season'], data['status'], data['type'], data['comment'], data['url']) + ( + sanitized_data['name'], + sanitized_data['year'], + sanitized_data['season'], + sanitized_data['status'], + sanitized_data['type'], + sanitized_data['comment'], + sanitized_data['url'] + ) ) self.db.commit() except Exception as e: logging.error(f"Error adding anime: {e}") + self.db.rollback() def edit_anime(self, anime_id, data): try: + # Sanitize string inputs + sanitized_data = { + 'name': html.escape(data['name'].strip()) if data['name'] else '', + 'year': data['year'], + 'season': data['season'].strip() if data['season'] else '', + 'status': data['status'].strip() if data['status'] else 'unwatched', + 'type': data['type'].strip() if data['type'] else '', + 'comment': html.escape(data['comment'].strip()) if data['comment'] else '', + 'url': data['url'].strip() if data['url'] else '' + } cursor = self.db.cursor() cursor.execute( "UPDATE anime SET name=?, year=?, season=?, status=?, type=?, comment=?, url=? WHERE id=?", - (data['name'], data['year'], data['season'], data['status'], data['type'], data['comment'], data['url'], anime_id) + ( + sanitized_data['name'], + sanitized_data['year'], + sanitized_data['season'], + sanitized_data['status'], + sanitized_data['type'], + sanitized_data['comment'], + sanitized_data['url'], + anime_id + ) ) self.db.commit() except Exception as e: logging.error(f"Error editing anime id {anime_id}: {e}") + self.db.rollback() def delete_anime(self, anime_id): try: @@ -95,6 +136,7 @@ class AnimeBackend: self.db.commit() except Exception as e: logging.error(f"Error deleting anime id {anime_id}: {e}") + self.db.rollback() def change_status(self, anime_id, new_status): try: @@ -103,6 +145,7 @@ class AnimeBackend: self.db.commit() except Exception as e: logging.error(f"Error changing status for anime id {anime_id}: {e}") + self.db.rollback() def add_placeholders_for_year(self, year): try: @@ -115,6 +158,7 @@ class AnimeBackend: self.db.commit() except Exception as e: logging.error(f"Error adding placeholders for year {year}: {e}") + self.db.rollback() def import_from_csv(self, file_name): try: @@ -134,6 +178,13 @@ class AnimeBackend: year = int(year_str) except ValueError: continue + # Sanitize CSV inputs + name = html.escape(name.strip()) if name else '' + season = season.strip() if season else '' + status = status.strip() if status else 'unwatched' + type_ = type_.strip() if type_ else '' + comment = html.escape(comment.strip()) if comment else '' + url = url.strip() if url else '' cursor.execute( "SELECT id FROM anime WHERE name = ? AND year = ? AND season = ?", (name, year, season) @@ -146,6 +197,7 @@ class AnimeBackend: self.db.commit() except Exception as e: logging.error(f"Error importing from CSV {file_name}: {e}") + self.db.rollback() def export_to_csv(self, file_name): try: @@ -153,7 +205,7 @@ class AnimeBackend: cursor.execute("SELECT * FROM anime") rows = cursor.fetchall() with open(file_name, 'w', newline='') as f: - writer = csv.writer(f) + writer = csv.writer(f, quoting=csv.QUOTE_MINIMAL) writer.writerow(['id', 'name', 'year', 'season', 'status', 'type', 'comment', 'url']) writer.writerows(rows) except Exception as e: @@ -165,4 +217,5 @@ class AnimeBackend: cursor.execute("DELETE FROM anime WHERE year = ?", (year,)) self.db.commit() except Exception as e: - logging.error(f"Error deleting year {year}: {e}") \ No newline at end of file + logging.error(f"Error deleting year {year}: {e}") + self.db.rollback() \ No newline at end of file diff --git a/frontend.py b/frontend.py index aa92f28..18c1d3d 100644 --- a/frontend.py +++ b/frontend.py @@ -9,7 +9,7 @@ from PyQt5.QtWidgets import ( QMainWindow, QTabWidget, QScrollArea, QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, QLabel, QToolButton, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QSpinBox, QComboBox, QTextEdit, QDialogButtonBox, QAction, QFileDialog, QMessageBox, - QInputDialog, QApplication, QAbstractItemView, QSizePolicy, QHeaderView, QPushButton + QInputDialog, QApplication, QAbstractItemView, QSizePolicy, QHeaderView ) from PyQt5.QtCore import Qt, QSettings from PyQt5.QtGui import QColor, QIcon, QFont @@ -25,6 +25,7 @@ class AnimeDialog(QDialog): self.setWindowTitle("Add Anime" if entry is None else "Edit Anime") layout = QFormLayout(self) self.name_edit = QLineEdit() + self.name_edit.setMaxLength(255) # Prevent overly long inputs layout.addRow("Name", self.name_edit) self.year_spin = QSpinBox() self.year_spin.setRange(1900, 2100) @@ -39,20 +40,23 @@ class AnimeDialog(QDialog): self.status_combo.addItems(['unwatched', 'watching', 'completed']) layout.addRow("Status", self.status_combo) self.comment_edit = QTextEdit() + self.comment_edit.setAcceptRichText(False) # Prevent HTML injection layout.addRow("Comment", self.comment_edit) self.url_edit = QLineEdit() + self.url_edit.setMaxLength(2048) # Reasonable limit for URLs layout.addRow("MAL URL", self.url_edit) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addRow(buttons) if entry: - self.name_edit.setText(entry[1]) + # Unescape for display in input fields + self.name_edit.setText(html.unescape(entry[1])) self.year_spin.setValue(entry[2]) self.season_combo.setCurrentText(entry[3]) self.status_combo.setCurrentText(entry[4]) self.type_combo.setCurrentText(entry[5] or '') - self.comment_edit.setText(entry[6] or '') + self.comment_edit.setPlainText(html.unescape(entry[6] or '')) self.url_edit.setText(entry[7] or '') else: if default_year is not None: @@ -70,14 +74,15 @@ class AnimeDialog(QDialog): self.season_combo.setEnabled(True) def get_data(self): + # Sanitize inputs by escaping special characters return { - 'name': self.name_edit.text(), + 'name': html.escape(self.name_edit.text().strip()), 'year': self.year_spin.value(), - 'season': self.season_combo.currentText(), - 'status': self.status_combo.currentText(), - 'type': self.type_combo.currentText(), - 'comment': self.comment_edit.toPlainText(), - 'url': self.url_edit.text() + 'season': html.escape(self.season_combo.currentText().strip()), + 'status': html.escape(self.status_combo.currentText().strip()), + 'type': html.escape(self.type_combo.currentText().strip()), + 'comment': html.escape(self.comment_edit.toPlainText().strip()), + 'url': html.escape(self.url_edit.text().strip()) } class ShortcutsDialog(QDialog): @@ -514,6 +519,7 @@ class AnimeTracker(QMainWindow): index = self.tab_widget.addTab(year_tab, tab_text) if completed: self.tab_widget.tabBar().setTabTextColor(index, QColor('gray')) + self.filter_tables(self.search_text) # Apply search filter after loading tabs def create_actions_widget(self, anime_id, status): widget = QWidget() @@ -572,6 +578,9 @@ class AnimeTracker(QMainWindow): if dialog.exec_() == QDialog.Accepted: current_id = self.get_current_tab_identifier() data = dialog.get_data() + if not data['name']: + QMessageBox.warning(self, "Error", "Anime name cannot be empty.") + return self.backend.add_anime(data) self.load_tabs() self.set_current_tab_by_identifier(current_id) @@ -583,6 +592,9 @@ class AnimeTracker(QMainWindow): dialog = AnimeDialog(self, entry) if dialog.exec_() == QDialog.Accepted: data = dialog.get_data() + if not data['name']: + QMessageBox.warning(self, "Error", "Anime name cannot be empty.") + return self.backend.edit_anime(anime_id, data) self.load_tabs() self.set_current_tab_by_identifier(current_id)