Version in base suite: 6.13.0+repack-2+deb12u5 Base version: calibre_6.13.0+repack-2+deb12u5 Target version: calibre_6.13.0+repack-2+deb12u6 Base file: /srv/ftp-master.debian.org/ftp/pool/main/c/calibre/calibre_6.13.0+repack-2+deb12u5.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/c/calibre/calibre_6.13.0+repack-2+deb12u6.dsc NEWS | 20 calibre.install | 1 changelog | 22 control | 1 patches/0037-CVE-2026-25635-CHM-Input-Ignore-internal-files-that-.patch | 81 + patches/0038-CVE-2026-25636-DRYer.patch | 37 patches/0039-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch | 541 ++++++++++ patches/0040-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch | 36 patches/0041-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch | 120 ++ patches/0042-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch | 43 patches/0043-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch | 34 patches/series | 7 12 files changed, 942 insertions(+), 1 deletion(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpuqpcuukz/calibre_6.13.0+repack-2+deb12u5.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpuqpcuukz/calibre_6.13.0+repack-2+deb12u6.dsc: no acceptable signature found diff -Nru calibre-6.13.0+repack/debian/NEWS calibre-6.13.0+repack/debian/NEWS --- calibre-6.13.0+repack/debian/NEWS 2025-09-21 15:51:57.000000000 +0000 +++ calibre-6.13.0+repack/debian/NEWS 2026-03-01 07:14:12.000000000 +0000 @@ -1,3 +1,23 @@ +calibre (6.13.0+repack-2+deb12u6) bookworm; urgency=medium + + This version includes CVE-2026-25731 fix. From this version, HTML + template engine is changed from "templite" to "pystache". + This change makes some impact to some uses. + + Who is impacted: + * Users who download and use third-party HTML export templates + * Users who use templates shared on forums, GitHub, or other sources + * Systems running Calibre in automated pipelines with user-supplied + templates + + If you are impacted user, you needs to port your template file to + pystache. + + See upstream document for more info: + https://github.com/kovidgoyal/calibre/security/advisories/GHSA-xrh9-w7qx-3gcc + + -- YOKOTA Hiroshi Sun, 01 Mar 2026 16:00:08 +0900 + calibre (4.99.3+dfsg-2) unstable; urgency=medium This is the first version of Calibre using Python3 uploaded to sid. diff -Nru calibre-6.13.0+repack/debian/calibre.install calibre-6.13.0+repack/debian/calibre.install --- calibre-6.13.0+repack/debian/calibre.install 2025-09-21 15:51:57.000000000 +0000 +++ calibre-6.13.0+repack/debian/calibre.install 2026-03-01 05:20:54.000000000 +0000 @@ -7,7 +7,6 @@ # usr/lib/calibre/odf usr/lib/calibre/qt -usr/lib/calibre/templite usr/lib/calibre/tinycss usr/lib/calibre/css_selectors usr/lib/calibre/polyglot diff -Nru calibre-6.13.0+repack/debian/changelog calibre-6.13.0+repack/debian/changelog --- calibre-6.13.0+repack/debian/changelog 2025-11-09 09:15:24.000000000 +0000 +++ calibre-6.13.0+repack/debian/changelog 2026-03-01 07:14:12.000000000 +0000 @@ -1,3 +1,25 @@ +calibre (6.13.0+repack-2+deb12u6) bookworm; urgency=medium + + * CVE-2026-25635: CHM Input: Ignore internal files that have paths that + end up outside the container + * CVE-2026-25636: DRYer + * 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. + * Use pystache instead of templite to fix CVE-2026-25731 + * Add NEWS about CVE-2026-25731 fix + * CVE-2026-26064: ODT Input: Ensure images are extracted within + container + * CVE-2026-26065: PDB Input: Ensure extracted images are within the + container + * CVE-2026-27810: Content server: Sanitize content disposition received + as query parameter + * 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 + + -- YOKOTA Hiroshi Sun, 01 Mar 2026 16:14:12 +0900 + calibre (6.13.0+repack-2+deb12u5) bookworm; urgency=medium * Fix CVE-2025-64486 diff -Nru calibre-6.13.0+repack/debian/control calibre-6.13.0+repack/debian/control --- calibre-6.13.0+repack/debian/control 2025-11-09 09:15:24.000000000 +0000 +++ calibre-6.13.0+repack/debian/control 2026-03-01 07:14:12.000000000 +0000 @@ -63,6 +63,7 @@ python3-pyqt6.qtsvg, python3-pyqt6.qtwebengine, python3-pyqtbuild, + python3-pystache, python3-regex, python3-routes, python3-setuptools, diff -Nru calibre-6.13.0+repack/debian/patches/0037-CVE-2026-25635-CHM-Input-Ignore-internal-files-that-.patch calibre-6.13.0+repack/debian/patches/0037-CVE-2026-25635-CHM-Input-Ignore-internal-files-that-.patch --- calibre-6.13.0+repack/debian/patches/0037-CVE-2026-25635-CHM-Input-Ignore-internal-files-that-.patch 1970-01-01 00:00:00.000000000 +0000 +++ calibre-6.13.0+repack/debian/patches/0037-CVE-2026-25635-CHM-Input-Ignore-internal-files-that-.patch 2026-03-01 07:14:12.000000000 +0000 @@ -0,0 +1,81 @@ +From: Kovid Goyal +Date: Wed, 4 Feb 2026 09:39:54 +0530 +Subject: CVE-2026-25635: CHM Input: Ignore internal files that have paths + that end up outside the container + +Forwarded: not-needed +Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-32vh-whvh-9fxr +Origin: backport, https://github.com/kovidgoyal/calibre/commit/9739232fcb029ac15dfe52ccd4fdb4a07ebb6ce9 + +Also, allow extraction of long filenames + +Signed-off-by: YOKOTA Hiroshi +--- + src/calibre/ebooks/chm/reader.py | 33 ++++++++++++++++----------------- + 1 file changed, 16 insertions(+), 17 deletions(-) + +diff --git a/src/calibre/ebooks/chm/reader.py b/src/calibre/ebooks/chm/reader.py +index 102f439..de5deaf 100644 +--- a/src/calibre/ebooks/chm/reader.py ++++ b/src/calibre/ebooks/chm/reader.py +@@ -12,6 +12,7 @@ from calibre.constants import filesystem_encoding, iswindows + from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString + from calibre.ebooks.chardet import xml_to_unicode + from calibre.ebooks.metadata.toc import TOC ++from calibre.utils.filenames import make_long_path_useable + from chm.chm import CHMFile, chmlib + from polyglot.builtins import as_unicode + +@@ -159,37 +160,35 @@ class CHMReader(CHMFile): + + def ExtractFiles(self, output_dir=os.getcwd(), debug_dump=False): + html_files = set() ++ base = output_dir = os.path.abspath(output_dir) ++ if not base.endswith(os.sep): ++ base += os.sep + for path in self.Contents(): +- fpath = path +- lpath = os.path.join(output_dir, fpath) ++ fpath = path.partition(';')[0] # fix file names with ";" at the end, see _reformat() ++ fpath = fpath.replace('/', os.sep) ++ lpath = os.path.abspath(os.path.join(output_dir, fpath)) ++ if os.path.commonprefix((lpath, base)) != base: ++ self.log.warn(f'{path!r} outside container, skipping') ++ continue + self._ensure_dir(lpath) + try: + data = self.GetFile(path) + except: + self.log.exception('Failed to extract %s from CHM, ignoring'%path) + continue +- if lpath.find(';') != -1: +- # fix file names with ";" at the end, see _reformat() +- lpath = lpath.split(';')[0] ++ with open(make_long_path_useable(lpath), 'wb') as f: ++ f.write(data) + try: +- with open(lpath, 'wb') as f: +- f.write(data) +- try: +- if 'html' in guess_mimetype(path)[0]: +- html_files.add(lpath) +- except: +- pass ++ if 'html' in guess_mimetype(os.path.basename(lpath))[0]: ++ html_files.add(lpath) + except: +- if iswindows and len(lpath) > 250: +- self.log.warn('%r filename too long, skipping'%path) +- continue +- raise ++ pass + + if debug_dump: + import shutil + shutil.copytree(output_dir, os.path.join(debug_dump, 'debug_dump')) + for lpath in html_files: +- with open(lpath, 'r+b') as f: ++ with open(make_long_path_useable(lpath), 'r+b') as f: + data = f.read() + data = self._reformat(data, lpath) + if isinstance(data, str): diff -Nru calibre-6.13.0+repack/debian/patches/0038-CVE-2026-25636-DRYer.patch calibre-6.13.0+repack/debian/patches/0038-CVE-2026-25636-DRYer.patch --- calibre-6.13.0+repack/debian/patches/0038-CVE-2026-25636-DRYer.patch 1970-01-01 00:00:00.000000000 +0000 +++ calibre-6.13.0+repack/debian/patches/0038-CVE-2026-25636-DRYer.patch 2026-03-01 07:14:12.000000000 +0000 @@ -0,0 +1,37 @@ +From: Kovid Goyal +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 +--- + 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 9915c2b..87c0e7d 100644 +--- a/src/calibre/ebooks/conversion/plugins/epub_input.py ++++ b/src/calibre/ebooks/conversion/plugins/epub_input.py +@@ -62,15 +62,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-6.13.0+repack/debian/patches/0039-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch calibre-6.13.0+repack/debian/patches/0039-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch --- calibre-6.13.0+repack/debian/patches/0039-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch 1970-01-01 00:00:00.000000000 +0000 +++ calibre-6.13.0+repack/debian/patches/0039-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch 2026-03-01 07:14:12.000000000 +0000 @@ -0,0 +1,541 @@ +From: Kovid Goyal +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 +--- + COPYRIGHT | 6 -- + 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 | 60 +++++++++------ + src/templite/__init__.py | 89 ---------------------- + 7 files changed, 163 insertions(+), 252 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 3425d82..702ced2 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/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 @@ ++ ++ ++ ++{{{head_content}}} ++ ++ ++ ++ ++ ++ ++
++
++ {{#meta.titles}} ++ {{#is_first}} ++

{{title}}

++ {{/is_first}} ++ {{^is_first}} ++
{{title}}
++ {{/is_first}} ++ {{/meta.titles}} ++
++
{{meta.creators}}
++
++ ++
++ ++
++ {{#has_link}} ++
++ {{#prev_link}} ++ {{prev_page}} ++ {{/prev_link}} ++ {{^prev_link}} ++ {{prev_page}} ++ {{/prev_link}} ++ {{#next_link}} ++ {{next_page}} ++ {{/next_link}} ++
++ {{/has_link}} ++ ++ {{{ebook_content}}} ++
++ ++ {{#has_toc}} ++
++

{{table_of_contents}}

++ {{{toc}}} ++
++ {{/has_toc}} ++ ++
++ {{#prev_link}} ++ {{prev_page}} ++ {{/prev_link}} ++ {{^prev_link}} ++ {{prev_page}} ++ {{/prev_link}} ++ ++ {{start}} ++ ++ {{#next_link}} ++ {{next_page}} ++ {{/next_link}} ++
++ ++
++ ++ ++ +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 @@ +- +- +- +-${head_content}$ +- +- +- +- +- +- +-
+-
+- ${pos1=1}$ +- ${for title in meta.titles():}$ +- ${if pos1:}$ +-

+- ${print(title)}$ +-

+- ${:else:}$ +-
${print(title)}$
+- ${:endif}$ +- ${pos1=0}$ +- ${:endfor}$ +-
+-
+- ${print(', '.join(meta.creators()))}$ +-
+-
+- +-
+- +-
+- ${if prevLink or nextLink:}$ +-
+- ${if prevLink:}$ +- ${print(_('previous page'))}$ +- ${:else:}$ +- ${print(_('previous page'))}$ +- ${:endif}$ +- +- ${if nextLink:}$ +- ${print(_('next page'))}$ +- ${:endif}$ +-
+- ${:endif}$ +- +- ${ebookContent}$ +-
+- +- ${if has_toc:}$ +-
+-

${print( _('Table of contents'))}$

+- ${print(toc())}$ +-
+- ${:endif}$ +- +-
+- ${if prevLink:}$ +- ${print(_('previous page'))}$ +- ${:else:}$ +- ${print(_('previous page'))}$ +- ${:endif}$ +- +- ${print(_('start'))}$ +- +- ${if nextLink:}$ +- ${print(_('next page'))}$ +- ${:endif}$ +-
+- +-
+- +- +- +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 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++{{meta.creators}} - {{meta.first_title}} ++ ++{{#meta.items}} ++ ++{{/meta.items}} ++ ++ ++ ++ ++ ++
++
++ {{#meta.titles}} ++ {{#is_first}} ++

{{title}}

++ {{/is_first}} ++ {{^is_first}} ++
{{title}}
++ {{/is_first}} ++ {{/meta.titles}} ++
++
{{meta.creators}}
++
++ ++
++
++ {{#has_toc}} ++
++

{{table_of_contents}}

++ {{{toc}}} ++
++ {{/has_toc}} ++ {{^has_toc}} ++

{{no_toc}}

++ ++ {{/has_toc}} ++
++ ++
++ {{#next_link}} ++ {{next_page}} ++ {{/next_link}} ++
++
++ ++ ++ +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 @@ +- +- +- +- +- +- +- +- +-${print(', '.join(meta.creators()))}$ - ${print(next(meta.titles())); print(meta.titles().close())}$ +- +-${for item in meta:}$ +- +-${:endfor}$ +- +- +- +- +- +-
+-
+- ${pos1=1}$ +- ${for title in meta.titles():}$ +- ${if pos1:}$ +-

+- ${print(title)}$ +-

+- ${:else:}$ +-
${print(title)}$
+- ${:endif}$ +- ${pos1=0}$ +- ${:endfor}$ +-
+-
+- ${print(', '.join(meta.creators()))}$ +-
+-
+- +-
+-
+- +- ${if has_toc:}$ +-
+-

${print(_('Table of contents'))}$

+- ${toc}$ +-
+- ${:else:}$ +-

${print(_('No table of contents present'))}$

+- +- ${:endif}$ +- +-
+- +-
+- ${if nextLink:}$ +- ${print(_('next page'))}$ +- ${:endif}$ +-
+-
+- +- +- +diff --git a/src/calibre/ebooks/conversion/plugins/html_output.py b/src/calibre/ebooks/conversion/plugins/html_output.py +index d8e2f18..2d311d1 100644 +--- a/src/calibre/ebooks/conversion/plugins/html_output.py ++++ b/src/calibre/ebooks/conversion/plugins/html_output.py +@@ -26,13 +26,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 ' +@@ -84,6 +84,7 @@ class HTMLOutput(OutputFormatPlugin): + xml_declaration=False) + + def convert(self, oeb_book, output_path, input_plugin, opts, log): ++ import pystache + from lxml import etree + + from calibre.ebooks.html.meta import EasyMeta +@@ -96,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: +@@ -110,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 +@@ -129,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) +@@ -196,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 f46e777..0000000 +--- a/src/templite/__init__.py ++++ /dev/null +@@ -1,89 +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 +-# +-# 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 sys, re +- +-from polyglot.builtins import unicode_type +- +-class Templite: +- auto_emit = re.compile('(^[\'\"])|(^[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), '' % 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-6.13.0+repack/debian/patches/0040-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch calibre-6.13.0+repack/debian/patches/0040-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch --- calibre-6.13.0+repack/debian/patches/0040-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch 1970-01-01 00:00:00.000000000 +0000 +++ calibre-6.13.0+repack/debian/patches/0040-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch 2026-03-01 07:14:12.000000000 +0000 @@ -0,0 +1,36 @@ +From: Kovid Goyal +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 +--- + 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 b90045e..6dc736b 100644 +--- a/src/calibre/ebooks/odt/input.py ++++ b/src/calibre/ebooks/odt/input.py +@@ -27,10 +27,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-6.13.0+repack/debian/patches/0041-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch calibre-6.13.0+repack/debian/patches/0041-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch --- calibre-6.13.0+repack/debian/patches/0041-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch 1970-01-01 00:00:00.000000000 +0000 +++ calibre-6.13.0+repack/debian/patches/0041-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch 2026-03-01 07:14:12.000000000 +0000 @@ -0,0 +1,120 @@ +From: Kovid Goyal +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 +--- + 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 16c35b8..5b1348c 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 +@@ -157,13 +166,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(0, 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('Writing image %s to images/' % name) +- 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) + +@@ -210,8 +220,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(0, 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 152b3d1..9b4ccf4 100644 +--- a/src/calibre/ebooks/pdb/ereader/reader202.py ++++ b/src/calibre/ebooks/pdb/ereader/reader202.py +@@ -109,13 +109,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('Writing image %s to images/' % name) + imgf.write(img) + +@@ -151,6 +151,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 +@@ -159,8 +168,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(0, 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-6.13.0+repack/debian/patches/0042-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch calibre-6.13.0+repack/debian/patches/0042-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch --- calibre-6.13.0+repack/debian/patches/0042-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch 1970-01-01 00:00:00.000000000 +0000 +++ calibre-6.13.0+repack/debian/patches/0042-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch 2026-03-01 07:14:12.000000000 +0000 @@ -0,0 +1,43 @@ +From: Kovid Goyal +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 +--- + src/calibre/srv/content.py | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +diff --git a/src/calibre/srv/content.py b/src/calibre/srv/content.py +index af28510..764c48e 100644 +--- a/src/calibre/srv/content.py ++++ b/src/calibre/srv/content.py +@@ -6,6 +6,7 @@ __copyright__ = '2015, Kovid Goyal ' + + import errno + import os ++import re + from contextlib import suppress + from functools import partial + from io import BytesIO +@@ -211,7 +212,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'] = '''{}; filename="{}"; filename*=utf-8''{}'''.format( + cd, book_filename(rd, book_id, mi, fmt), book_filename(rd, book_id, mi, fmt, as_encoded_unicode=True)) + +@@ -347,3 +348,7 @@ def get(ctx, rd, what, book_id, library_id): + return book_fmt(ctx, rd, library_id, db, book_id, what.lower()) + except NoSuchFormat: + raise HTTPNotFound(f'No {what.lower()} format for the book {book_id!r}') ++ ++ ++def sanitize_content_disposition(x: str) -> str: ++ return re.sub(r'[^a-zA-Z0-9./-]', '-', x) diff -Nru calibre-6.13.0+repack/debian/patches/0043-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch calibre-6.13.0+repack/debian/patches/0043-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch --- calibre-6.13.0+repack/debian/patches/0043-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch 1970-01-01 00:00:00.000000000 +0000 +++ calibre-6.13.0+repack/debian/patches/0043-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch 2026-03-01 07:14:12.000000000 +0000 @@ -0,0 +1,34 @@ +From: Kovid Goyal +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 +--- + 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 8396316..ea9d503 100644 +--- a/src/calibre/srv/auth.py ++++ b/src/calibre/srv/auth.py +@@ -270,9 +270,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='Too many login attempts from: %s' % (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-6.13.0+repack/debian/patches/series calibre-6.13.0+repack/debian/patches/series --- calibre-6.13.0+repack/debian/patches/series 2025-11-09 09:15:24.000000000 +0000 +++ calibre-6.13.0+repack/debian/patches/series 2026-03-01 07:14:12.000000000 +0000 @@ -34,3 +34,10 @@ 0034-Fix-2075128-Private-bug-https-bugs.launchpad.net-cal.patch 0035-Fix-2076515-calibredb-list-command-ignores-fields-op.patch 0036-Fix-CVE-2025-64486.patch +0037-CVE-2026-25635-CHM-Input-Ignore-internal-files-that-.patch +0038-CVE-2026-25636-DRYer.patch +0039-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch +0040-CVE-2026-26064-ODT-Input-Ensure-images-are-extracted.patch +0041-CVE-2026-26065-PDB-Input-Ensure-extracted-images-are.patch +0042-CVE-2026-27810-Content-server-Sanitize-content-dispo.patch +0043-CVE-2026-27824-Content-server-When-banning-IPs-for-r.patch