Bug#1143581: trixie-pu: package calibre/8.5.0+ds-1+deb13u4
YOKOTA Hiroshi <[email protected]> Tue, 04 Aug 2026 21:53:46 +0900
| Newsgroups | gmane.linux.debian.devel.release |
|---|---|
| Message-ID | <178584802670.497691.9669251083012201823.reportbug__24482.3211150774$1785848174$gmane$org@loadstone.darkstar.local> |
This is a multi-part MIME message sent by reportbug. --===============4685015674154026920== Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Content-Disposition: inline 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 1143484 https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1143484 [ Impact ] Failed to convert ePub file with obfuscated fonts [ Tests ] Build time unit test was successful. [ Risks ] I don't have ePub file with obfuscated fonts. So I can't test specific file conversions. [ 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 ] Add fixup to CVE-2026-25636 fix that in 8.5.0+ds-1+deb13u2. [ Other info ] debdiff is too big because it includes file rename. I also add minimized diff file that drops renamed files. You can examine from online: https://github.com/debian- calibre/calibre/compare/debian/8.5.0+ds-1+deb13u3...debian/trixie --===============4685015674154026920== Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="calibre_8.5.0+ds-1+deb13u4.debdiff" diff -Nru calibre-8.5.0+ds/debian/changelog calibre-8.5.0+ds/debian/changelog --- calibre-8.5.0+ds/debian/changelog 2026-05-25 08:19:53.000000000 +0900 +++ calibre-8.5.0+ds/debian/changelog 2026-08-04 16:13:08.000000000 +0900 @@ -1,3 +1,15 @@ +calibre (8.5.0+ds-1+deb13u4) trixie; urgency=medium + + * This update adds fixup for CVE-2026-25636 patch in 8.5.0+ds-1+deb13u2 + + * Set default Debian branch name for trixie + * CVE-2026-25636 (1/2): EPUB Input: Ensure font obfuscation processing + happens only on files from the EPUB (Closes: #1143484) + * Rename a CVE-2026-25636 patch in 8.5.0+ds-1+deb13u2 + * Renumber patches + + -- YOKOTA Hiroshi <[email protected]> Tue, 04 Aug 2026 16:13:08 +0900 + calibre (8.5.0+ds-1+deb13u3) trixie; urgency=medium * Fix security vulnerabilities and code quality issues (Closes: #1135543) diff -Nru calibre-8.5.0+ds/debian/gbp.conf calibre-8.5.0+ds/debian/gbp.conf --- calibre-8.5.0+ds/debian/gbp.conf 2026-05-25 08:19:53.000000000 +0900 +++ calibre-8.5.0+ds/debian/gbp.conf 2026-08-04 16:13:08.000000000 +0900 @@ -1,5 +1,6 @@ [DEFAULT] pristine-tar = True +debian-branch = debian/trixie [dch] full = True 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-05-25 08:19:53.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/series 2026-08-04 16:13:08.000000000 +0900 @@ -81,14 +81,15 @@ 0081-Revert-Update-7zip-wrapper-code-for-removal-of-read-.patch upstream/0082-Fix-CVE-2025-64486.patch upstream/0083-CVE-2026-25635-CHM-Input-Ignore-internal-files-that-.patch -upstream/0084-CVE-2026-25636-DRYer.patch -upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch -upstream/0086-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch -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 -upstream/0091-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch -upstream/0092-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch -upstream/0093-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch -upstream/0094-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch +upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch +upstream/0085-CVE-2026-25636-2-2-DRYer.patch +upstream/0086-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch +upstream/0087-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch +upstream/0088-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch +upstream/0089-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch +upstream/0090-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch +upstream/0091-Fix-security-vulnerabilities-and-code-quality-issues.patch +upstream/0092-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch +upstream/0093-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch +upstream/0094-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch +upstream/0095-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch --- calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch 1970-01-01 09:00:00.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch 2026-08-04 16:13:08.000000000 +0900 @@ -0,0 +1,63 @@ +From: Kovid Goyal <[email protected]> +Date: Mon, 2 Feb 2026 09:51:18 +0530 +Subject: CVE-2026-25636 (1/2): EPUB Input: Ensure font obfuscation processing + happens only on files from the EPUB + +Forwarded: not-needed +Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-8r26-m7j5-hm29 +Origin: backport, https://github.com/kovidgoyal/calibre/commit/267bfd34020a4f297c2de9cc0cde50ebe5d024d4 +Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1143484 + +Signed-off-by: YOKOTA Hiroshi <[email protected]> +--- + src/calibre/ebooks/conversion/plugins/epub_input.py | 4 +++- + src/calibre/utils/filenames.py | 14 ++++++++++++++ + 2 files changed, 17 insertions(+), 1 deletion(-) + +diff --git a/src/calibre/ebooks/conversion/plugins/epub_input.py b/src/calibre/ebooks/conversion/plugins/epub_input.py +index 2505169..f62fed1 100644 +--- a/src/calibre/ebooks/conversion/plugins/epub_input.py ++++ b/src/calibre/ebooks/conversion/plugins/epub_input.py +@@ -44,6 +44,8 @@ class EPUBInput(InputFormatPlugin): + import uuid + + from lxml import etree ++ ++ from calibre.utils.filenames import is_existing_subpath + idpf_key = opf.raw_unique_identifier + if idpf_key: + idpf_key = re.sub(r'[ \t\r\n]', '', idpf_key) +@@ -74,7 +76,7 @@ class EPUBInput(InputFormatPlugin): + uri = cr.get('URI') + path = os.path.abspath(os.path.join(os.path.dirname(encfile), '..', *uri.split('/'))) + tkey = (key if algorithm == ADOBE_OBFUSCATION else idpf_key) +- if (tkey and os.path.exists(path)): ++ if (tkey and is_existing_subpath(path, os.getcwd())): + self._encrypted_font_uris.append(uri) + decrypt_font(tkey, path, algorithm) + return True +diff --git a/src/calibre/utils/filenames.py b/src/calibre/utils/filenames.py +index 5182f75..1cb07f4 100644 +--- a/src/calibre/utils/filenames.py ++++ b/src/calibre/utils/filenames.py +@@ -644,6 +644,20 @@ def copytree_using_links(path, dest, dest_is_parent=True, filecopyfunc=copyfile) + filecopyfunc(src, df) + + ++def is_existing_subpath(child: str, parent: str) -> bool: ++ ' Check if child is under parent. If either child or parent dont exist, returns False. ' ++ try: ++ parent = os.path.realpath(parent, strict=True) # resolve symlinks ++ child = os.path.realpath(child, strict=True) ++ except OSError: ++ return False ++ parent = os.path.abspath(parent) ++ child = os.path.abspath(child) ++ if not parent.endswith(os.sep): ++ parent += os.sep ++ return child.startswith(parent) ++ ++ + rmtree = shutil.rmtree + + diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch --- calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch 2026-05-25 08:19:53.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch 1970-01-01 09:00:00.000000000 +0900 @@ -1,37 +0,0 @@ -From: Kovid Goyal <[email protected]> -Date: Mon, 2 Feb 2026 11:25:09 +0530 -Subject: CVE-2026-25636: DRYer - -Forwarded: not-needed -Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-8r26-m7j5-hm29 -Origin: backport, https://github.com/kovidgoyal/calibre/commit/9484ea82c6ab226c18e6ca5aa000fa16de598726 - -Signed-off-by: YOKOTA Hiroshi <[email protected]> ---- - src/calibre/ebooks/conversion/plugins/epub_input.py | 6 ++++-- - 1 file changed, 4 insertions(+), 2 deletions(-) - -diff --git a/src/calibre/ebooks/conversion/plugins/epub_input.py b/src/calibre/ebooks/conversion/plugins/epub_input.py -index 2505169..edfebee 100644 ---- a/src/calibre/ebooks/conversion/plugins/epub_input.py -+++ b/src/calibre/ebooks/conversion/plugins/epub_input.py -@@ -66,15 +66,17 @@ class EPUBInput(InputFormatPlugin): - - try: - root = etree.parse(encfile) -+ base = os.path.dirname(encfile) -+ container_base = os.path.dirname(base) - for em in root.xpath('descendant::*[contains(name(), "EncryptionMethod")]'): - algorithm = em.get('Algorithm', '') - if algorithm not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}: - return False - cr = em.getparent().xpath('descendant::*[contains(name(), "CipherReference")]')[0] - uri = cr.get('URI') -- path = os.path.abspath(os.path.join(os.path.dirname(encfile), '..', *uri.split('/'))) -+ path = os.path.abspath(os.path.join(base, '..', *uri.split('/'))) - tkey = (key if algorithm == ADOBE_OBFUSCATION else idpf_key) -- if (tkey and os.path.exists(path)): -+ if (tkey and is_existing_subpath(path, container_base)): - self._encrypted_font_uris.append(uri) - decrypt_font(tkey, path, algorithm) - return True diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25636-2-2-DRYer.patch calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25636-2-2-DRYer.patch --- calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25636-2-2-DRYer.patch 1970-01-01 09:00:00.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25636-2-2-DRYer.patch 2026-08-04 16:13:08.000000000 +0900 @@ -0,0 +1,37 @@ +From: Kovid Goyal <[email protected]> +Date: Mon, 2 Feb 2026 11:25:09 +0530 +Subject: CVE-2026-25636 (2/2): DRYer + +Forwarded: not-needed +Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-8r26-m7j5-hm29 +Origin: backport, https://github.com/kovidgoyal/calibre/commit/9484ea82c6ab226c18e6ca5aa000fa16de598726 + +Signed-off-by: YOKOTA Hiroshi <[email protected]> +--- + src/calibre/ebooks/conversion/plugins/epub_input.py | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/src/calibre/ebooks/conversion/plugins/epub_input.py b/src/calibre/ebooks/conversion/plugins/epub_input.py +index f62fed1..6b81949 100644 +--- a/src/calibre/ebooks/conversion/plugins/epub_input.py ++++ b/src/calibre/ebooks/conversion/plugins/epub_input.py +@@ -68,15 +68,17 @@ class EPUBInput(InputFormatPlugin): + + try: + root = etree.parse(encfile) ++ base = os.path.dirname(encfile) ++ container_base = os.path.dirname(base) + for em in root.xpath('descendant::*[contains(name(), "EncryptionMethod")]'): + algorithm = em.get('Algorithm', '') + if algorithm not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}: + return False + cr = em.getparent().xpath('descendant::*[contains(name(), "CipherReference")]')[0] + uri = cr.get('URI') +- path = os.path.abspath(os.path.join(os.path.dirname(encfile), '..', *uri.split('/'))) ++ path = os.path.abspath(os.path.join(base, '..', *uri.split('/'))) + tkey = (key if algorithm == ADOBE_OBFUSCATION else idpf_key) +- if (tkey and is_existing_subpath(path, os.getcwd())): ++ if (tkey and is_existing_subpath(path, container_base)): + self._encrypted_font_uris.append(uri) + decrypt_font(tkey, path, algorithm) + return True diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch --- calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch 2026-05-25 08:19:53.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch 1970-01-01 09:00:00.000000000 +0900 @@ -1,588 +0,0 @@ -From: Kovid Goyal <[email protected]> -Date: Thu, 5 Feb 2026 14:21:25 +0530 -Subject: CVE-2026-25731: ZIP Output: Change the template engine used for HTML - templating from templite to Mustache, - for greater safety and performance. Note that this is a breaking change if - you use custom templates with ZIP output. - -Forwarded: not-needed -Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-xrh9-w7qx-3gcc -Origin: backport, https://github.com/kovidgoyal/calibre/commit/f0649b27512e987b95fcab2e1e0a3bcdafc23379 - -Signed-off-by: YOKOTA Hiroshi <[email protected]> ---- - COPYRIGHT | 6 -- - pyproject.toml | 5 +- - resources/templates/html_export_default.mustache | 70 ++++++++++++++++ - resources/templates/html_export_default.tmpl | 74 ----------------- - .../templates/html_export_default_index.mustache | 55 +++++++++++++ - resources/templates/html_export_default_index.tmpl | 61 -------------- - .../ebooks/conversion/plugins/html_output.py | 61 ++++++++------ - src/templite/__init__.py | 96 ---------------------- - 8 files changed, 164 insertions(+), 264 deletions(-) - create mode 100644 resources/templates/html_export_default.mustache - delete mode 100644 resources/templates/html_export_default.tmpl - create mode 100644 resources/templates/html_export_default_index.mustache - delete mode 100644 resources/templates/html_export_default_index.tmpl - delete mode 100644 src/templite/__init__.py - -diff --git a/COPYRIGHT b/COPYRIGHT -index a44d756..09e1308 100644 ---- a/COPYRIGHT -+++ b/COPYRIGHT -@@ -12,12 +12,6 @@ Files: resources/rapydscript/* - Copyright: Various - License: BSD - --Files: src/templite/* --Copyright: Copyright (c) 2009 joonis new media, Thimo Kraemer --License: GPL-2+ -- The full text of the GPL is distributed as in -- /usr/share/common-licenses/GPL-2 on Debian systems. -- - Files: src/calibre/devices/bambook/* - Copyright: 2010, Li Fanxi - License: GPL-3 -diff --git a/pyproject.toml b/pyproject.toml -index b3697bd..f7ef5cb 100644 ---- a/pyproject.toml -+++ b/pyproject.toml -@@ -26,7 +26,6 @@ exclude = [ - "setup/linux-installer.py", - "src/css_selectors/*", - "src/polyglot/*", -- "src/templite/*", - "src/tinycss/*", - ] - preview = true -@@ -71,7 +70,7 @@ unfixable = ['PIE794', 'ISC001'] - detect-same-package = true - extra-standard-library = ["aes", "elementmaker", "encodings"] - known-first-party = ["calibre_extensions", "calibre_plugins", "polyglot"] --known-third-party = ["odf", "qt", "templite", "tinycss", "css_selectors"] -+known-third-party = ["odf", "qt", "tinycss", "css_selectors"] - relative-imports-order = "closest-to-furthest" - split-on-trailing-comma = false - section-order = ['__python__', "future", "standard-library", "third-party", "first-party", "local-folder"] -@@ -189,7 +188,6 @@ skip = [ - "./setup/linux-installer.py", - "./src/css_selectors/*", - "./src/polyglot/*", -- "./src/templite/*", - "./src/tinycss/*", - "./src/unicode_names/*", - ] -@@ -205,7 +203,6 @@ exclude = [ - "src/calibre/gui2/store/stores/", - "src/css_selectors/", - "src/polyglot/", -- "src/templite/", - "src/tinycss/", - ] - -diff --git a/resources/templates/html_export_default.mustache b/resources/templates/html_export_default.mustache -new file mode 100644 -index 0000000..1c8691a ---- /dev/null -+++ b/resources/templates/html_export_default.mustache -@@ -0,0 +1,70 @@ -+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -+<html xmlns="http://www.w3.org/1999/xhtml"> -+<head> -+{{{head_content}}} -+ -+<link href="{{css_link}}" type="text/css" rel="stylesheet" /> -+ -+</head> -+<body> -+ -+<div class="calibreMeta"> -+ <div class="calibreMetaTitle"> -+ {{#meta.titles}} -+ {{#is_first}} -+ <h1><a href="{{toc_url}}">{{title}}</a> </h1> -+ {{/is_first}} -+ {{^is_first}} -+ <div class="calibreMetaSubtitle">{{title}}</div> -+ {{/is_first}} -+ {{/meta.titles}} -+ </div> -+ <div class="calibreMetaAuthor">{{meta.creators}}</div> -+</div> -+ -+<div class="calibreMain"> -+ -+ <div class="calibreEbookContent"> -+ {{#has_link}} -+ <div class="calibreEbNavTop"> -+ {{#prev_link}} -+ <a href="{{prev_link}}" class="calibreAPrev">{{prev_page}}</a> -+ {{/prev_link}} -+ {{^prev_link}} -+ <a href="{{toc_url}}" class="calibreAPrev">{{prev_page}}</a> -+ {{/prev_link}} -+ {{#next_link}} -+ <a href="{{next_link}}" class="calibreANext">{{next_page}}</a> -+ {{/next_link}} -+ </div> -+ {{/has_link}} -+ -+ {{{ebook_content}}} -+ </div> -+ -+ {{#has_toc}} -+ <div class="calibreToc"> -+ <h2><a href="{{toc_url}}">{{table_of_contents}}</a></h2> -+ {{{toc}}} -+ </div> -+ {{/has_toc}} -+ -+ <div class="calibreEbNav"> -+ {{#prev_link}} -+ <a href="{{prev_link}}" class="calibreAPrev">{{prev_page}}</a> -+ {{/prev_link}} -+ {{^prev_link}} -+ <a href="{{toc_url}}" class="calibreAPrev">{{prev_page}}</a> -+ {{/prev_link}} -+ -+ <a href="{{toc_url}}" class="calibreAHome">{{start}}</a> -+ -+ {{#next_link}} -+ <a href="{{next_link}}" class="calibreANext">{{next_page}}</a> -+ {{/next_link}} -+ </div> -+ -+</div> -+ -+</body> -+</html> -diff --git a/resources/templates/html_export_default.tmpl b/resources/templates/html_export_default.tmpl -deleted file mode 100644 -index 7aac247..0000000 ---- a/resources/templates/html_export_default.tmpl -+++ /dev/null -@@ -1,74 +0,0 @@ --<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> --<html xmlns="http://www.w3.org/1999/xhtml"> --<head> --${head_content}$ -- --<link href="${cssLink}$" type="text/css" rel="stylesheet" /> -- --</head> --<body> -- --<div class="calibreMeta"> -- <div class="calibreMetaTitle"> -- ${pos1=1}$ -- ${for title in meta.titles():}$ -- ${if pos1:}$ -- <h1> -- <a href="${tocUrl}$">${print(title)}$</a> -- </h1> -- ${:else:}$ -- <div class="calibreMetaSubtitle">${print(title)}$</div> -- ${:endif}$ -- ${pos1=0}$ -- ${:endfor}$ -- </div> -- <div class="calibreMetaAuthor"> -- ${print(', '.join(meta.creators()))}$ -- </div> --</div> -- --<div class="calibreMain"> -- -- <div class="calibreEbookContent"> -- ${if prevLink or nextLink:}$ -- <div class="calibreEbNavTop"> -- ${if prevLink:}$ -- <a href="${prevLink}$" class="calibreAPrev">${print(_('previous page'))}$</a> -- ${:else:}$ -- <a href="${tocUrl}$" class="calibreAPrev">${print(_('previous page'))}$</a> -- ${:endif}$ -- -- ${if nextLink:}$ -- <a href="${nextLink}$" class="calibreANext">${print(_('next page'))}$</a> -- ${:endif}$ -- </div> -- ${:endif}$ -- -- ${ebookContent}$ -- </div> -- -- ${if has_toc:}$ -- <div class="calibreToc"> -- <h2><a href="${tocUrl}$">${print( _('Table of contents'))}$</a></h2> -- ${print(toc())}$ -- </div> -- ${:endif}$ -- -- <div class="calibreEbNav"> -- ${if prevLink:}$ -- <a href="${prevLink}$" class="calibreAPrev">${print(_('previous page'))}$</a> -- ${:else:}$ -- <a href="${tocUrl}$" class="calibreAPrev">${print(_('previous page'))}$</a> -- ${:endif}$ -- -- <a href="${tocUrl}$" class="calibreAHome">${print(_('start'))}$</a> -- -- ${if nextLink:}$ -- <a href="${nextLink}$" class="calibreANext">${print(_('next page'))}$</a> -- ${:endif}$ -- </div> -- --</div> -- --</body> --</html> -diff --git a/resources/templates/html_export_default_index.mustache b/resources/templates/html_export_default_index.mustache -new file mode 100644 -index 0000000..aa1bc4d ---- /dev/null -+++ b/resources/templates/html_export_default_index.mustache -@@ -0,0 +1,55 @@ -+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -+<html xmlns="http://www.w3.org/1999/xhtml"> -+<head> -+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> -+ -+<link rel="schema.DC" href="http://purl.org/dc/elements/1.1/" /> -+<link rel="schema.DCTERMS" href="http://purl.org/dc/terms/" /> -+ -+<title>{{meta.creators}} - {{meta.first_title}}</title> -+ -+{{#meta.items}} -+ <meta name="DC.{{name}}" content="{{value}}" /> -+{{/meta.items}} -+ -+<link href="{{css_link}}" type="text/css" rel="stylesheet" /> -+</head> -+<body> -+ -+<div class="calibreMeta"> -+ <div class="calibreMetaTitle"> -+ {{#meta.titles}} -+ {{#is_first}} -+ <h1><a href="{{toc_url}}">{{title}}</a> </h1> -+ {{/is_first}} -+ {{^is_first}} -+ <div class="calibreMetaSubtitle">{{title}}</div> -+ {{/is_first}} -+ {{/meta.titles}} -+ </div> -+ <div class="calibreMetaAuthor">{{meta.creators}}</div> -+</div> -+ -+<div class="calibreMain"> -+ <div class="calibreEbookContent"> -+ {{#has_toc}} -+ <div class="calibreTocIndex"> -+ <h2>{{table_of_contents}}</h2> -+ {{{toc}}} -+ </div> -+ {{/has_toc}} -+ {{^has_toc}} -+ <h2>{{no_toc}}</h2> -+ <div><strong><a href="{{next_link}}">{{begin_to_read}}</a></strong></div> -+ {{/has_toc}} -+ </div> -+ -+ <div class="calibreEbNav"> -+ {{#next_link}} -+ <a href="{{next_link}}" class="calibreANext">{{next_page}}</a> -+ {{/next_link}} -+ </div> -+</div> -+ -+</body> -+</html> -diff --git a/resources/templates/html_export_default_index.tmpl b/resources/templates/html_export_default_index.tmpl -deleted file mode 100644 -index f0665ad..0000000 ---- a/resources/templates/html_export_default_index.tmpl -+++ /dev/null -@@ -1,61 +0,0 @@ --<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> --<html xmlns="http://www.w3.org/1999/xhtml"> --<head> --<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> -- --<link rel="schema.DC" href="http://purl.org/dc/elements/1.1/" /> --<link rel="schema.DCTERMS" href="http://purl.org/dc/terms/" /> -- --<title>${print(', '.join(meta.creators()))}$ - ${print(next(meta.titles())); print(meta.titles().close())}$</title> -- --${for item in meta:}$ -- <meta ${print('name="DC.'+item['name']+'"')}$ ${print('content="'+item['value']+'"')}$ /> --${:endfor}$ -- --<link href="${cssLink}$" type="text/css" rel="stylesheet" /> --</head> --<body> -- --<div class="calibreMeta"> -- <div class="calibreMetaTitle"> -- ${pos1=1}$ -- ${for title in meta.titles():}$ -- ${if pos1:}$ -- <h1> -- <a href="${tocUrl}$">${print(title)}$</a> -- </h1> -- ${:else:}$ -- <div class="calibreMetaSubtitle">${print(title)}$</div> -- ${:endif}$ -- ${pos1=0}$ -- ${:endfor}$ -- </div> -- <div class="calibreMetaAuthor"> -- ${print(', '.join(meta.creators()))}$ -- </div> --</div> -- --<div class="calibreMain"> -- <div class="calibreEbookContent"> -- -- ${if has_toc:}$ -- <div class="calibreTocIndex"> -- <h2>${print(_('Table of contents'))}$</h2> -- ${toc}$ -- </div> -- ${:else:}$ -- <h2>${print(_('No table of contents present'))}$</h2> -- <div><strong><a href="${nextLink}$">${print(_('begin to read'))}$</a></strong></div> -- ${:endif}$ -- -- </div> -- -- <div class="calibreEbNav"> -- ${if nextLink:}$ -- <a href="${nextLink}$" class="calibreANext">${print(_('next page'))}$</a> -- ${:endif}$ -- </div> --</div> -- --</body> --</html> -diff --git a/src/calibre/ebooks/conversion/plugins/html_output.py b/src/calibre/ebooks/conversion/plugins/html_output.py -index ea64c70..5573d04 100644 ---- a/src/calibre/ebooks/conversion/plugins/html_output.py -+++ b/src/calibre/ebooks/conversion/plugins/html_output.py -@@ -27,13 +27,13 @@ class HTMLOutput(OutputFormatPlugin): - - options = { - OptionRecommendation(name='template_css', -- help=_('CSS file used for the output instead of the default file')), -+ help=_('CSS file used for the output instead of the default CSS.')), - - OptionRecommendation(name='template_html_index', -- help=_('Template used for generation of the HTML index file instead of the default file')), -+ help=_('Template used for generation of the HTML index file instead of the default template. In Mustache format.')), - - OptionRecommendation(name='template_html', -- help=_('Template used for the generation of the HTML contents of the book instead of the default file')), -+ help=_('Template used for the generation of the HTML contents of the book instead of the default template. In Mustache format.')), - - OptionRecommendation(name='extract_to', - help=_('Extract the contents of the generated ZIP file to the ' -@@ -85,8 +85,8 @@ class HTMLOutput(OutputFormatPlugin): - xml_declaration=False) - - def convert(self, oeb_book, output_path, input_plugin, opts, log): -+ import pystache - from lxml import etree -- from templite import Templite - - from calibre.ebooks.html.meta import EasyMeta - from calibre.utils import zipfile -@@ -97,7 +97,7 @@ class HTMLOutput(OutputFormatPlugin): - with open(opts.template_html_index, 'rb') as f: - template_html_index_data = f.read() - else: -- template_html_index_data = P('templates/html_export_default_index.tmpl', data=True) -+ template_html_data = P('templates/html_export_default.mustache', data=True) - - if opts.template_html is not None: - with open(opts.template_html, 'rb') as f: -@@ -111,9 +111,10 @@ class HTMLOutput(OutputFormatPlugin): - else: - template_css_data = P('templates/html_export_default.css', data=True) - -- template_html_index_data = template_html_index_data.decode('utf-8') -- template_html_data = template_html_data.decode('utf-8') -+ template_html_index = pystache.parse(template_html_index_data.decode('utf-8')) -+ template_html = pystache.parse(template_html_data.decode('utf-8')) - template_css_data = template_css_data.decode('utf-8') -+ has_toc = bool(oeb_book.toc.count()) - - self.log = log - self.opts = opts -@@ -130,18 +131,31 @@ class HTMLOutput(OutputFormatPlugin): - css_path = output_dir+os.sep+'calibreHtmlOutBasicCss.css' - with open(css_path, 'wb') as f: - f.write(template_css_data.encode('utf-8')) -+ meta_dict = { -+ 'titles': [{'title': x, 'is_first': i == 0} for i, x in enumerate(meta.titles())], -+ 'creators': authors_to_string(tuple(meta.creators())), -+ 'items': list(meta), -+ } -+ meta_dict['first_title'] = meta_dict['titles'][0]['title'] if meta_dict['titles'] else '' -+ basic_template_vars = { -+ 'meta': meta_dict, 'has_toc': has_toc, -+ 'table_of_contents': _('Table of contents'), 'no_toc': _('No table of contents present'), -+ 'begin_to_read': _('begin to read'), 'start': _('start'), -+ 'prev_page': _('previous page'), 'next_page': _('next page'), -+ } - - with open(output_file, 'wb') as f: -- html_toc = self.generate_html_toc(oeb_book, output_file, output_dir) -- templite = Templite(template_html_index_data) - nextLink = oeb_book.spine[0].href - nextLink = relpath(output_dir+os.sep+nextLink, dirname(output_file)) - cssLink = relpath(abspath(css_path), dirname(output_file)) - tocUrl = relpath(output_file, dirname(output_file)) -- t = templite.render(has_toc=bool(oeb_book.toc.count()), -- toc=html_toc, meta=meta, nextLink=nextLink, -- tocUrl=tocUrl, cssLink=cssLink, -- firstContentPageLink=nextLink) -+ toc_as_html = self.generate_html_toc(oeb_book, output_file, output_dir) if has_toc else '' -+ v = basic_template_vars.copy() -+ v.update({ -+ 'toc': toc_as_html, 'css_link': cssLink, 'toc_url': tocUrl, 'next_link': nextLink, -+ 'first_content_page_link': nextLink, -+ }) -+ t = pystache.render(template_html_index, v) - if isinstance(t, str): - t = t.encode('utf-8') - f.write(t) -@@ -197,17 +211,18 @@ class HTMLOutput(OutputFormatPlugin): - firstContentPageLink = oeb_book.spine[0].href - - # render template -- templite = Templite(template_html_data) -- - def toc(): -- return self.generate_html_toc(oeb_book, path, output_dir) -- t = templite.render(ebookContent=ebook_content, -- prevLink=prevLink, nextLink=nextLink, -- has_toc=bool(oeb_book.toc.count()), toc=toc, -- tocUrl=tocUrl, head_content=head_content, -- meta=meta, cssLink=cssLink, -- firstContentPageLink=firstContentPageLink) -- -+ return -+ toc_as_html = self.generate_html_toc(oeb_book, path, output_dir) if has_toc else '' -+ v = basic_template_vars.copy() -+ v.update({ -+ 'has_link': prevLink or nextLink, -+ 'prev_link': prevLink, 'next_link': nextLink, 'toc_url': tocUrl, -+ 'head_content': head_content, 'ebook_content': ebook_content, -+ 'css_link': cssLink, 'toc': toc_as_html, -+ 'first_content_page_link': firstContentPageLink, -+ }) -+ t = pystache.render(template_html, v) - # write html to file - with open(path, 'wb') as f: - f.write(t.encode('utf-8')) -diff --git a/src/templite/__init__.py b/src/templite/__init__.py -deleted file mode 100644 -index 8723d0d..0000000 ---- a/src/templite/__init__.py -+++ /dev/null -@@ -1,96 +0,0 @@ --#!/usr/bin/env python --# --# Templite+ --# A light-weight, fully functional, general purpose templating engine --# --# Copyright (c) 2009 joonis new media --# Author: Thimo Kraemer <[email protected]> --# --# Based on Templite - Tomer Filiba --# http://code.activestate.com/recipes/496702/ --# --# This program is free software; you can redistribute it and/or modify --# it under the terms of the GNU General Public License as published by --# the Free Software Foundation; either version 2 of the License, or --# (at your option) any later version. --# --# This program is distributed in the hope that it will be useful, --# but WITHOUT ANY WARRANTY; without even the implied warranty of --# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --# GNU General Public License for more details. --# --# You should have received a copy of the GNU General Public License --# along with this program; if not, write to the Free Software --# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, --# MA 02110-1301, USA. --# -- --import re --import sys -- --from polyglot.builtins import unicode_type -- -- --class Templite: -- auto_emit = re.compile(r'''(^['"])|(^[a-zA-Z0-9_[\]'"]+$)''') -- -- def __init__(self, template, start='${', end='}$'): -- if len(start) != 2 or len(end) != 2: -- raise ValueError('each delimiter must be two characters long') -- delimiter = re.compile('%s(.*?)%s' % (re.escape(start), re.escape(end)), re.DOTALL) -- offset = 0 -- tokens = [] -- for i, part in enumerate(delimiter.split(template)): -- part = part.replace('\\'.join(list(start)), start) -- part = part.replace('\\'.join(list(end)), end) -- if i % 2 == 0: -- if not part: -- continue -- part = part.replace('\\', '\\\\').replace('"', '\\"') -- part = '\t' * offset + 'emit("""%s""")' % part -- else: -- part = part.rstrip() -- if not part: -- continue -- if part.lstrip().startswith(':'): -- if not offset: -- raise SyntaxError('no block statement to terminate: ${%s}$' % part) -- offset -= 1 -- part = part.lstrip()[1:] -- if not part.endswith(':'): -- continue -- elif self.auto_emit.match(part.lstrip()): -- part = 'emit(%s)' % part.lstrip() -- lines = part.splitlines() -- margin = min(len(l) - len(l.lstrip()) for l in lines if l.strip()) -- part = '\n'.join('\t' * offset + l[margin:] for l in lines) -- if part.endswith(':'): -- offset += 1 -- tokens.append(part) -- if offset: -- raise SyntaxError('%i block statement(s) not terminated' % offset) -- self.__code = compile('\n'.join(tokens), '<templite %r>' % template[:20], 'exec') -- -- def render(self, __namespace=None, **kw): -- """ -- renders the template according to the given namespace. -- __namespace - a dictionary serving as a namespace for evaluation -- **kw - keyword arguments which are added to the namespace -- """ -- namespace = {} -- if __namespace: -- namespace.update(__namespace) -- if kw: -- namespace.update(kw) -- namespace['emit'] = self.write -- -- __stdout = sys.stdout -- sys.stdout = self -- self.__output = [] -- eval(self.__code, namespace) -- sys.stdout = __stdout -- return ''.join(self.__output) -- -- def write(self, *args): -- for a in args: -- self.__output.append(unicode_type(a)) diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0086-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch calibre-8.5.0+ds/debian/patches/upstream/0086-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch --- calibre-8.5.0+ds/debian/patches/upstream/0086-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch 1970-01-01 09:00:00.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0086-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch 2026-08-04 16:13:08.000000000 +0900 @@ -0,0 +1,588 @@ +From: Kovid Goyal <[email protected]> +Date: Thu, 5 Feb 2026 14:21:25 +0530 +Subject: CVE-2026-25731: ZIP Output: Change the template engine used for HTML + templating from templite to Mustache, + for greater safety and performance. Note that this is a breaking change if + you use custom templates with ZIP output. + +Forwarded: not-needed +Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-xrh9-w7qx-3gcc +Origin: backport, https://github.com/kovidgoyal/calibre/commit/f0649b27512e987b95fcab2e1e0a3bcdafc23379 + +Signed-off-by: YOKOTA Hiroshi <[email protected]> +--- + COPYRIGHT | 6 -- + pyproject.toml | 5 +- + resources/templates/html_export_default.mustache | 70 ++++++++++++++++ + resources/templates/html_export_default.tmpl | 74 ----------------- + .../templates/html_export_default_index.mustache | 55 +++++++++++++ + resources/templates/html_export_default_index.tmpl | 61 -------------- + .../ebooks/conversion/plugins/html_output.py | 61 ++++++++------ + src/templite/__init__.py | 96 ---------------------- + 8 files changed, 164 insertions(+), 264 deletions(-) + create mode 100644 resources/templates/html_export_default.mustache + delete mode 100644 resources/templates/html_export_default.tmpl + create mode 100644 resources/templates/html_export_default_index.mustache + delete mode 100644 resources/templates/html_export_default_index.tmpl + delete mode 100644 src/templite/__init__.py + +diff --git a/COPYRIGHT b/COPYRIGHT +index a44d756..09e1308 100644 +--- a/COPYRIGHT ++++ b/COPYRIGHT +@@ -12,12 +12,6 @@ Files: resources/rapydscript/* + Copyright: Various + License: BSD + +-Files: src/templite/* +-Copyright: Copyright (c) 2009 joonis new media, Thimo Kraemer +-License: GPL-2+ +- The full text of the GPL is distributed as in +- /usr/share/common-licenses/GPL-2 on Debian systems. +- + Files: src/calibre/devices/bambook/* + Copyright: 2010, Li Fanxi + License: GPL-3 +diff --git a/pyproject.toml b/pyproject.toml +index b3697bd..f7ef5cb 100644 +--- a/pyproject.toml ++++ b/pyproject.toml +@@ -26,7 +26,6 @@ exclude = [ + "setup/linux-installer.py", + "src/css_selectors/*", + "src/polyglot/*", +- "src/templite/*", + "src/tinycss/*", + ] + preview = true +@@ -71,7 +70,7 @@ unfixable = ['PIE794', 'ISC001'] + detect-same-package = true + extra-standard-library = ["aes", "elementmaker", "encodings"] + known-first-party = ["calibre_extensions", "calibre_plugins", "polyglot"] +-known-third-party = ["odf", "qt", "templite", "tinycss", "css_selectors"] ++known-third-party = ["odf", "qt", "tinycss", "css_selectors"] + relative-imports-order = "closest-to-furthest" + split-on-trailing-comma = false + section-order = ['__python__', "future", "standard-library", "third-party", "first-party", "local-folder"] +@@ -189,7 +188,6 @@ skip = [ + "./setup/linux-installer.py", + "./src/css_selectors/*", + "./src/polyglot/*", +- "./src/templite/*", + "./src/tinycss/*", + "./src/unicode_names/*", + ] +@@ -205,7 +203,6 @@ exclude = [ + "src/calibre/gui2/store/stores/", + "src/css_selectors/", + "src/polyglot/", +- "src/templite/", + "src/tinycss/", + ] + +diff --git a/resources/templates/html_export_default.mustache b/resources/templates/html_export_default.mustache +new file mode 100644 +index 0000000..1c8691a +--- /dev/null ++++ b/resources/templates/html_export_default.mustache +@@ -0,0 +1,70 @@ ++<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> ++<html xmlns="http://www.w3.org/1999/xhtml"> ++<head> ++{{{head_content}}} ++ ++<link href="{{css_link}}" type="text/css" rel="stylesheet" /> ++ ++</head> ++<body> ++ ++<div class="calibreMeta"> ++ <div class="calibreMetaTitle"> ++ {{#meta.titles}} ++ {{#is_first}} ++ <h1><a href="{{toc_url}}">{{title}}</a> </h1> ++ {{/is_first}} ++ {{^is_first}} ++ <div class="calibreMetaSubtitle">{{title}}</div> ++ {{/is_first}} ++ {{/meta.titles}} ++ </div> ++ <div class="calibreMetaAuthor">{{meta.creators}}</div> ++</div> ++ ++<div class="calibreMain"> ++ ++ <div class="calibreEbookContent"> ++ {{#has_link}} ++ <div class="calibreEbNavTop"> ++ {{#prev_link}} ++ <a href="{{prev_link}}" class="calibreAPrev">{{prev_page}}</a> ++ {{/prev_link}} ++ {{^prev_link}} ++ <a href="{{toc_url}}" class="calibreAPrev">{{prev_page}}</a> ++ {{/prev_link}} ++ {{#next_link}} ++ <a href="{{next_link}}" class="calibreANext">{{next_page}}</a> ++ {{/next_link}} ++ </div> ++ {{/has_link}} ++ ++ {{{ebook_content}}} ++ </div> ++ ++ {{#has_toc}} ++ <div class="calibreToc"> ++ <h2><a href="{{toc_url}}">{{table_of_contents}}</a></h2> ++ {{{toc}}} ++ </div> ++ {{/has_toc}} ++ ++ <div class="calibreEbNav"> ++ {{#prev_link}} ++ <a href="{{prev_link}}" class="calibreAPrev">{{prev_page}}</a> ++ {{/prev_link}} ++ {{^prev_link}} ++ <a href="{{toc_url}}" class="calibreAPrev">{{prev_page}}</a> ++ {{/prev_link}} ++ ++ <a href="{{toc_url}}" class="calibreAHome">{{start}}</a> ++ ++ {{#next_link}} ++ <a href="{{next_link}}" class="calibreANext">{{next_page}}</a> ++ {{/next_link}} ++ </div> ++ ++</div> ++ ++</body> ++</html> +diff --git a/resources/templates/html_export_default.tmpl b/resources/templates/html_export_default.tmpl +deleted file mode 100644 +index 7aac247..0000000 +--- a/resources/templates/html_export_default.tmpl ++++ /dev/null +@@ -1,74 +0,0 @@ +-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +-<html xmlns="http://www.w3.org/1999/xhtml"> +-<head> +-${head_content}$ +- +-<link href="${cssLink}$" type="text/css" rel="stylesheet" /> +- +-</head> +-<body> +- +-<div class="calibreMeta"> +- <div class="calibreMetaTitle"> +- ${pos1=1}$ +- ${for title in meta.titles():}$ +- ${if pos1:}$ +- <h1> +- <a href="${tocUrl}$">${print(title)}$</a> +- </h1> +- ${:else:}$ +- <div class="calibreMetaSubtitle">${print(title)}$</div> +- ${:endif}$ +- ${pos1=0}$ +- ${:endfor}$ +- </div> +- <div class="calibreMetaAuthor"> +- ${print(', '.join(meta.creators()))}$ +- </div> +-</div> +- +-<div class="calibreMain"> +- +- <div class="calibreEbookContent"> +- ${if prevLink or nextLink:}$ +- <div class="calibreEbNavTop"> +- ${if prevLink:}$ +- <a href="${prevLink}$" class="calibreAPrev">${print(_('previous page'))}$</a> +- ${:else:}$ +- <a href="${tocUrl}$" class="calibreAPrev">${print(_('previous page'))}$</a> +- ${:endif}$ +- +- ${if nextLink:}$ +- <a href="${nextLink}$" class="calibreANext">${print(_('next page'))}$</a> +- ${:endif}$ +- </div> +- ${:endif}$ +- +- ${ebookContent}$ +- </div> +- +- ${if has_toc:}$ +- <div class="calibreToc"> +- <h2><a href="${tocUrl}$">${print( _('Table of contents'))}$</a></h2> +- ${print(toc())}$ +- </div> +- ${:endif}$ +- +- <div class="calibreEbNav"> +- ${if prevLink:}$ +- <a href="${prevLink}$" class="calibreAPrev">${print(_('previous page'))}$</a> +- ${:else:}$ +- <a href="${tocUrl}$" class="calibreAPrev">${print(_('previous page'))}$</a> +- ${:endif}$ +- +- <a href="${tocUrl}$" class="calibreAHome">${print(_('start'))}$</a> +- +- ${if nextLink:}$ +- <a href="${nextLink}$" class="calibreANext">${print(_('next page'))}$</a> +- ${:endif}$ +- </div> +- +-</div> +- +-</body> +-</html> +diff --git a/resources/templates/html_export_default_index.mustache b/resources/templates/html_export_default_index.mustache +new file mode 100644 +index 0000000..aa1bc4d +--- /dev/null ++++ b/resources/templates/html_export_default_index.mustache +@@ -0,0 +1,55 @@ ++<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> ++<html xmlns="http://www.w3.org/1999/xhtml"> ++<head> ++<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> ++ ++<link rel="schema.DC" href="http://purl.org/dc/elements/1.1/" /> ++<link rel="schema.DCTERMS" href="http://purl.org/dc/terms/" /> ++ ++<title>{{meta.creators}} - {{meta.first_title}}</title> ++ ++{{#meta.items}} ++ <meta name="DC.{{name}}" content="{{value}}" /> ++{{/meta.items}} ++ ++<link href="{{css_link}}" type="text/css" rel="stylesheet" /> ++</head> ++<body> ++ ++<div class="calibreMeta"> ++ <div class="calibreMetaTitle"> ++ {{#meta.titles}} ++ {{#is_first}} ++ <h1><a href="{{toc_url}}">{{title}}</a> </h1> ++ {{/is_first}} ++ {{^is_first}} ++ <div class="calibreMetaSubtitle">{{title}}</div> ++ {{/is_first}} ++ {{/meta.titles}} ++ </div> ++ <div class="calibreMetaAuthor">{{meta.creators}}</div> ++</div> ++ ++<div class="calibreMain"> ++ <div class="calibreEbookContent"> ++ {{#has_toc}} ++ <div class="calibreTocIndex"> ++ <h2>{{table_of_contents}}</h2> ++ {{{toc}}} ++ </div> ++ {{/has_toc}} ++ {{^has_toc}} ++ <h2>{{no_toc}}</h2> ++ <div><strong><a href="{{next_link}}">{{begin_to_read}}</a></strong></div> ++ {{/has_toc}} ++ </div> ++ ++ <div class="calibreEbNav"> ++ {{#next_link}} ++ <a href="{{next_link}}" class="calibreANext">{{next_page}}</a> ++ {{/next_link}} ++ </div> ++</div> ++ ++</body> ++</html> +diff --git a/resources/templates/html_export_default_index.tmpl b/resources/templates/html_export_default_index.tmpl +deleted file mode 100644 +index f0665ad..0000000 +--- a/resources/templates/html_export_default_index.tmpl ++++ /dev/null +@@ -1,61 +0,0 @@ +-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +-<html xmlns="http://www.w3.org/1999/xhtml"> +-<head> +-<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +- +-<link rel="schema.DC" href="http://purl.org/dc/elements/1.1/" /> +-<link rel="schema.DCTERMS" href="http://purl.org/dc/terms/" /> +- +-<title>${print(', '.join(meta.creators()))}$ - ${print(next(meta.titles())); print(meta.titles().close())}$</title> +- +-${for item in meta:}$ +- <meta ${print('name="DC.'+item['name']+'"')}$ ${print('content="'+item['value']+'"')}$ /> +-${:endfor}$ +- +-<link href="${cssLink}$" type="text/css" rel="stylesheet" /> +-</head> +-<body> +- +-<div class="calibreMeta"> +- <div class="calibreMetaTitle"> +- ${pos1=1}$ +- ${for title in meta.titles():}$ +- ${if pos1:}$ +- <h1> +- <a href="${tocUrl}$">${print(title)}$</a> +- </h1> +- ${:else:}$ +- <div class="calibreMetaSubtitle">${print(title)}$</div> +- ${:endif}$ +- ${pos1=0}$ +- ${:endfor}$ +- </div> +- <div class="calibreMetaAuthor"> +- ${print(', '.join(meta.creators()))}$ +- </div> +-</div> +- +-<div class="calibreMain"> +- <div class="calibreEbookContent"> +- +- ${if has_toc:}$ +- <div class="calibreTocIndex"> +- <h2>${print(_('Table of contents'))}$</h2> +- ${toc}$ +- </div> +- ${:else:}$ +- <h2>${print(_('No table of contents present'))}$</h2> +- <div><strong><a href="${nextLink}$">${print(_('begin to read'))}$</a></strong></div> +- ${:endif}$ +- +- </div> +- +- <div class="calibreEbNav"> +- ${if nextLink:}$ +- <a href="${nextLink}$" class="calibreANext">${print(_('next page'))}$</a> +- ${:endif}$ +- </div> +-</div> +- +-</body> +-</html> +diff --git a/src/calibre/ebooks/conversion/plugins/html_output.py b/src/calibre/ebooks/conversion/plugins/html_output.py +index ea64c70..5573d04 100644 +--- a/src/calibre/ebooks/conversion/plugins/html_output.py ++++ b/src/calibre/ebooks/conversion/plugins/html_output.py +@@ -27,13 +27,13 @@ class HTMLOutput(OutputFormatPlugin): + + options = { + OptionRecommendation(name='template_css', +- help=_('CSS file used for the output instead of the default file')), ++ help=_('CSS file used for the output instead of the default CSS.')), + + OptionRecommendation(name='template_html_index', +- help=_('Template used for generation of the HTML index file instead of the default file')), ++ help=_('Template used for generation of the HTML index file instead of the default template. In Mustache format.')), + + OptionRecommendation(name='template_html', +- help=_('Template used for the generation of the HTML contents of the book instead of the default file')), ++ help=_('Template used for the generation of the HTML contents of the book instead of the default template. In Mustache format.')), + + OptionRecommendation(name='extract_to', + help=_('Extract the contents of the generated ZIP file to the ' +@@ -85,8 +85,8 @@ class HTMLOutput(OutputFormatPlugin): + xml_declaration=False) + + def convert(self, oeb_book, output_path, input_plugin, opts, log): ++ import pystache + from lxml import etree +- from templite import Templite + + from calibre.ebooks.html.meta import EasyMeta + from calibre.utils import zipfile +@@ -97,7 +97,7 @@ class HTMLOutput(OutputFormatPlugin): + with open(opts.template_html_index, 'rb') as f: + template_html_index_data = f.read() + else: +- template_html_index_data = P('templates/html_export_default_index.tmpl', data=True) ++ template_html_data = P('templates/html_export_default.mustache', data=True) + + if opts.template_html is not None: + with open(opts.template_html, 'rb') as f: +@@ -111,9 +111,10 @@ class HTMLOutput(OutputFormatPlugin): + else: + template_css_data = P('templates/html_export_default.css', data=True) + +- template_html_index_data = template_html_index_data.decode('utf-8') +- template_html_data = template_html_data.decode('utf-8') ++ template_html_index = pystache.parse(template_html_index_data.decode('utf-8')) ++ template_html = pystache.parse(template_html_data.decode('utf-8')) + template_css_data = template_css_data.decode('utf-8') ++ has_toc = bool(oeb_book.toc.count()) + + self.log = log + self.opts = opts +@@ -130,18 +131,31 @@ class HTMLOutput(OutputFormatPlugin): + css_path = output_dir+os.sep+'calibreHtmlOutBasicCss.css' + with open(css_path, 'wb') as f: + f.write(template_css_data.encode('utf-8')) ++ meta_dict = { ++ 'titles': [{'title': x, 'is_first': i == 0} for i, x in enumerate(meta.titles())], ++ 'creators': authors_to_string(tuple(meta.creators())), ++ 'items': list(meta), ++ } ++ meta_dict['first_title'] = meta_dict['titles'][0]['title'] if meta_dict['titles'] else '' ++ basic_template_vars = { ++ 'meta': meta_dict, 'has_toc': has_toc, ++ 'table_of_contents': _('Table of contents'), 'no_toc': _('No table of contents present'), ++ 'begin_to_read': _('begin to read'), 'start': _('start'), ++ 'prev_page': _('previous page'), 'next_page': _('next page'), ++ } + + with open(output_file, 'wb') as f: +- html_toc = self.generate_html_toc(oeb_book, output_file, output_dir) +- templite = Templite(template_html_index_data) + nextLink = oeb_book.spine[0].href + nextLink = relpath(output_dir+os.sep+nextLink, dirname(output_file)) + cssLink = relpath(abspath(css_path), dirname(output_file)) + tocUrl = relpath(output_file, dirname(output_file)) +- t = templite.render(has_toc=bool(oeb_book.toc.count()), +- toc=html_toc, meta=meta, nextLink=nextLink, +- tocUrl=tocUrl, cssLink=cssLink, +- firstContentPageLink=nextLink) ++ toc_as_html = self.generate_html_toc(oeb_book, output_file, output_dir) if has_toc else '' ++ v = basic_template_vars.copy() ++ v.update({ ++ 'toc': toc_as_html, 'css_link': cssLink, 'toc_url': tocUrl, 'next_link': nextLink, ++ 'first_content_page_link': nextLink, ++ }) ++ t = pystache.render(template_html_index, v) + if isinstance(t, str): + t = t.encode('utf-8') + f.write(t) +@@ -197,17 +211,18 @@ class HTMLOutput(OutputFormatPlugin): + firstContentPageLink = oeb_book.spine[0].href + + # render template +- templite = Templite(template_html_data) +- + def toc(): +- return self.generate_html_toc(oeb_book, path, output_dir) +- t = templite.render(ebookContent=ebook_content, +- prevLink=prevLink, nextLink=nextLink, +- has_toc=bool(oeb_book.toc.count()), toc=toc, +- tocUrl=tocUrl, head_content=head_content, +- meta=meta, cssLink=cssLink, +- firstContentPageLink=firstContentPageLink) +- ++ return ++ toc_as_html = self.generate_html_toc(oeb_book, path, output_dir) if has_toc else '' ++ v = basic_template_vars.copy() ++ v.update({ ++ 'has_link': prevLink or nextLink, ++ 'prev_link': prevLink, 'next_link': nextLink, 'toc_url': tocUrl, ++ 'head_content': head_content, 'ebook_content': ebook_content, ++ 'css_link': cssLink, 'toc': toc_as_html, ++ 'first_content_page_link': firstContentPageLink, ++ }) ++ t = pystache.render(template_html, v) + # write html to file + with open(path, 'wb') as f: + f.write(t.encode('utf-8')) +diff --git a/src/templite/__init__.py b/src/templite/__init__.py +deleted file mode 100644 +index 8723d0d..0000000 +--- a/src/templite/__init__.py ++++ /dev/null +@@ -1,96 +0,0 @@ +-#!/usr/bin/env python +-# +-# Templite+ +-# A light-weight, fully functional, general purpose templating engine +-# +-# Copyright (c) 2009 joonis new media +-# Author: Thimo Kraemer <[email protected]> +-# +-# Based on Templite - Tomer Filiba +-# http://code.activestate.com/recipes/496702/ +-# +-# This program is free software; you can redistribute it and/or modify +-# it under the terms of the GNU General Public License as published by +-# the Free Software Foundation; either version 2 of the License, or +-# (at your option) any later version. +-# +-# This program is distributed in the hope that it will be useful, +-# but WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-# GNU General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +-# MA 02110-1301, USA. +-# +- +-import re +-import sys +- +-from polyglot.builtins import unicode_type +- +- +-class Templite: +- auto_emit = re.compile(r'''(^['"])|(^[a-zA-Z0-9_[\]'"]+$)''') +- +- def __init__(self, template, start='${', end='}$'): +- if len(start) != 2 or len(end) != 2: +- raise ValueError('each delimiter must be two characters long') +- delimiter = re.compile('%s(.*?)%s' % (re.escape(start), re.escape(end)), re.DOTALL) +- offset = 0 +- tokens = [] +- for i, part in enumerate(delimiter.split(template)): +- part = part.replace('\\'.join(list(start)), start) +- part = part.replace('\\'.join(list(end)), end) +- if i % 2 == 0: +- if not part: +- continue +- part = part.replace('\\', '\\\\').replace('"', '\\"') +- part = '\t' * offset + 'emit("""%s""")' % part +- else: +- part = part.rstrip() +- if not part: +- continue +- if part.lstrip().startswith(':'): +- if not offset: +- raise SyntaxError('no block statement to terminate: ${%s}$' % part) +- offset -= 1 +- part = part.lstrip()[1:] +- if not part.endswith(':'): +- continue +- elif self.auto_emit.match(part.lstrip()): +- part = 'emit(%s)' % part.lstrip() +- lines = part.splitlines() +- margin = min(len(l) - len(l.lstrip()) for l in lines if l.strip()) +- part = '\n'.join('\t' * offset + l[margin:] for l in lines) +- if part.endswith(':'): +- offset += 1 +- tokens.append(part) +- if offset: +- raise SyntaxError('%i block statement(s) not terminated' % offset) +- self.__code = compile('\n'.join(tokens), '<templite %r>' % template[:20], 'exec') +- +- def render(self, __namespace=None, **kw): +- """ +- renders the template according to the given namespace. +- __namespace - a dictionary serving as a namespace for evaluation +- **kw - keyword arguments which are added to the namespace +- """ +- namespace = {} +- if __namespace: +- namespace.update(__namespace) +- if kw: +- namespace.update(kw) +- namespace['emit'] = self.write +- +- __stdout = sys.stdout +- sys.stdout = self +- self.__output = [] +- eval(self.__code, namespace) +- sys.stdout = __stdout +- return ''.join(self.__output) +- +- def write(self, *args): +- for a in args: +- self.__output.append(unicode_type(a)) diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0086-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch calibre-8.5.0+ds/debian/patches/upstream/0086-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch --- calibre-8.5.0+ds/debian/patches/upstream/0086-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch 2026-05-25 08:19:53.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0086-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch 1970-01-01 09:00:00.000000000 +0900 @@ -1,36 +0,0 @@ -From: Kovid Goyal <[email protected]> -Date: Thu, 12 Feb 2026 10:15:57 +0530 -Subject: CVE-2026-26064: ODT Input: Ensure images are extracted within - container - -Forwarded: not-needed -Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-72ch-3hqc-pgmp -Origin: https://github.com/kovidgoyal/calibre/commit/e1b5f9b45a5e8fa96c136963ad9a1d35e6adac62 - -Signed-off-by: YOKOTA Hiroshi <[email protected]> ---- - src/calibre/ebooks/odt/input.py | 8 +++++++- - 1 file changed, 7 insertions(+), 1 deletion(-) - -diff --git a/src/calibre/ebooks/odt/input.py b/src/calibre/ebooks/odt/input.py -index d2da2e1..0daa298 100644 ---- a/src/calibre/ebooks/odt/input.py -+++ b/src/calibre/ebooks/odt/input.py -@@ -28,10 +28,16 @@ class Extract(ODF2XHTML): - def extract_pictures(self, zf): - if not os.path.exists('Pictures'): - os.makedirs('Pictures') -+ base = os.path.abspath(os.getcwd()) -+ if not base.endswith(os.sep): -+ base += os.sep - for name in zf.namelist(): - if name.startswith('Pictures') and name not in {'Pictures', 'Pictures/'}: -+ dest = os.path.abspath(os.path.join(base, name)) -+ if os.path.commonprefix([base, dest]) != dest: -+ continue - data = zf.read(name) -- with open(name, 'wb') as f: -+ with open(dest, 'wb') as f: - f.write(data) - - def apply_list_starts(self, root, log): diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0087-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch calibre-8.5.0+ds/debian/patches/upstream/0087-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch --- calibre-8.5.0+ds/debian/patches/upstream/0087-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch 1970-01-01 09:00:00.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0087-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch 2026-08-04 16:13:08.000000000 +0900 @@ -0,0 +1,36 @@ +From: Kovid Goyal <[email protected]> +Date: Thu, 12 Feb 2026 10:15:57 +0530 +Subject: CVE-2026-26064: ODT Input: Ensure images are extracted within + container + +Forwarded: not-needed +Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-72ch-3hqc-pgmp +Origin: https://github.com/kovidgoyal/calibre/commit/e1b5f9b45a5e8fa96c136963ad9a1d35e6adac62 + +Signed-off-by: YOKOTA Hiroshi <[email protected]> +--- + src/calibre/ebooks/odt/input.py | 8 +++++++- + 1 file changed, 7 insertions(+), 1 deletion(-) + +diff --git a/src/calibre/ebooks/odt/input.py b/src/calibre/ebooks/odt/input.py +index d2da2e1..0daa298 100644 +--- a/src/calibre/ebooks/odt/input.py ++++ b/src/calibre/ebooks/odt/input.py +@@ -28,10 +28,16 @@ class Extract(ODF2XHTML): + def extract_pictures(self, zf): + if not os.path.exists('Pictures'): + os.makedirs('Pictures') ++ base = os.path.abspath(os.getcwd()) ++ if not base.endswith(os.sep): ++ base += os.sep + for name in zf.namelist(): + if name.startswith('Pictures') and name not in {'Pictures', 'Pictures/'}: ++ dest = os.path.abspath(os.path.join(base, name)) ++ if os.path.commonprefix([base, dest]) != dest: ++ continue + data = zf.read(name) +- with open(name, 'wb') as f: ++ with open(dest, 'wb') as f: + f.write(data) + + def apply_list_starts(self, root, log): diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0087-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch calibre-8.5.0+ds/debian/patches/upstream/0087-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch --- calibre-8.5.0+ds/debian/patches/upstream/0087-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch 2026-05-25 08:19:53.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0087-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch 1970-01-01 09:00:00.000000000 +0900 @@ -1,120 +0,0 @@ -From: Kovid Goyal <[email protected]> -Date: Thu, 12 Feb 2026 10:31:53 +0530 -Subject: CVE-2026-26065: PDB Input: Ensure extracted images are within the - container - -Forwarded: not-needed -Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-vmfh-7mr7-pp2w -Origin: https://github.com/kovidgoyal/calibre/commit/b6da1c3878c06eb1356cb0ec1106cb66e0e9bfb8 - -Signed-off-by: YOKOTA Hiroshi <[email protected]> ---- - src/calibre/ebooks/pdb/ereader/reader132.py | 27 +++++++++++++++++++-------- - src/calibre/ebooks/pdb/ereader/reader202.py | 25 ++++++++++++++++++------- - 2 files changed, 37 insertions(+), 15 deletions(-) - -diff --git a/src/calibre/ebooks/pdb/ereader/reader132.py b/src/calibre/ebooks/pdb/ereader/reader132.py -index a92fa3d..f8c9284 100644 ---- a/src/calibre/ebooks/pdb/ereader/reader132.py -+++ b/src/calibre/ebooks/pdb/ereader/reader132.py -@@ -90,6 +90,15 @@ class Reader132(FormatReader): - img = data[62:] - return name, img - -+ def image_dest(self, name, cwd): -+ base = os.path.abspath(cwd) -+ if not base.endswith(os.sep): -+ base += os.sep -+ ans = os.path.abspath(os.path.join(base, name)) -+ if os.path.commonprefix([ans, base]) != base: -+ ans = '' -+ return ans -+ - def get_text_page(self, number): - ''' - Only palmdoc and zlib compressed are supported. The text is -@@ -156,13 +165,14 @@ class Reader132(FormatReader): - if not os.path.exists(os.path.join(output_dir, 'images/')): - os.makedirs(os.path.join(output_dir, 'images/')) - images = [] -- with CurrentDir(os.path.join(output_dir, 'images/')): -+ with CurrentDir(os.path.join(output_dir, 'images/')) as cwd: - for i in range(self.header_record.num_image_pages): - name, img = self.get_image(self.header_record.image_data_offset + i) -- images.append(name) -- with open(name, 'wb') as imgf: -- self.log.debug(f'Writing image {name} to images/') -- imgf.write(img) -+ if dest := self.image_dest(name, cwd): -+ images.append(name) -+ with open(dest, 'wb') as imgf: -+ self.log.debug(f'Writing image {name} to images/') -+ imgf.write(img) - - opf_path = self.create_opf(output_dir, images, toc) - -@@ -209,8 +219,9 @@ class Reader132(FormatReader): - if not os.path.exists(output_dir): - os.makedirs(output_dir) - -- with CurrentDir(output_dir): -+ with CurrentDir(output_dir) as cwd: - for i in range(self.header_record.num_image_pages): - name, img = self.get_image(self.header_record.image_data_offset + i) -- with open(name, 'wb') as imgf: -- imgf.write(img) -+ if dest := self.image_dest(name, cwd): -+ with open(dest, 'wb') as imgf: -+ imgf.write(img) -diff --git a/src/calibre/ebooks/pdb/ereader/reader202.py b/src/calibre/ebooks/pdb/ereader/reader202.py -index 259bfe0..d233072 100644 ---- a/src/calibre/ebooks/pdb/ereader/reader202.py -+++ b/src/calibre/ebooks/pdb/ereader/reader202.py -@@ -108,13 +108,13 @@ class Reader202(FormatReader): - if not os.path.exists(os.path.join(output_dir, 'images/')): - os.makedirs(os.path.join(output_dir, 'images/')) - images = [] -- with CurrentDir(os.path.join(output_dir, 'images/')): -+ with CurrentDir(os.path.join(output_dir, 'images/')) as cwd: - for i in range(self.header_record.non_text_offset, len(self.sections)): - name, img = self.get_image(i) -- if name: -- name = as_unicode(name) -+ name = as_unicode(name or b'') -+ if name and (dest := self.image_dest(name, cwd)): - images.append(name) -- with open(name, 'wb') as imgf: -+ with open(dest, 'wb') as imgf: - self.log.debug(f'Writing image {name} to images/') - imgf.write(img) - -@@ -150,6 +150,15 @@ class Reader202(FormatReader): - - return pml - -+ def image_dest(self, name, cwd): -+ base = os.path.abspath(cwd) -+ if not base.endswith(os.sep): -+ base += os.sep -+ ans = os.path.abspath(os.path.join(base, name)) -+ if os.path.commonprefix([ans, base]) != base: -+ ans = '' -+ return ans -+ - def dump_images(self, output_dir): - ''' - This is primarily used for debugging and 3rd party tools to -@@ -158,8 +167,10 @@ class Reader202(FormatReader): - if not os.path.exists(output_dir): - os.makedirs(output_dir) - -- with CurrentDir(output_dir): -+ with CurrentDir(output_dir) as cwd: - for i in range(self.header_record.num_image_pages): - name, img = self.get_image(self.header_record.image_data_offset + i) -- with open(name, 'wb') as imgf: -- imgf.write(img) -+ name = as_unicode(name or b'') -+ if name and (dest := self.image_dest(name, cwd)): -+ with open(dest, 'wb') as imgf: -+ imgf.write(img) diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0088-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch calibre-8.5.0+ds/debian/patches/upstream/0088-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch --- calibre-8.5.0+ds/debian/patches/upstream/0088-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch 1970-01-01 09:00:00.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0088-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch 2026-08-04 16:13:08.000000000 +0900 @@ -0,0 +1,120 @@ +From: Kovid Goyal <[email protected]> +Date: Thu, 12 Feb 2026 10:31:53 +0530 +Subject: CVE-2026-26065: PDB Input: Ensure extracted images are within the + container + +Forwarded: not-needed +Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-vmfh-7mr7-pp2w +Origin: https://github.com/kovidgoyal/calibre/commit/b6da1c3878c06eb1356cb0ec1106cb66e0e9bfb8 + +Signed-off-by: YOKOTA Hiroshi <[email protected]> +--- + src/calibre/ebooks/pdb/ereader/reader132.py | 27 +++++++++++++++++++-------- + src/calibre/ebooks/pdb/ereader/reader202.py | 25 ++++++++++++++++++------- + 2 files changed, 37 insertions(+), 15 deletions(-) + +diff --git a/src/calibre/ebooks/pdb/ereader/reader132.py b/src/calibre/ebooks/pdb/ereader/reader132.py +index a92fa3d..f8c9284 100644 +--- a/src/calibre/ebooks/pdb/ereader/reader132.py ++++ b/src/calibre/ebooks/pdb/ereader/reader132.py +@@ -90,6 +90,15 @@ class Reader132(FormatReader): + img = data[62:] + return name, img + ++ def image_dest(self, name, cwd): ++ base = os.path.abspath(cwd) ++ if not base.endswith(os.sep): ++ base += os.sep ++ ans = os.path.abspath(os.path.join(base, name)) ++ if os.path.commonprefix([ans, base]) != base: ++ ans = '' ++ return ans ++ + def get_text_page(self, number): + ''' + Only palmdoc and zlib compressed are supported. The text is +@@ -156,13 +165,14 @@ class Reader132(FormatReader): + if not os.path.exists(os.path.join(output_dir, 'images/')): + os.makedirs(os.path.join(output_dir, 'images/')) + images = [] +- with CurrentDir(os.path.join(output_dir, 'images/')): ++ with CurrentDir(os.path.join(output_dir, 'images/')) as cwd: + for i in range(self.header_record.num_image_pages): + name, img = self.get_image(self.header_record.image_data_offset + i) +- images.append(name) +- with open(name, 'wb') as imgf: +- self.log.debug(f'Writing image {name} to images/') +- imgf.write(img) ++ if dest := self.image_dest(name, cwd): ++ images.append(name) ++ with open(dest, 'wb') as imgf: ++ self.log.debug(f'Writing image {name} to images/') ++ imgf.write(img) + + opf_path = self.create_opf(output_dir, images, toc) + +@@ -209,8 +219,9 @@ class Reader132(FormatReader): + if not os.path.exists(output_dir): + os.makedirs(output_dir) + +- with CurrentDir(output_dir): ++ with CurrentDir(output_dir) as cwd: + for i in range(self.header_record.num_image_pages): + name, img = self.get_image(self.header_record.image_data_offset + i) +- with open(name, 'wb') as imgf: +- imgf.write(img) ++ if dest := self.image_dest(name, cwd): ++ with open(dest, 'wb') as imgf: ++ imgf.write(img) +diff --git a/src/calibre/ebooks/pdb/ereader/reader202.py b/src/calibre/ebooks/pdb/ereader/reader202.py +index 259bfe0..d233072 100644 +--- a/src/calibre/ebooks/pdb/ereader/reader202.py ++++ b/src/calibre/ebooks/pdb/ereader/reader202.py +@@ -108,13 +108,13 @@ class Reader202(FormatReader): + if not os.path.exists(os.path.join(output_dir, 'images/')): + os.makedirs(os.path.join(output_dir, 'images/')) + images = [] +- with CurrentDir(os.path.join(output_dir, 'images/')): ++ with CurrentDir(os.path.join(output_dir, 'images/')) as cwd: + for i in range(self.header_record.non_text_offset, len(self.sections)): + name, img = self.get_image(i) +- if name: +- name = as_unicode(name) ++ name = as_unicode(name or b'') ++ if name and (dest := self.image_dest(name, cwd)): + images.append(name) +- with open(name, 'wb') as imgf: ++ with open(dest, 'wb') as imgf: + self.log.debug(f'Writing image {name} to images/') + imgf.write(img) + +@@ -150,6 +150,15 @@ class Reader202(FormatReader): + + return pml + ++ def image_dest(self, name, cwd): ++ base = os.path.abspath(cwd) ++ if not base.endswith(os.sep): ++ base += os.sep ++ ans = os.path.abspath(os.path.join(base, name)) ++ if os.path.commonprefix([ans, base]) != base: ++ ans = '' ++ return ans ++ + def dump_images(self, output_dir): + ''' + This is primarily used for debugging and 3rd party tools to +@@ -158,8 +167,10 @@ class Reader202(FormatReader): + if not os.path.exists(output_dir): + os.makedirs(output_dir) + +- with CurrentDir(output_dir): ++ with CurrentDir(output_dir) as cwd: + for i in range(self.header_record.num_image_pages): + name, img = self.get_image(self.header_record.image_data_offset + i) +- with open(name, 'wb') as imgf: +- imgf.write(img) ++ name = as_unicode(name or b'') ++ if name and (dest := self.image_dest(name, cwd)): ++ with open(dest, 'wb') as imgf: ++ imgf.write(img) diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0088-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch calibre-8.5.0+ds/debian/patches/upstream/0088-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch --- calibre-8.5.0+ds/debian/patches/upstream/0088-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch 2026-05-25 08:19:53.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0088-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch 1970-01-01 09:00:00.000000000 +0900 @@ -1,41 +0,0 @@ -From: Kovid Goyal <[email protected]> -Date: Tue, 24 Feb 2026 09:06:11 +0530 -Subject: CVE-2026-27810: Content server: Sanitize content disposition - received as query parameter - -Forwarded: not-needed -Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-5fpj-fxw7-8grw -Origin: https://github.com/kovidgoyal/calibre/commit/a468ce0f268032eea1f7431853248148ffa2e06a - -Signed-off-by: YOKOTA Hiroshi <[email protected]> ---- - src/calibre/srv/content.py | 8 ++++++-- - 1 file changed, 6 insertions(+), 2 deletions(-) - -diff --git a/src/calibre/srv/content.py b/src/calibre/srv/content.py -index a7e4e89..0094580 100644 ---- a/src/calibre/srv/content.py -+++ b/src/calibre/srv/content.py -@@ -221,7 +221,7 @@ def book_fmt(ctx, rd, library_id, db, book_id, fmt): - set_metadata(dest, mi, fmt) - dest.seek(0) - -- cd = rd.query.get('content_disposition', 'attachment') -+ cd = sanitize_content_disposition(rd.query.get('content_disposition', 'attachment')) - rd.outheaders['Content-Disposition'] = ( - f'''{cd}; filename="{book_filename(rd, book_id, mi, fmt)}"; filename*=utf-8''{book_filename(rd, book_id, mi, fmt, as_encoded_unicode=True)}''') - -@@ -522,8 +522,12 @@ def set_note(ctx, rd, field, item_id, library_id): - return srv_html - - -+def sanitize_content_disposition(x: str) -> str: -+ return re.sub(r'[^a-zA-Z0-9./-]', '-', x) -+ -+ - def data_file(rd, fname, path, stat_result): -- cd = rd.query.get('content_disposition', 'attachment') -+ cd = sanitize_content_disposition(rd.query.get('content_disposition', 'attachment')) - rd.outheaders['Content-Disposition'] = ( - f'''{cd}; filename="{fname_for_content_disposition(fname)}"; filename*=utf-8''{fname_for_content_disposition(fname, as_encoded_unicode=True)}''') - return rd.filesystem_file_with_custom_etag(share_open(path, 'rb'), stat_result.st_dev, stat_result.st_ino, stat_result.st_size, stat_result.st_mtime) diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0089-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch calibre-8.5.0+ds/debian/patches/upstream/0089-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch --- calibre-8.5.0+ds/debian/patches/upstream/0089-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch 1970-01-01 09:00:00.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0089-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch 2026-08-04 16:13:08.000000000 +0900 @@ -0,0 +1,41 @@ +From: Kovid Goyal <[email protected]> +Date: Tue, 24 Feb 2026 09:06:11 +0530 +Subject: CVE-2026-27810: Content server: Sanitize content disposition + received as query parameter + +Forwarded: not-needed +Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-5fpj-fxw7-8grw +Origin: https://github.com/kovidgoyal/calibre/commit/a468ce0f268032eea1f7431853248148ffa2e06a + +Signed-off-by: YOKOTA Hiroshi <[email protected]> +--- + src/calibre/srv/content.py | 8 ++++++-- + 1 file changed, 6 insertions(+), 2 deletions(-) + +diff --git a/src/calibre/srv/content.py b/src/calibre/srv/content.py +index a7e4e89..0094580 100644 +--- a/src/calibre/srv/content.py ++++ b/src/calibre/srv/content.py +@@ -221,7 +221,7 @@ def book_fmt(ctx, rd, library_id, db, book_id, fmt): + set_metadata(dest, mi, fmt) + dest.seek(0) + +- cd = rd.query.get('content_disposition', 'attachment') ++ cd = sanitize_content_disposition(rd.query.get('content_disposition', 'attachment')) + rd.outheaders['Content-Disposition'] = ( + f'''{cd}; filename="{book_filename(rd, book_id, mi, fmt)}"; filename*=utf-8''{book_filename(rd, book_id, mi, fmt, as_encoded_unicode=True)}''') + +@@ -522,8 +522,12 @@ def set_note(ctx, rd, field, item_id, library_id): + return srv_html + + ++def sanitize_content_disposition(x: str) -> str: ++ return re.sub(r'[^a-zA-Z0-9./-]', '-', x) ++ ++ + def data_file(rd, fname, path, stat_result): +- cd = rd.query.get('content_disposition', 'attachment') ++ cd = sanitize_content_disposition(rd.query.get('content_disposition', 'attachment')) + rd.outheaders['Content-Disposition'] = ( + f'''{cd}; filename="{fname_for_content_disposition(fname)}"; filename*=utf-8''{fname_for_content_disposition(fname, as_encoded_unicode=True)}''') + return rd.filesystem_file_with_custom_etag(share_open(path, 'rb'), stat_result.st_dev, stat_result.st_ino, stat_result.st_size, stat_result.st_mtime) diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0089-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch calibre-8.5.0+ds/debian/patches/upstream/0089-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch --- calibre-8.5.0+ds/debian/patches/upstream/0089-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch 2026-05-25 08:19:53.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0089-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch 1970-01-01 09:00:00.000000000 +0900 @@ -1,34 +0,0 @@ -From: Kovid Goyal <[email protected]> -Date: Tue, 24 Feb 2026 16:53:46 +0530 -Subject: CVE-2026-27824: Content server: When banning IPs for repeated login - is enabled, only use the IP address not any HTTP headers as the ban key - -Forwarded: not-needed -Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-vhxc-r7v8-2xrw -Origin: https://github.com/kovidgoyal/calibre/commit/2f273444460d06f72f7a8f390f5f9ff325d1f836 - -This means banning is ineffective behind a proxy, but cant be helped -since we have no way of authenticating that a header comes from the -proxy. - -Signed-off-by: YOKOTA Hiroshi <[email protected]> ---- - src/calibre/srv/auth.py | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) - -diff --git a/src/calibre/srv/auth.py b/src/calibre/srv/auth.py -index a5b5a0c..e2a4e64 100644 ---- a/src/calibre/srv/auth.py -+++ b/src/calibre/srv/auth.py -@@ -269,9 +269,9 @@ class AuthController: - return cookie and validate_nonce(self.key_order, cookie, path, self.secret) and not is_nonce_stale(cookie, self.max_age_seconds) - - def do_http_auth(self, data, endpoint): -- ban_key = data.remote_addr, data.forwarded_for -+ ban_key = data.remote_addr - if self.ban_list.is_banned(ban_key): -- raise HTTPForbidden('Too many login attempts', log=f'Too many login attempts from: {ban_key if data.forwarded_for else data.remote_addr}') -+ raise HTTPForbidden('Too many login attempts', log=f'Too many login attempts from: {data.remote_addr}') - auth = data.inheaders.get('Authorization') - nonce_is_stale = False - log_msg = None diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0090-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch calibre-8.5.0+ds/debian/patches/upstream/0090-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch --- calibre-8.5.0+ds/debian/patches/upstream/0090-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch 1970-01-01 09:00:00.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0090-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch 2026-08-04 16:13:08.000000000 +0900 @@ -0,0 +1,34 @@ +From: Kovid Goyal <[email protected]> +Date: Tue, 24 Feb 2026 16:53:46 +0530 +Subject: CVE-2026-27824: Content server: When banning IPs for repeated login + is enabled, only use the IP address not any HTTP headers as the ban key + +Forwarded: not-needed +Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-vhxc-r7v8-2xrw +Origin: https://github.com/kovidgoyal/calibre/commit/2f273444460d06f72f7a8f390f5f9ff325d1f836 + +This means banning is ineffective behind a proxy, but cant be helped +since we have no way of authenticating that a header comes from the +proxy. + +Signed-off-by: YOKOTA Hiroshi <[email protected]> +--- + src/calibre/srv/auth.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/src/calibre/srv/auth.py b/src/calibre/srv/auth.py +index a5b5a0c..e2a4e64 100644 +--- a/src/calibre/srv/auth.py ++++ b/src/calibre/srv/auth.py +@@ -269,9 +269,9 @@ class AuthController: + return cookie and validate_nonce(self.key_order, cookie, path, self.secret) and not is_nonce_stale(cookie, self.max_age_seconds) + + def do_http_auth(self, data, endpoint): +- ban_key = data.remote_addr, data.forwarded_for ++ ban_key = data.remote_addr + if self.ban_list.is_banned(ban_key): +- raise HTTPForbidden('Too many login attempts', log=f'Too many login attempts from: {ban_key if data.forwarded_for else data.remote_addr}') ++ raise HTTPForbidden('Too many login attempts', log=f'Too many login attempts from: {data.remote_addr}') + auth = data.inheaders.get('Authorization') + nonce_is_stale = False + log_msg = None 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 2026-05-25 08:19:53.000000000 +0900 +++ 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 @@ -1,163 +0,0 @@ -From: ECB <[email protected]> -Date: Tue, 21 Apr 2026 12:48:42 +0200 -Subject: Fix security vulnerabilities and code quality issues (Closes: - #1135543) - -Forwarded: not-needed -Bug: https://github.com/kovidgoyal/calibre/pull/3101 -Origin: backport, 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 diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0091-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch calibre-8.5.0+ds/debian/patches/upstream/0091-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch --- calibre-8.5.0+ds/debian/patches/upstream/0091-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch 2026-05-25 08:19:53.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0091-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch 1970-01-01 09:00:00.000000000 +0900 @@ -1,58 +0,0 @@ -From: Kovid Goyal <[email protected]> -Date: Fri, 6 Mar 2026 07:39:44 +0530 -Subject: CVE-2026-30853: RB Input: Ensure files are extracted within - container dir - -Forwarded: not-needed -Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-7mp7-rfrg-542x -Origin: https://github.com/kovidgoyal/calibre/commit/0f8dc639337d9ace67201e15ca12d5906d05f4c8 - -Signed-off-by: YOKOTA Hiroshi <[email protected]> ---- - src/calibre/ebooks/rb/reader.py | 19 +++++++++++++++---- - 1 file changed, 15 insertions(+), 4 deletions(-) - -diff --git a/src/calibre/ebooks/rb/reader.py b/src/calibre/ebooks/rb/reader.py -index c1f77dd..b13066a 100644 ---- a/src/calibre/ebooks/rb/reader.py -+++ b/src/calibre/ebooks/rb/reader.py -@@ -67,6 +67,15 @@ class Reader: - - return toc - -+ def get_safe_path(self, output_dir, name): -+ base = os.path.abspath(output_dir) -+ if not base.endswith(os.sep): -+ base += os.sep -+ ans = os.path.abspath(os.path.join(base, name)) -+ if os.path.commonprefix([ans, base]) != base: -+ ans = '' -+ return ans -+ - def get_text(self, toc_item, output_dir): - if toc_item.flags in (1, 2): - return -@@ -87,8 +96,9 @@ class Reader: - else: - output += self.stream.read(toc_item.size).decode('cp1252' if self.encoding is None else self.encoding, 'replace') - -- with open(os.path.join(output_dir, toc_item.name.decode('utf-8')), 'wb') as html: -- html.write(output.replace('<TITLE>', '<TITLE> ').encode('utf-8')) -+ if path := self.get_safe_path(output_dir, toc_item.name.decode('utf-8')): -+ with open(path, 'wb') as html: -+ html.write(output.replace('<TITLE>', '<TITLE> ').encode('utf-8')) - - def get_image(self, toc_item, output_dir): - if toc_item.flags != 0: -@@ -97,8 +107,9 @@ class Reader: - self.stream.seek(toc_item.offset) - data = self.stream.read(toc_item.size) - -- with open(os.path.join(output_dir, toc_item.name.decode('utf-8')), 'wb') as img: -- img.write(data) -+ if path := self.get_safe_path(output_dir, toc_item.name.decode('utf-8')): -+ with open(path, 'wb') as img: -+ img.write(data) - - def extract_content(self, output_dir): - self.log.debug('Extracting content from file...') diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0091-Fix-security-vulnerabilities-and-code-quality-issues.patch calibre-8.5.0+ds/debian/patches/upstream/0091-Fix-security-vulnerabilities-and-code-quality-issues.patch --- calibre-8.5.0+ds/debian/patches/upstream/0091-Fix-security-vulnerabilities-and-code-quality-issues.patch 1970-01-01 09:00:00.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0091-Fix-security-vulnerabilities-and-code-quality-issues.patch 2026-08-04 16:13:08.000000000 +0900 @@ -0,0 +1,163 @@ +From: ECB <[email protected]> +Date: Tue, 21 Apr 2026 12:48:42 +0200 +Subject: Fix security vulnerabilities and code quality issues (Closes: + #1135543) + +Forwarded: not-needed +Bug: https://github.com/kovidgoyal/calibre/pull/3101 +Origin: backport, 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 diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0092-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch calibre-8.5.0+ds/debian/patches/upstream/0092-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch --- calibre-8.5.0+ds/debian/patches/upstream/0092-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch 1970-01-01 09:00:00.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0092-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch 2026-08-04 16:13:08.000000000 +0900 @@ -0,0 +1,58 @@ +From: Kovid Goyal <[email protected]> +Date: Fri, 6 Mar 2026 07:39:44 +0530 +Subject: CVE-2026-30853: RB Input: Ensure files are extracted within + container dir + +Forwarded: not-needed +Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-7mp7-rfrg-542x +Origin: https://github.com/kovidgoyal/calibre/commit/0f8dc639337d9ace67201e15ca12d5906d05f4c8 + +Signed-off-by: YOKOTA Hiroshi <[email protected]> +--- + src/calibre/ebooks/rb/reader.py | 19 +++++++++++++++---- + 1 file changed, 15 insertions(+), 4 deletions(-) + +diff --git a/src/calibre/ebooks/rb/reader.py b/src/calibre/ebooks/rb/reader.py +index c1f77dd..b13066a 100644 +--- a/src/calibre/ebooks/rb/reader.py ++++ b/src/calibre/ebooks/rb/reader.py +@@ -67,6 +67,15 @@ class Reader: + + return toc + ++ def get_safe_path(self, output_dir, name): ++ base = os.path.abspath(output_dir) ++ if not base.endswith(os.sep): ++ base += os.sep ++ ans = os.path.abspath(os.path.join(base, name)) ++ if os.path.commonprefix([ans, base]) != base: ++ ans = '' ++ return ans ++ + def get_text(self, toc_item, output_dir): + if toc_item.flags in (1, 2): + return +@@ -87,8 +96,9 @@ class Reader: + else: + output += self.stream.read(toc_item.size).decode('cp1252' if self.encoding is None else self.encoding, 'replace') + +- with open(os.path.join(output_dir, toc_item.name.decode('utf-8')), 'wb') as html: +- html.write(output.replace('<TITLE>', '<TITLE> ').encode('utf-8')) ++ if path := self.get_safe_path(output_dir, toc_item.name.decode('utf-8')): ++ with open(path, 'wb') as html: ++ html.write(output.replace('<TITLE>', '<TITLE> ').encode('utf-8')) + + def get_image(self, toc_item, output_dir): + if toc_item.flags != 0: +@@ -97,8 +107,9 @@ class Reader: + self.stream.seek(toc_item.offset) + data = self.stream.read(toc_item.size) + +- with open(os.path.join(output_dir, toc_item.name.decode('utf-8')), 'wb') as img: +- img.write(data) ++ if path := self.get_safe_path(output_dir, toc_item.name.decode('utf-8')): ++ with open(path, 'wb') as img: ++ img.write(data) + + def extract_content(self, output_dir): + self.log.debug('Extracting content from file...') diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0092-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch calibre-8.5.0+ds/debian/patches/upstream/0092-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch --- calibre-8.5.0+ds/debian/patches/upstream/0092-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch 2026-05-25 08:19:53.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0092-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch 1970-01-01 09:00:00.000000000 +0900 @@ -1,31 +0,0 @@ -From: Kovid Goyal <[email protected]> -Date: Mon, 16 Mar 2026 08:50:19 +0530 -Subject: CVE-2026-33205 (1/2): E-book viewer: prevent reading background - images from outside the config dir - -Forwarded: not-needed -Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-4926-v9px-wv7v -Origin: https://github.com/kovidgoyal/calibre/commit/6eb7b5458f183c8a037e9d7dac428122a77204e4 - -Signed-off-by: YOKOTA Hiroshi <[email protected]> ---- - src/calibre/gui2/viewer/web_view.py | 5 ++++- - 1 file changed, 4 insertions(+), 1 deletion(-) - -diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py -index d2116bb..e09688c 100644 ---- a/src/calibre/gui2/viewer/web_view.py -+++ b/src/calibre/gui2/viewer/web_view.py -@@ -106,8 +106,11 @@ def background_image(encoded_fname=''): - except FileNotFoundError: - return 'image/jpeg', b'' - fname = bytes.fromhex(encoded_fname).decode() -- img_path = os.path.join(viewer_config_dir, 'background-images', fname) -+ base = os.path.abspath(os.path.join(viewer_config_dir, 'background-images')) + os.sep -+ img_path = os.path.abspath(os.path.join(base, fname)) - mt = guess_type(fname)[0] or 'image/jpeg' -+ if not img_path.startswith(base): -+ return mt, b'' - try: - with open(make_long_path_useable(img_path), 'rb') as f: - return mt, f.read() diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0093-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch calibre-8.5.0+ds/debian/patches/upstream/0093-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch --- calibre-8.5.0+ds/debian/patches/upstream/0093-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch 1970-01-01 09:00:00.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0093-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch 2026-08-04 16:13:08.000000000 +0900 @@ -0,0 +1,31 @@ +From: Kovid Goyal <[email protected]> +Date: Mon, 16 Mar 2026 08:50:19 +0530 +Subject: CVE-2026-33205 (1/2): E-book viewer: prevent reading background + images from outside the config dir + +Forwarded: not-needed +Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-4926-v9px-wv7v +Origin: https://github.com/kovidgoyal/calibre/commit/6eb7b5458f183c8a037e9d7dac428122a77204e4 + +Signed-off-by: YOKOTA Hiroshi <[email protected]> +--- + src/calibre/gui2/viewer/web_view.py | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py +index d2116bb..e09688c 100644 +--- a/src/calibre/gui2/viewer/web_view.py ++++ b/src/calibre/gui2/viewer/web_view.py +@@ -106,8 +106,11 @@ def background_image(encoded_fname=''): + except FileNotFoundError: + return 'image/jpeg', b'' + fname = bytes.fromhex(encoded_fname).decode() +- img_path = os.path.join(viewer_config_dir, 'background-images', fname) ++ base = os.path.abspath(os.path.join(viewer_config_dir, 'background-images')) + os.sep ++ img_path = os.path.abspath(os.path.join(base, fname)) + mt = guess_type(fname)[0] or 'image/jpeg' ++ if not img_path.startswith(base): ++ return mt, b'' + try: + with open(make_long_path_useable(img_path), 'rb') as f: + return mt, f.read() diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0093-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch calibre-8.5.0+ds/debian/patches/upstream/0093-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch --- calibre-8.5.0+ds/debian/patches/upstream/0093-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch 2026-05-25 08:19:53.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0093-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch 1970-01-01 09:00:00.000000000 +0900 @@ -1,36 +0,0 @@ -From: Kovid Goyal <[email protected]> -Date: Mon, 16 Mar 2026 08:58:25 +0530 -Subject: CVE-2026-33205 (2/2): E-book viewer: Disallow background images from - the internet. This was an unused feature anyway - -Forwarded: not-needed -Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-4926-v9px-wv7v -Origin: https://github.com/kovidgoyal/calibre/commit/b1ef6a8142b8dadeb7e72c250c65d42b36ee7118 - -Signed-off-by: YOKOTA Hiroshi <[email protected]> ---- - src/calibre/gui2/viewer/web_view.py | 11 ----------- - 1 file changed, 11 deletions(-) - -diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py -index e09688c..9c8e237 100644 ---- a/src/calibre/gui2/viewer/web_view.py -+++ b/src/calibre/gui2/viewer/web_view.py -@@ -115,17 +115,6 @@ def background_image(encoded_fname=''): - with open(make_long_path_useable(img_path), 'rb') as f: - return mt, f.read() - except FileNotFoundError: -- if fname.startswith(('https://', 'http://')): -- from calibre import browser -- br = browser() -- try: -- with br.open(fname) as src: -- data = src.read() -- except Exception: -- return mt, b'' -- with open(make_long_path_useable(img_path), 'wb') as dest: -- dest.write(data) -- return mt, data - return mt, b'' - - diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0094-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch calibre-8.5.0+ds/debian/patches/upstream/0094-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch --- calibre-8.5.0+ds/debian/patches/upstream/0094-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch 1970-01-01 09:00:00.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0094-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch 2026-08-04 16:13:08.000000000 +0900 @@ -0,0 +1,36 @@ +From: Kovid Goyal <[email protected]> +Date: Mon, 16 Mar 2026 08:58:25 +0530 +Subject: CVE-2026-33205 (2/2): E-book viewer: Disallow background images from + the internet. This was an unused feature anyway + +Forwarded: not-needed +Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-4926-v9px-wv7v +Origin: https://github.com/kovidgoyal/calibre/commit/b1ef6a8142b8dadeb7e72c250c65d42b36ee7118 + +Signed-off-by: YOKOTA Hiroshi <[email protected]> +--- + src/calibre/gui2/viewer/web_view.py | 11 ----------- + 1 file changed, 11 deletions(-) + +diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py +index e09688c..9c8e237 100644 +--- a/src/calibre/gui2/viewer/web_view.py ++++ b/src/calibre/gui2/viewer/web_view.py +@@ -115,17 +115,6 @@ def background_image(encoded_fname=''): + with open(make_long_path_useable(img_path), 'rb') as f: + return mt, f.read() + except FileNotFoundError: +- if fname.startswith(('https://', 'http://')): +- from calibre import browser +- br = browser() +- try: +- with br.open(fname) as src: +- data = src.read() +- except Exception: +- return mt, b'' +- with open(make_long_path_useable(img_path), 'wb') as dest: +- dest.write(data) +- return mt, data + return mt, b'' + + diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0094-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch calibre-8.5.0+ds/debian/patches/upstream/0094-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch --- calibre-8.5.0+ds/debian/patches/upstream/0094-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch 2026-05-25 08:19:53.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0094-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch 1970-01-01 09:00:00.000000000 +0900 @@ -1,27 +0,0 @@ -From: Kovid Goyal <[email protected]> -Date: Mon, 16 Mar 2026 08:37:16 +0530 -Subject: CVE-2026-33206: TXT Input: Ensure resource files are read only from - book contents - -Forwarded: not-needed -Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-h3p4-m74f-43g6 -Origin: https://github.com/kovidgoyal/calibre/commit/c43f347837dbc00d9a7b5ff15a228b6f6081e290 - -Signed-off-by: YOKOTA Hiroshi <[email protected]> ---- - src/calibre/ebooks/conversion/plugins/txt_input.py | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/src/calibre/ebooks/conversion/plugins/txt_input.py b/src/calibre/ebooks/conversion/plugins/txt_input.py -index d6d43a6..6bc8289 100644 ---- a/src/calibre/ebooks/conversion/plugins/txt_input.py -+++ b/src/calibre/ebooks/conversion/plugins/txt_input.py -@@ -112,7 +112,7 @@ class TXTInput(InputFormatPlugin): - src = img.get('src') - prefix = src.split(':', 1)[0].lower() - if src and prefix not in ('file', 'http', 'https', 'ftp') and not os.path.isabs(src): -- src = os.path.join(base_dir, src) -+ src = os.path.abspath(os.path.join(base_dir, src)) - if os.path.normcase(src).startswith(base_dir) and os.path.isfile(src) and os.access(src, os.R_OK): - with open(src, 'rb') as f: - data = f.read() diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0095-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch calibre-8.5.0+ds/debian/patches/upstream/0095-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch --- calibre-8.5.0+ds/debian/patches/upstream/0095-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch 1970-01-01 09:00:00.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0095-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch 2026-08-04 16:13:08.000000000 +0900 @@ -0,0 +1,27 @@ +From: Kovid Goyal <[email protected]> +Date: Mon, 16 Mar 2026 08:37:16 +0530 +Subject: CVE-2026-33206: TXT Input: Ensure resource files are read only from + book contents + +Forwarded: not-needed +Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-h3p4-m74f-43g6 +Origin: https://github.com/kovidgoyal/calibre/commit/c43f347837dbc00d9a7b5ff15a228b6f6081e290 + +Signed-off-by: YOKOTA Hiroshi <[email protected]> +--- + src/calibre/ebooks/conversion/plugins/txt_input.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/calibre/ebooks/conversion/plugins/txt_input.py b/src/calibre/ebooks/conversion/plugins/txt_input.py +index d6d43a6..6bc8289 100644 +--- a/src/calibre/ebooks/conversion/plugins/txt_input.py ++++ b/src/calibre/ebooks/conversion/plugins/txt_input.py +@@ -112,7 +112,7 @@ class TXTInput(InputFormatPlugin): + src = img.get('src') + prefix = src.split(':', 1)[0].lower() + if src and prefix not in ('file', 'http', 'https', 'ftp') and not os.path.isabs(src): +- src = os.path.join(base_dir, src) ++ src = os.path.abspath(os.path.join(base_dir, src)) + if os.path.normcase(src).startswith(base_dir) and os.path.isfile(src) and os.access(src, os.R_OK): + with open(src, 'rb') as f: + data = f.read() --===============4685015674154026920== Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="calibre_8.5.0+ds-1+deb13u4-minimize.diff" diff --git a/debian/changelog b/debian/changelog index 2c68c9e613..13caa1e364 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +calibre (8.5.0+ds-1+deb13u4) trixie; urgency=medium + + * This update adds fixup for CVE-2026-25636 patch in 8.5.0+ds-1+deb13u2 + + * Set default Debian branch name for trixie + * CVE-2026-25636 (1/2): EPUB Input: Ensure font obfuscation processing + happens only on files from the EPUB (Closes: #1143484) + * Rename a CVE-2026-25636 patch in 8.5.0+ds-1+deb13u2 + * Renumber patches + + -- YOKOTA Hiroshi <[email protected]> Tue, 04 Aug 2026 16:13:08 +0900 + calibre (8.5.0+ds-1+deb13u3) trixie; urgency=medium * Fix security vulnerabilities and code quality issues (Closes: #1135543) diff --git a/debian/gbp.conf b/debian/gbp.conf index 6b7cdfe43c..4cc8345f8e 100644 --- a/debian/gbp.conf +++ b/debian/gbp.conf @@ -1,5 +1,6 @@ [DEFAULT] pristine-tar = True +debian-branch = debian/trixie [dch] full = True diff --git a/debian/patches/series b/debian/patches/series index da92a1e411..8ee25d4e3d 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -81,14 +81,15 @@ pykakasi/0080-Revert-Fix-a-regression-that-caused-incorrect-Englis.patch 0081-Revert-Update-7zip-wrapper-code-for-removal-of-read-.patch upstream/0082-Fix-CVE-2025-64486.patch upstream/0083-CVE-2026-25635-CHM-Input-Ignore-internal-files-that-.patch -upstream/0084-CVE-2026-25636-DRYer.patch -upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch -upstream/0086-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch -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 -upstream/0091-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch -upstream/0092-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch -upstream/0093-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch -upstream/0094-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch +upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch +upstream/0085-CVE-2026-25636-2-2-DRYer.patch +upstream/0086-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch +upstream/0087-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch +upstream/0088-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch +upstream/0089-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch +upstream/0090-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch +upstream/0091-Fix-security-vulnerabilities-and-code-quality-issues.patch +upstream/0092-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch +upstream/0093-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch +upstream/0094-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch +upstream/0095-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch diff --git a/debian/patches/upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch b/debian/patches/upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch new file mode 100644 index 0000000000..9f906d768a --- /dev/null +++ b/debian/patches/upstream/0084-CVE-2026-25636-1-2-EPUB-Input-Ensure-font-obfuscatio.patch @@ -0,0 +1,63 @@ +From: Kovid Goyal <[email protected]> +Date: Mon, 2 Feb 2026 09:51:18 +0530 +Subject: CVE-2026-25636 (1/2): EPUB Input: Ensure font obfuscation processing + happens only on files from the EPUB + +Forwarded: not-needed +Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-8r26-m7j5-hm29 +Origin: backport, https://github.com/kovidgoyal/calibre/commit/267bfd34020a4f297c2de9cc0cde50ebe5d024d4 +Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1143484 + +Signed-off-by: YOKOTA Hiroshi <[email protected]> +--- + src/calibre/ebooks/conversion/plugins/epub_input.py | 4 +++- + src/calibre/utils/filenames.py | 14 ++++++++++++++ + 2 files changed, 17 insertions(+), 1 deletion(-) + +diff --git a/src/calibre/ebooks/conversion/plugins/epub_input.py b/src/calibre/ebooks/conversion/plugins/epub_input.py +index 2505169..f62fed1 100644 +--- a/src/calibre/ebooks/conversion/plugins/epub_input.py ++++ b/src/calibre/ebooks/conversion/plugins/epub_input.py +@@ -44,6 +44,8 @@ class EPUBInput(InputFormatPlugin): + import uuid + + from lxml import etree ++ ++ from calibre.utils.filenames import is_existing_subpath + idpf_key = opf.raw_unique_identifier + if idpf_key: + idpf_key = re.sub(r'[ \t\r\n]', '', idpf_key) +@@ -74,7 +76,7 @@ class EPUBInput(InputFormatPlugin): + uri = cr.get('URI') + path = os.path.abspath(os.path.join(os.path.dirname(encfile), '..', *uri.split('/'))) + tkey = (key if algorithm == ADOBE_OBFUSCATION else idpf_key) +- if (tkey and os.path.exists(path)): ++ if (tkey and is_existing_subpath(path, os.getcwd())): + self._encrypted_font_uris.append(uri) + decrypt_font(tkey, path, algorithm) + return True +diff --git a/src/calibre/utils/filenames.py b/src/calibre/utils/filenames.py +index 5182f75..1cb07f4 100644 +--- a/src/calibre/utils/filenames.py ++++ b/src/calibre/utils/filenames.py +@@ -644,6 +644,20 @@ def copytree_using_links(path, dest, dest_is_parent=True, filecopyfunc=copyfile) + filecopyfunc(src, df) + + ++def is_existing_subpath(child: str, parent: str) -> bool: ++ ' Check if child is under parent. If either child or parent dont exist, returns False. ' ++ try: ++ parent = os.path.realpath(parent, strict=True) # resolve symlinks ++ child = os.path.realpath(child, strict=True) ++ except OSError: ++ return False ++ parent = os.path.abspath(parent) ++ child = os.path.abspath(child) ++ if not parent.endswith(os.sep): ++ parent += os.sep ++ return child.startswith(parent) ++ ++ + rmtree = shutil.rmtree + + diff --git a/debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch b/debian/patches/upstream/0085-CVE-2026-25636-2-2-DRYer.patch similarity index 89% rename from debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch rename to debian/patches/upstream/0085-CVE-2026-25636-2-2-DRYer.patch index c4d08af444..e057ec930c 100644 --- a/debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch +++ b/debian/patches/upstream/0085-CVE-2026-25636-2-2-DRYer.patch @@ -1,6 +1,6 @@ From: Kovid Goyal <[email protected]> Date: Mon, 2 Feb 2026 11:25:09 +0530 -Subject: CVE-2026-25636: DRYer +Subject: CVE-2026-25636 (2/2): DRYer Forwarded: not-needed Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-8r26-m7j5-hm29 @@ -12,10 +12,10 @@ Signed-off-by: YOKOTA Hiroshi <[email protected]> 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/conversion/plugins/epub_input.py b/src/calibre/ebooks/conversion/plugins/epub_input.py -index 2505169..edfebee 100644 +index f62fed1..6b81949 100644 --- a/src/calibre/ebooks/conversion/plugins/epub_input.py +++ b/src/calibre/ebooks/conversion/plugins/epub_input.py -@@ -66,15 +66,17 @@ class EPUBInput(InputFormatPlugin): +@@ -68,15 +68,17 @@ class EPUBInput(InputFormatPlugin): try: root = etree.parse(encfile) @@ -30,7 +30,7 @@ index 2505169..edfebee 100644 - path = os.path.abspath(os.path.join(os.path.dirname(encfile), '..', *uri.split('/'))) + path = os.path.abspath(os.path.join(base, '..', *uri.split('/'))) tkey = (key if algorithm == ADOBE_OBFUSCATION else idpf_key) -- if (tkey and os.path.exists(path)): +- if (tkey and is_existing_subpath(path, os.getcwd())): + if (tkey and is_existing_subpath(path, container_base)): self._encrypted_font_uris.append(uri) decrypt_font(tkey, path, algorithm) diff --git a/debian/patches/upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch b/debian/patches/upstream/0086-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch similarity index 100% rename from debian/patches/upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch rename to debian/patches/upstream/0086-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch diff --git a/debian/patches/upstream/0086-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch b/debian/patches/upstream/0087-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch similarity index 100% rename from debian/patches/upstream/0086-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch rename to debian/patches/upstream/0087-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch diff --git a/debian/patches/upstream/0087-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch b/debian/patches/upstream/0088-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch similarity index 100% rename from debian/patches/upstream/0087-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch rename to debian/patches/upstream/0088-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch diff --git a/debian/patches/upstream/0088-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch b/debian/patches/upstream/0089-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch similarity index 100% rename from debian/patches/upstream/0088-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch rename to debian/patches/upstream/0089-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch diff --git a/debian/patches/upstream/0089-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch b/debian/patches/upstream/0090-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch similarity index 100% rename from debian/patches/upstream/0089-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch rename to debian/patches/upstream/0090-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch diff --git a/debian/patches/upstream/0090-Fix-security-vulnerabilities-and-code-quality-issues.patch b/debian/patches/upstream/0091-Fix-security-vulnerabilities-and-code-quality-issues.patch similarity index 100% rename from debian/patches/upstream/0090-Fix-security-vulnerabilities-and-code-quality-issues.patch rename to debian/patches/upstream/0091-Fix-security-vulnerabilities-and-code-quality-issues.patch diff --git a/debian/patches/upstream/0091-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch b/debian/patches/upstream/0092-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch similarity index 100% rename from debian/patches/upstream/0091-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch rename to debian/patches/upstream/0092-CVE-2026-30853-RB-Input-Ensure-files-are-extracted-w.patch diff --git a/debian/patches/upstream/0092-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch b/debian/patches/upstream/0093-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch similarity index 100% rename from debian/patches/upstream/0092-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch rename to debian/patches/upstream/0093-CVE-2026-33205-1-2-E-book-viewer-prevent-reading-bac.patch diff --git a/debian/patches/upstream/0093-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch b/debian/patches/upstream/0094-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch similarity index 100% rename from debian/patches/upstream/0093-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch rename to debian/patches/upstream/0094-CVE-2026-33205-2-2-E-book-viewer-Disallow-background.patch diff --git a/debian/patches/upstream/0094-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch b/debian/patches/upstream/0095-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch similarity index 100% rename from debian/patches/upstream/0094-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch rename to debian/patches/upstream/0095-CVE-2026-33206-TXT-Input-Ensure-resource-files-are-r.patch --===============4685015674154026920==--