Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected], [email protected]
Control: affects -1 + src:calibre
User: [email protected]
Usertags: pu

[ Reason ]
Fix Debian bug 1135543:
 calibre: upstream 9.8 contains unannounced security fixes
 https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1135543

[ Impact ]
Some security issues unfixed.

[ Tests ]
Build time test was passed.

[ Risks ]
Not well tested on trixie machine.

[ Checklist ]
  [x] *all* changes are documented in the d/changelog
  [x] I reviewed all changes and I approve them
  [x] attach debdiff against the package in (old)stable
  [x] the issue is verified as fixed in unstable

[ Changes ]
High severity:
  - Fix typo normapth -> normpath in srv/content.py (broken endpoint)
  - Replace eval() with ast.literal_eval() in catalogs/epub_mobi.py
  - Log exceptions in FunctionDispatcher.dispatch instead of swallowing

Medium severity:
  - Add path traversal protection to DirContainer read/write/exists
  - Fix XPath injection in comments_editor.py merge_contiguous_links
  - Use parameterized SQL queries in database2.py library_id setter
  - Add safety comment to pickle_loads in utils/serialize.py

[ Other info ]
Upstream disscussion about this fix:
  https://github.com/kovidgoyal/calibre/pull/3101
Examine debdiff update from online:
  https://github.com/debian-
calibre/calibre/compare/debian/8.5.0+ds-1+deb13u2...debian/trixie
diff -Nru calibre-8.5.0+ds/debian/changelog calibre-8.5.0+ds/debian/changelog
--- calibre-8.5.0+ds/debian/changelog   2026-03-01 16:11:43.000000000 +0900
+++ calibre-8.5.0+ds/debian/changelog   2026-05-09 03:47:46.000000000 +0900
@@ -1,3 +1,18 @@
+calibre (8.5.0+ds-1+deb13u3) trixie; urgency=medium
+
+  * Fix security vulnerabilities and code quality issues
+    High severity:
+      * Fix typo normapth -> normpath in srv/content.py (broken endpoint)
+      * Replace eval() with ast.literal_eval() in catalogs/epub_mobi.py
+      * Log exceptions in FunctionDispatcher.dispatch instead of swallowing
+    Medium severity:
+      * Add path traversal protection to DirContainer read/write/exists
+      * Fix XPath injection in comments_editor.py merge_contiguous_links
+      * Use parameterized SQL queries in database2.py library_id setter
+      * Add safety comment to pickle_loads in utils/serialize.py
+
+ -- YOKOTA Hiroshi <[email protected]>  Sat, 09 May 2026 03:47:46 +0900
+
 calibre (8.5.0+ds-1+deb13u2) trixie; urgency=medium
 
   * CVE-2026-25635: CHM Input: Ignore internal files that have paths that
diff -Nru calibre-8.5.0+ds/debian/patches/series 
calibre-8.5.0+ds/debian/patches/series
--- calibre-8.5.0+ds/debian/patches/series      2026-03-01 16:11:43.000000000 
+0900
+++ calibre-8.5.0+ds/debian/patches/series      2026-05-09 03:39:18.000000000 
+0900
@@ -87,3 +87,4 @@
 upstream/0087-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch
 upstream/0088-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch
 upstream/0089-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch
+upstream/0090-Fix-security-vulnerabilities-and-code-quality-issues.patch
diff -Nru 
calibre-8.5.0+ds/debian/patches/upstream/0090-Fix-security-vulnerabilities-and-code-quality-issues.patch
 
calibre-8.5.0+ds/debian/patches/upstream/0090-Fix-security-vulnerabilities-and-code-quality-issues.patch
--- 
calibre-8.5.0+ds/debian/patches/upstream/0090-Fix-security-vulnerabilities-and-code-quality-issues.patch
    1970-01-01 09:00:00.000000000 +0900
+++ 
calibre-8.5.0+ds/debian/patches/upstream/0090-Fix-security-vulnerabilities-and-code-quality-issues.patch
    2026-05-09 03:39:18.000000000 +0900
@@ -0,0 +1,162 @@
+From: ECB <[email protected]>
+Date: Tue, 21 Apr 2026 12:48:42 +0200
+Subject: Fix security vulnerabilities and code quality issues
+
+Forwarded: not-needed
+Bug: https://github.com/kovidgoyal/calibre/pull/3101
+Origin: 
https://github.com/kovidgoyal/calibre/commit/b0c4ba19686232d5bff99d58ce6019546ef4d166
+
+High severity:
+- Fix typo normapth -> normpath in srv/content.py (broken endpoint)
+- Replace eval() with ast.literal_eval() in catalogs/epub_mobi.py
+- Log exceptions in FunctionDispatcher.dispatch instead of swallowing
+
+Medium severity:
+- Add path traversal protection to DirContainer read/write/exists
+- Fix XPath injection in comments_editor.py merge_contiguous_links
+- Use parameterized SQL queries in database2.py library_id setter
+- Add safety comment to pickle_loads in utils/serialize.py
+---
+ src/calibre/ebooks/oeb/base.py            | 12 +++++++++---
+ src/calibre/gui2/__init__.py              |  2 ++
+ src/calibre/gui2/comments_editor.py       |  2 +-
+ src/calibre/library/catalogs/epub_mobi.py | 10 ++++++----
+ src/calibre/library/database2.py          |  6 ++----
+ src/calibre/srv/content.py                |  2 +-
+ src/calibre/utils/serialize.py            |  2 +-
+ 7 files changed, 22 insertions(+), 14 deletions(-)
+
+diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py
+index 1192c87..44abb94 100644
+--- a/src/calibre/ebooks/oeb/base.py
++++ b/src/calibre/ebooks/oeb/base.py
+@@ -597,12 +597,16 @@ class DirContainer:
+     def read(self, path):
+         if path is None:
+             path = self.opfname
+-        path = os.path.join(self.rootdir, self._unquote(path))
++        path = os.path.abspath(os.path.join(self.rootdir, 
self._unquote(path)))
++        if not path.startswith(os.path.abspath(self.rootdir)):
++            raise ValueError(f'Path {path!r} is not inside {self.rootdir!r}')
+         with open(path, 'rb') as f:
+             return f.read()
+ 
+     def write(self, path, data):
+-        path = os.path.join(self.rootdir, self._unquote(path))
++        path = os.path.abspath(os.path.join(self.rootdir, 
self._unquote(path)))
++        if not path.startswith(os.path.abspath(self.rootdir)):
++            raise ValueError(f'Path {path!r} is not inside {self.rootdir!r}')
+         dir = os.path.dirname(path)
+         if not os.path.isdir(dir):
+             os.makedirs(dir)
+@@ -613,9 +617,11 @@ class DirContainer:
+         if not path:
+             return False
+         try:
+-            path = os.path.join(self.rootdir, self._unquote(path))
++            path = os.path.abspath(os.path.join(self.rootdir, 
self._unquote(path)))
+         except ValueError:  # Happens if path contains quoted special chars
+             return False
++        if not path.startswith(os.path.abspath(self.rootdir)):
++            return False
+         try:
+             return os.path.isfile(path)
+         except UnicodeEncodeError:
+diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
+index a5893d2..aca7df6 100644
+--- a/src/calibre/gui2/__init__.py
++++ b/src/calibre/gui2/__init__.py
+@@ -845,6 +845,8 @@ class FunctionDispatcher(QObject):
+         try:
+             res = self.func(*args, **kwargs)
+         except:
++            import traceback
++            traceback.print_exc()
+             res = None
+         q.put(res)
+ 
+diff --git a/src/calibre/gui2/comments_editor.py 
b/src/calibre/gui2/comments_editor.py
+index 8fb2919..6017c57 100644
+--- a/src/calibre/gui2/comments_editor.py
++++ b/src/calibre/gui2/comments_editor.py
+@@ -198,7 +198,7 @@ def use_implicit_styling_for_a(a, style_map):
+ def merge_contiguous_links(root):
+     all_hrefs = set(root.xpath('//a/@href'))
+     for href in all_hrefs:
+-        tags = root.xpath(f'//a[@href="{href}"]')
++        tags = root.xpath('//a[@href=$h]', h=href)
+         processed = set()
+ 
+         def insert_tag(parent, child):
+diff --git a/src/calibre/library/catalogs/epub_mobi.py 
b/src/calibre/library/catalogs/epub_mobi.py
+index 3e56266..e6408f6 100644
+--- a/src/calibre/library/catalogs/epub_mobi.py
++++ b/src/calibre/library/catalogs/epub_mobi.py
+@@ -351,10 +351,11 @@ class EPUB_MOBI(CatalogPlugin):
+             log.error(f"coercing thumb_width from '{opts.thumb_width}' to 
'{self.THUMB_SMALLEST}'")
+             opts.thumb_width = '1.0'
+ 
+-        # eval prefix_rules if passed from command line
++        # parse prefix_rules if passed from command line
+         if type(opts.prefix_rules) is not tuple:
+             try:
+-                opts.prefix_rules = eval(opts.prefix_rules)
++                import ast
++                opts.prefix_rules = ast.literal_eval(opts.prefix_rules)
+             except:
+                 log.error(f'malformed --prefix-rules: {opts.prefix_rules}')
+                 raise
+@@ -362,10 +363,11 @@ class EPUB_MOBI(CatalogPlugin):
+                 if len(rule) != 4:
+                     log.error(f'incorrect number of args for --prefix-rules: 
{rule!r}')
+ 
+-        # eval exclusion_rules if passed from command line
++        # parse exclusion_rules if passed from command line
+         if type(opts.exclusion_rules) is not tuple:
+             try:
+-                opts.exclusion_rules = eval(opts.exclusion_rules)
++                import ast
++                opts.exclusion_rules = ast.literal_eval(opts.exclusion_rules)
+             except:
+                 log.error(f'malformed --exclusion-rules: 
{opts.exclusion_rules}')
+                 raise
+diff --git a/src/calibre/library/database2.py 
b/src/calibre/library/database2.py
+index ff0d30e..646ad15 100644
+--- a/src/calibre/library/database2.py
++++ b/src/calibre/library/database2.py
+@@ -108,10 +108,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, 
CustomColumns):
+     @library_id.setter
+     def library_id(self, val):
+         self._library_id_ = str(val)
+-        self.conn.executescript(f'''
+-                DELETE FROM library_id;
+-                INSERT INTO library_id (uuid) VALUES ("{self._library_id_}");
+-                ''')
++        self.conn.execute('DELETE FROM library_id')
++        self.conn.execute('INSERT INTO library_id (uuid) VALUES (?)', 
(self._library_id_,))
+         self.conn.commit()
+ 
+     def connect(self):
+diff --git a/src/calibre/srv/content.py b/src/calibre/srv/content.py
+index 0094580..2b1a0e5 100644
+--- a/src/calibre/srv/content.py
++++ b/src/calibre/srv/content.py
+@@ -306,7 +306,7 @@ def icon(ctx, rd, which):
+ 
+ @endpoint('/reader-background/{encoded_fname}', android_workaround=True)
+ def reader_background(ctx, rd, encoded_fname):
+-    base = os.path.abspath(os.path.normapth(os.path.join(config_dir, 
'viewer', 'background-images')))
++    base = os.path.abspath(os.path.normpath(os.path.join(config_dir, 
'viewer', 'background-images')))
+     fname = bytes.fromhex(encoded_fname)
+     q = os.path.abspath(os.path.normpath(os.path.join(base, fname)))
+     if not q.startswith(base):
+diff --git a/src/calibre/utils/serialize.py b/src/calibre/utils/serialize.py
+index 81e187f..75f3370 100644
+--- a/src/calibre/utils/serialize.py
++++ b/src/calibre/utils/serialize.py
+@@ -119,4 +119,4 @@ def pickle_dumps(data):
+ 
+ def pickle_loads(dump):
+     import pickle
+-    return pickle.loads(dump, encoding='utf-8')
++    return pickle.loads(dump, encoding='utf-8')  # nosec: only used for 
calibre's own serialized data

Reply via email to