diff --git a/CHANGELOG.md b/CHANGELOG.md index 383c4ab..9087539 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,48 @@ All notable changes to Library Manager will be documented in this file. +## [0.9.0-beta.153] - 2026-06-12 + +### Security +- **#255: Validate and sanitize BookDB response data** — Added `_sanitize_api_response()` to strip null bytes, control characters, HTML tags, and truncate oversized fields from all Skaldleita/BookDB API responses. Prevents path traversal, XSS, and memory issues from malformed responses. +- **#256: Signing nonce and tighter replay window** — Added UUID nonce (`X-LM-Nonce`) to signed request headers for replay attack prevention. Reduced timestamp tolerance from 300s to 120s. Backwards compatible with existing BookDB. +- **#236: SSRF prevention on plugin endpoints** — (from 0.9.0-beta.152) + +### Fixed +- **#252: Rate limiter aligned with actual BookDB tiers** — Increased min_delay from 3.6s (1000/hr) to 12.0s (300/hr) to match BookDB's API key tier. Added adaptive backoff from Retry-After headers and specific 429 handling in audio identification. +- **#253: server_notice handled in audio identification** — BookDB's abort/notice signals are now processed in `identify_audio_with_bookdb()` (submit, poll, and result stages), matching existing coverage in `search_bookdb()`. + +### Enhanced +- **#254: Differentiated BookDB source trust weighting** — `sl_source` field now maps to distinct source names (`bookdb_audio`, `bookdb_transcript`, `bookdb_scrape`) with graduated weights (65/55/45) instead of treating all BookDB audio results identically. + +### Documentation +- **#257: ABS plugin architecture** — Added `docs/ABS-Plugin-Architecture.md` scoping Library Manager as an Audiobookshelf metadata provider plugin. + +--- + +## [0.9.0-beta.152] - 2026-06-12 + +### Security +- **#219: Library boundary enforcement** on undo/replace/remove file operations +- **#237: XSS fix** — escapeHtml covers all 5 HTML entities including single quotes +- **#236: SSRF prevention** on custom plugin endpoints + +### Fixed +- **#220: Atomic status guard** on apply_fix prevents race condition double-renames +- **#221: Worker stop checks** in all 4 legacy batch loops +- **#222-224: Rate limits and circuit breakers** for BookDB contribute, community consensus, Gemini language detect, OpenRouter identify, plus process endpoint re-entrancy guard +- **#225-226: Undo parent mkdir** and path sanitize false positive fix +- **#227: L1 preserves original metadata** on validation failure +- **#228-229: Pipeline stuck prevention** — L3 trust_sl no longer deletes queue entry before L4, AI verify creates history on null drastic check +- **#230: Custom source weights** now actually used in confidence calculations +- **#231: Fixed broken route** in queue multi-edit modal +- **#232: Settings save** now persists sl_trust_mode and enable_isbn_lookup, removed duplicate field +- **#233: Double-click prevention** on all 11 history page action buttons +- **#234: Famous numeric titles** (1984, 2001, etc.) no longer flagged as garbage +- **#235: Confidence scale normalization** handles both 0-1 and 0-100 scales + +--- + ## [0.9.0-beta.151] - 2026-06-05 ### Changed diff --git a/README.md b/README.md index 71417fa..18f3d8e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **Smart Audiobook Library Organizer with Multi-Source Metadata & AI Verification** -[![Version](https://img.shields.io/badge/version-0.9.0--beta.152-blue.svg)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-0.9.0--beta.153-blue.svg)](CHANGELOG.md) [![Docker](https://img.shields.io/badge/docker-ghcr.io-blue.svg)](https://ghcr.io/deucebucket/library-manager) [![License](https://img.shields.io/badge/license-AGPL--3.0-blue.svg)](LICENSE) diff --git a/app.py b/app.py index dfee0f1..6b53fb0 100644 --- a/app.py +++ b/app.py @@ -11,7 +11,7 @@ - Multi-provider AI (Gemini, OpenRouter, Ollama) """ -APP_VERSION = "0.9.0-beta.152" +APP_VERSION = "0.9.0-beta.153" GITHUB_REPO = "deucebucket/library-manager" # Your GitHub repo # Versioning Guide: diff --git a/library_manager/providers/bookdb.py b/library_manager/providers/bookdb.py index 372c928..a3fd983 100644 --- a/library_manager/providers/bookdb.py +++ b/library_manager/providers/bookdb.py @@ -11,6 +11,7 @@ """ import os +import re import time import logging import subprocess @@ -50,6 +51,33 @@ def get_and_clear_server_abort(): _abort_state.notice = None return notice +def _sanitize_api_response(data, context='bookdb'): + """Sanitize string fields from API responses to prevent path traversal, XSS, and oversized data.""" + if not isinstance(data, dict): + return data + + MAX_FIELD_LENGTH = 500 + sanitized = {} + string_fields = ('title', 'author', 'author_name', 'narrator', 'series', 'series_name', 'name', 'variant', 'edition') + + for key, value in data.items(): + if key in string_fields and isinstance(value, str): + original = value + # Strip null bytes and control characters (keep newlines for descriptions) + value = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', value) + # Strip HTML tags + value = re.sub(r'<[^>]+>', '', value) + # Truncate oversized fields + if len(value) > MAX_FIELD_LENGTH: + value = value[:MAX_FIELD_LENGTH] + # Log if sanitization changed the value + if value != original: + logger.warning(f"[{context}] Sanitized '{key}': {original[:100]!r} → {value[:100]!r}") + sanitized[key] = value + + return sanitized + + # Skaldleita API endpoint (our metadata service, legacy name: BookDB) BOOKDB_API_URL = "https://bookdb.deucebucket.com" # URL unchanged for backwards compatibility # Public API key for Library Manager users (no config needed) @@ -184,6 +212,7 @@ def search_bookdb(title, author=None, api_key=None, retry_count=0, bookdb_url=No API_CIRCUIT_BREAKER['bookdb']['failures'] = 0 data = resp.json() + data = _sanitize_api_response(data) # Issue #208: honor Skaldleita server_notice. Log every notice; on # action=abort_task, stash in thread-local so the watch-folder worker @@ -226,6 +255,9 @@ def search_bookdb(title, author=None, api_key=None, retry_count=0, bookdb_url=No if not best_book: best_book = books[0] + if best_book: + best_book = _sanitize_api_response(best_book) + # Build result - handle standalone books (no series) and series books result = { 'title': best_book.get('title') if best_book else (series.get('name') if series else None), @@ -460,6 +492,7 @@ def identify_audio_with_bookdb(audio_file, extract_seconds=90, bookdb_url=None): 'confidence': 'high' if best_match or sl_source == 'database' else 'medium', 'transcript': transcript[:500], } + result = _sanitize_api_response(result, context='SKALDLEITA') if result['author'] and result['title']: logger.info(f"[SKALDLEITA] Identified: {result['author']} - {result['title']}" + @@ -626,6 +659,7 @@ def lookup_community_consensus(title, author=None, bookdb_url=None): __all__ = [ 'BOOKDB_API_URL', 'BOOKDB_PUBLIC_KEY', + '_sanitize_api_response', 'get_signed_headers', 'search_bookdb', 'identify_audio_with_bookdb', diff --git a/library_manager/providers/fingerprint.py b/library_manager/providers/fingerprint.py index 518e875..e04d4ec 100644 --- a/library_manager/providers/fingerprint.py +++ b/library_manager/providers/fingerprint.py @@ -18,6 +18,7 @@ import requests +from library_manager.providers.bookdb import _sanitize_api_response from library_manager.providers.rate_limiter import handle_rate_limit_response logger = logging.getLogger(__name__) @@ -157,6 +158,7 @@ def lookup_fingerprint( # Return book if linked to database, otherwise contributed metadata result = data.get('book') or data.get('contributed_metadata') if result: + result = _sanitize_api_response(result, context='FINGERPRINT') result['confidence'] = data.get('confidence', 0) title = result.get('title', 'Unknown') logger.info(f"[FINGERPRINT] Match found: {title} "