Server-Side Request Forgery (SSRF) in janeczku/calibre-web
Description
Server-Side Request Forgery (SSRF) in Pypi calibreweb prior to 0.6.16.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
SSRF in Calibre-Web versions prior to 0.6.16 allows an attacker to make the server send requests to internal resources via the Kobo sync token generation endpoint.
Vulnerability
A Server-Side Request Forgery (SSRF) vulnerability exists in the Kobo sync authentication token generation endpoint of Calibre-Web prior to version 0.6.16. The endpoint /generate_auth_token/ improperly validates the Host header, allowing an attacker to specify an internal IP address or IPv6 loopback ([::1]) to bypass intended restrictions and generate token URLs that point to internal services [1][2][3].
Exploitation
An attacker with network access to the Calibre-Web instance can send a crafted HTTP request to the Kobo token generation endpoint with a manipulated Host header (e.g., 127.0.0.1, [::1], or an internal IP). The server then generates an authentication token URL containing the attacker-controlled host, which can be used to trigger subsequent server-side requests to internal resources, potentially leading to information disclosure [3]. No authentication is required if the endpoint is publicly accessible.
Impact
Successful exploitation enables an attacker to perform unauthorized requests from the Calibre-Web server to internal systems, such as metadata services or cloud provider endpoints, that are not directly reachable from the external network. This can result in data leakage, internal network reconnaissance, or further compromise of backend services [2][3].
Mitigation
The vulnerability is fixed in Calibre-Web version 0.6.16, released on 2022-01-30 [1][2][3]. Users should upgrade to this version or later. As a workaround, restrict network access to the Calibre-Web instance and disable the Kobo sync feature if not required.
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
calibrewebPyPI | < 0.6.16 | 0.6.16 |
Affected products
2- janeczku/janeczku/calibre-webv5Range: unspecified
Patches
235f6f4c727c8Deleted book formats remove book from synced to kobo table
6 files changed · +128 −151
cps/editbooks.py+2 −1 modified@@ -341,7 +341,8 @@ def delete_book_from_table(book_id, book_format, jsonResponse): else: calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\ filter(db.Data.format == book_format).delete() - kobo_sync_status.remove_synced_book(book.id, True) + if book_format.upper() in ['KEPUB', 'EPUB', 'EPUB3']: + kobo_sync_status.remove_synced_book(book.id, True) calibre_db.session.commit() except Exception as ex: log.debug_or_exception(ex)
cps/helper.py+10 −4 modified@@ -17,18 +17,18 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import sys import os import io import mimetypes import re import shutil -import time +import socket import unicodedata from datetime import datetime, timedelta from tempfile import gettempdir - +from urllib.parse import urlparse import requests + from babel.dates import format_datetime from babel.units import format_unit from flask import send_from_directory, make_response, redirect, abort, url_for @@ -584,10 +584,16 @@ def get_book_cover_internal(book, use_generic_cover_on_failure): # saves book cover from url def save_cover_from_url(url, book_path): try: + # 127.0.x.x, localhost, [::1], [::ffff:7f00:1] + ip = socket.getaddrinfo(urlparse(url).hostname, 0)[0][4][0] + if ip.startswith("127.") or ip.startswith('::ffff:7f') or ip == "::1": + log.error("Localhost was accessed for cover upload") + return False, _("You are not allowed to access localhost for cover uploads") img = requests.get(url, timeout=(10, 200)) # ToDo: Error Handling img.raise_for_status() return save_cover(img, book_path) - except (requests.exceptions.HTTPError, + except (socket.gaierror, + requests.exceptions.HTTPError, requests.exceptions.ConnectionError, requests.exceptions.Timeout) as ex: log.info(u'Cover Download Error %s', ex)
cps/kobo_sync_status.py+9 −4 modified@@ -21,6 +21,7 @@ from . import ub import datetime from sqlalchemy.sql.expression import or_, and_, true +from sqlalchemy import exc # Add the current book id to kobo_synced_books table for current user, if entry is already present, # do nothing (safety precaution) @@ -36,14 +37,18 @@ def add_synced_books(book_id): # Select all entries of current book in kobo_synced_books table, which are from current user and delete them -def remove_synced_book(book_id, all=False): +def remove_synced_book(book_id, all=False, session=None): if not all: user = ub.KoboSyncedBooks.user_id == current_user.id else: user = true() - ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id) \ - .filter(user).delete() - ub.session_commit() + if not session: + ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id).filter(user).delete() + ub.session_commit() + else: + session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id).filter(user).delete() + ub.session_commit(sess=session) + def change_archived_books(book_id, state=None, message=None):
cps/tasks/convert.py+6 −2 modified@@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import sys import os import re @@ -31,7 +30,8 @@ from cps import logger, config from cps.subproc_wrapper import process_open from flask_babel import gettext as _ -from flask import url_for +from cps.kobo_sync_status import remove_synced_book +from cps.ub import ini from cps.tasks.mail import TaskEmail from cps import gdriveutils @@ -147,6 +147,10 @@ def _convert_ebook_format(self): try: local_db.session.merge(new_format) local_db.session.commit() + if self.settings['new_book_format'].upper() in ['KEPUB', 'EPUB', 'EPUB3']: + ub_session = ini() + remove_synced_book(book_id, True, ub_session) + ub_session.close() except SQLAlchemyError as e: local_db.session.rollback() log.error("Database error: %s", e)
cps/ub.py+12 −3 modified@@ -773,6 +773,14 @@ def create_admin_user(session): except Exception: session.rollback() +def ini(): + global app_DB_path + engine = create_engine(u'sqlite:///{0}'.format(app_DB_path), echo=False) + + Session = scoped_session(sessionmaker()) + Session.configure(bind=engine) + return Session() + def init_db(app_db_path): # Open session for database connection @@ -830,12 +838,13 @@ def dispose(): except Exception: pass -def session_commit(success=None): +def session_commit(success=None, sess=None): + s = sess if sess else session try: - session.commit() + s.commit() if success: log.info(success) except (exc.OperationalError, exc.InvalidRequestError) as e: - session.rollback() + s.rollback() log.debug_or_exception(e) return ""
test/Calibre-Web TestSummary_Linux.html+89 −137 modified@@ -37,20 +37,20 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> <div class="row"> <div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;"> - <p class='text-justify attribute'><strong>Start Time: </strong>2022-01-18 21:11:17</p> + <p class='text-justify attribute'><strong>Start Time: </strong>2022-01-23 05:50:54</p> </div> </div> <div class="row"> <div class="col-xs-6 col-md-6 col-sm-offset-3"> - <p class='text-justify attribute'><strong>Stop Time: </strong>2022-01-19 01:03:52</p> + <p class='text-justify attribute'><strong>Stop Time: </strong>2022-01-23 10:25:22</p> </div> </div> <div class="row"> <div class="col-xs-6 col-md-6 col-sm-offset-3"> - <p class='text-justify attribute'><strong>Duration: </strong>3h 12 min</p> + <p class='text-justify attribute'><strong>Duration: </strong>3h 54 min</p> </div> </div> </div> @@ -714,15 +714,15 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> - <tr id="su" class="skipClass"> + <tr id="su" class="errorClass"> <td>TestEditAdditionalBooks</td> - <td class="text-center">17</td> + <td class="text-center">18</td> <td class="text-center">15</td> <td class="text-center">0</td> - <td class="text-center">0</td> + <td class="text-center">1</td> <td class="text-center">2</td> <td class="text-center"> - <a onclick="showClassDetail('c10', 17)">Detail</a> + <a onclick="showClassDetail('c10', 18)">Detail</a> </td> </tr> @@ -809,7 +809,36 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> - <tr id='pt10.10' class='hiddenRow bg-success'> + <tr id="et10.10" class="none bg-info"> + <td> + <div class='testcase'>TestEditAdditionalBooks - test_upload_cbz_coverformats</div> + </td> + <td colspan='6'> + <div class="text-center"> + <a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_et10.10')">ERROR</a> + </div> + <!--css div popup start--> + <div id="div_et10.10" class="popup_window test_output" style="display:block;"> + <div class='close_button pull-right'> + <button type="button" class="close" aria-label="Close" onfocus="this.blur();" + onclick="document.getElementById('div_et10.10').style.display='none'"><span + aria-hidden="true">×</span></button> + </div> + <div class="text-left pull-left"> + <pre class="text-left">Traceback (most recent call last): + File "/home/ozzie/Development/calibre-web-test/test/test_edit_additional_books.py", line 59, in test_upload_cbz_coverformats + original_cover = self.check_element_on_page((By.ID, "detailcover")).screenshot_as_png +AttributeError: 'bool' object has no attribute 'screenshot_as_png'</pre> + </div> + <div class="clearfix"></div> + </div> + <!--css div popup end--> + </td> + </tr> + + + + <tr id='pt10.11' class='hiddenRow bg-success'> <td> <div class='testcase'>TestEditAdditionalBooks - test_upload_edit_role</div> </td> @@ -818,7 +847,7 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> - <tr id='pt10.11' class='hiddenRow bg-success'> + <tr id='pt10.12' class='hiddenRow bg-success'> <td> <div class='testcase'>TestEditAdditionalBooks - test_upload_metadata_cbr</div> </td> @@ -827,7 +856,7 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> - <tr id='pt10.12' class='hiddenRow bg-success'> + <tr id='pt10.13' class='hiddenRow bg-success'> <td> <div class='testcase'>TestEditAdditionalBooks - test_upload_metadata_cbt</div> </td> @@ -836,19 +865,19 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> - <tr id="st10.13" class="none bg-warning"> + <tr id="st10.14" class="none bg-warning"> <td> <div class='testcase'>TestEditAdditionalBooks - test_writeonly_calibre_database</div> </td> <td colspan='6'> <div class="text-center"> - <a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_st10.13')">SKIP</a> + <a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_st10.14')">SKIP</a> </div> <!--css div popup start--> - <div id="div_st10.13" class="popup_window test_output" style="display:none;"> + <div id="div_st10.14" class="popup_window test_output" style="display:none;"> <div class='close_button pull-right'> <button type="button" class="close" aria-label="Close" onfocus="this.blur();" - onclick="document.getElementById('div_st10.13').style.display='none'"><span + onclick="document.getElementById('div_st10.14').style.display='none'"><span aria-hidden="true">×</span></button> </div> <div class="text-left pull-left"> @@ -862,7 +891,7 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> - <tr id='pt10.14' class='hiddenRow bg-success'> + <tr id='pt10.15' class='hiddenRow bg-success'> <td> <div class='testcase'>TestEditAdditionalBooks - test_writeonly_path</div> </td> @@ -871,7 +900,7 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> - <tr id='st10.15' class='none bg-warning'> + <tr id='st10.16' class='none bg-warning'> <td> <div class='testcase'>TestEditAdditionalBooks - test_xss_author_edit</div> </td> @@ -880,7 +909,7 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> - <tr id='pt10.16' class='hiddenRow bg-success'> + <tr id='pt10.17' class='hiddenRow bg-success'> <td> <div class='testcase'>TestEditAdditionalBooks - test_xss_comment_edit</div> </td> @@ -889,7 +918,7 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> - <tr id='pt10.17' class='hiddenRow bg-success'> + <tr id='pt10.18' class='hiddenRow bg-success'> <td> <div class='testcase'>TestEditAdditionalBooks - test_xss_custom_comment_edit</div> </td> @@ -901,13 +930,13 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> <tr id="su" class="skipClass"> <td>TestEditBooks</td> + <td class="text-center">36</td> <td class="text-center">35</td> - <td class="text-center">34</td> <td class="text-center">0</td> <td class="text-center">0</td> <td class="text-center">1</td> <td class="text-center"> - <a onclick="showClassDetail('c11', 35)">Detail</a> + <a onclick="showClassDetail('c11', 36)">Detail</a> </td> </tr> @@ -1237,6 +1266,15 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> <tr id='pt11.35' class='hiddenRow bg-success'> + <td> + <div class='testcase'>TestEditBooks - test_upload_cbz_coverformats</div> + </td> + <td colspan='6' align='center'>PASS</td> + </tr> + + + + <tr id='pt11.36' class='hiddenRow bg-success'> <td> <div class='testcase'>TestEditBooks - test_upload_cover_hdd</div> </td> @@ -1423,56 +1461,36 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> - <tr id="su" class="failClass"> + <tr id="su" class="passClass"> <td>TestLoadMetadata</td> <td class="text-center">1</td> - <td class="text-center">0</td> <td class="text-center">1</td> <td class="text-center">0</td> <td class="text-center">0</td> + <td class="text-center">0</td> <td class="text-center"> <a onclick="showClassDetail('c13', 1)">Detail</a> </td> </tr> - <tr id="ft13.1" class="none bg-danger"> + <tr id='pt13.1' class='hiddenRow bg-success'> <td> <div class='testcase'>TestLoadMetadata - test_load_metadata</div> </td> - <td colspan='6'> - <div class="text-center"> - <a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft13.1')">FAIL</a> - </div> - <!--css div popup start--> - <div id="div_ft13.1" class="popup_window test_output" style="display:block;"> - <div class='close_button pull-right'> - <button type="button" class="close" aria-label="Close" onfocus="this.blur();" - onclick="document.getElementById('div_ft13.1').style.display='none'"><span - aria-hidden="true">×</span></button> - </div> - <div class="text-left pull-left"> - <pre class="text-left">Traceback (most recent call last): - File "/home/ozzie/Development/calibre-web-test/test/test_edit_books_metadata.py", line 136, in test_load_metadata - self.assertGreaterEqual(diff(BytesIO(cover), BytesIO(original_cover), delete_diff_file=True), 0.05) -AssertionError: 0.0 not greater than or equal to 0.05</pre> - </div> - <div class="clearfix"></div> - </div> - <!--css div popup end--> - </td> + <td colspan='6' align='center'>PASS</td> </tr> - <tr id="su" class="errorClass"> + <tr id="su" class="passClass"> <td>TestEditBooksOnGdrive</td> <td class="text-center">20</td> - <td class="text-center">19</td> + <td class="text-center">20</td> + <td class="text-center">0</td> <td class="text-center">0</td> - <td class="text-center">1</td> <td class="text-center">0</td> <td class="text-center"> <a onclick="showClassDetail('c14', 20)">Detail</a> @@ -1616,31 +1634,11 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> - <tr id="et14.16" class="none bg-info"> + <tr id='pt14.16' class='hiddenRow bg-success'> <td> <div class='testcase'>TestEditBooksOnGdrive - test_edit_title</div> </td> - <td colspan='6'> - <div class="text-center"> - <a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_et14.16')">ERROR</a> - </div> - <!--css div popup start--> - <div id="div_et14.16" class="popup_window test_output" style="display:block;"> - <div class='close_button pull-right'> - <button type="button" class="close" aria-label="Close" onfocus="this.blur();" - onclick="document.getElementById('div_et14.16').style.display='none'"><span - aria-hidden="true">×</span></button> - </div> - <div class="text-left pull-left"> - <pre class="text-left">Traceback (most recent call last): - File "/home/ozzie/Development/calibre-web-test/test/test_edit_ebooks_gdrive.py", line 245, in test_edit_title - self.assertEqual(ele.text, u'Very long extra super turbo cool title without any issue of displaying including ö utf-8 characters') -AttributeError: 'bool' object has no attribute 'text'</pre> - </div> - <div class="clearfix"></div> - </div> - <!--css div popup end--> - </td> + <td colspan='6' align='center'>PASS</td> </tr> @@ -2008,11 +2006,11 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> - <tr id="su" class="failClass"> + <tr id="su" class="passClass"> <td>TestKoboSync</td> <td class="text-center">11</td> - <td class="text-center">10</td> - <td class="text-center">1</td> + <td class="text-center">11</td> + <td class="text-center">0</td> <td class="text-center">0</td> <td class="text-center">0</td> <td class="text-center"> @@ -2094,31 +2092,11 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> - <tr id="ft23.9" class="none bg-danger"> + <tr id='pt23.9' class='hiddenRow bg-success'> <td> <div class='testcase'>TestKoboSync - test_sync_shelf</div> </td> - <td colspan='6'> - <div class="text-center"> - <a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft23.9')">FAIL</a> - </div> - <!--css div popup start--> - <div id="div_ft23.9" class="popup_window test_output" style="display:block;"> - <div class='close_button pull-right'> - <button type="button" class="close" aria-label="Close" onfocus="this.blur();" - onclick="document.getElementById('div_ft23.9').style.display='none'"><span - aria-hidden="true">×</span></button> - </div> - <div class="text-left pull-left"> - <pre class="text-left">Traceback (most recent call last): - File "/home/ozzie/Development/calibre-web-test/test/test_kobo_sync.py", line 350, in test_sync_shelf - self.assertEqual(1, len(data), data) -AssertionError: 1 != 0 : []</pre> - </div> - <div class="clearfix"></div> - </div> - <!--css div popup end--> - </td> + <td colspan='6' align='center'>PASS</td> </tr> @@ -3013,11 +2991,11 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> - <tr id="su" class="failClass"> + <tr id="su" class="passClass"> <td>TestReader</td> <td class="text-center">5</td> - <td class="text-center">4</td> - <td class="text-center">1</td> + <td class="text-center">5</td> + <td class="text-center">0</td> <td class="text-center">0</td> <td class="text-center">0</td> <td class="text-center"> @@ -3054,37 +3032,11 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> - <tr id="ft33.4" class="none bg-danger"> + <tr id='pt33.4' class='hiddenRow bg-success'> <td> <div class='testcase'>TestReader - test_sound_listener</div> </td> - <td colspan='6'> - <div class="text-center"> - <a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft33.4')">FAIL</a> - </div> - <!--css div popup start--> - <div id="div_ft33.4" class="popup_window test_output" style="display:block;"> - <div class='close_button pull-right'> - <button type="button" class="close" aria-label="Close" onfocus="this.blur();" - onclick="document.getElementById('div_ft33.4').style.display='none'"><span - aria-hidden="true">×</span></button> - </div> - <div class="text-left pull-left"> - <pre class="text-left">Traceback (most recent call last): - File "/home/ozzie/Development/calibre-web-test/test/test_reader.py", line 230, in test_sound_listener - self.sound_test('music.flac', 'Unknown - music', '0:02') - File "/home/ozzie/Development/calibre-web-test/test/test_reader.py", line 219, in sound_test - self.assertEqual(duration, duration_item.text) -AssertionError: '0:02' != '0:01' -- 0:02 -? ^ -+ 0:01 -? ^</pre> - </div> - <div class="clearfix"></div> - </div> - <!--css div popup end--> - </td> + <td colspan='6' align='center'>PASS</td> </tr> @@ -4427,9 +4379,9 @@ <h1 id='report_title' class="text-center">Calibre-Web Tests</h1> <tr id='total_row' class="text-center bg-grey"> <td>Total</td> - <td>386</td> - <td>374</td> - <td>3</td> + <td>388</td> + <td>379</td> + <td>0</td> <td>1</td> <td>8</td> <td> </td> @@ -4459,7 +4411,7 @@ <h4 class="panel-title"> <tr> <th>Platform</th> - <td>Linux 5.13.0-25-generic #26~20.04.1-Ubuntu SMP Fri Jan 7 16:27:40 UTC 2022 x86_64 x86_64</td> + <td>Linux 5.13.0-27-generic #29~20.04.1-Ubuntu SMP Fri Jan 14 00:32:30 UTC 2022 x86_64 x86_64</td> <td>Basic</td> </tr> @@ -4561,7 +4513,7 @@ <h4 class="panel-title"> <tr> <th>SQLAlchemy</th> - <td>1.4.29</td> + <td>1.4.31</td> <td>Basic</td> </tr> @@ -4591,7 +4543,7 @@ <h4 class="panel-title"> <tr> <th>google-api-python-client</th> - <td>2.35.0</td> + <td>2.36.0</td> <td>TestCliGdrivedb</td> </tr> @@ -4621,7 +4573,7 @@ <h4 class="panel-title"> <tr> <th>google-api-python-client</th> - <td>2.35.0</td> + <td>2.36.0</td> <td>TestEbookConvertCalibreGDrive</td> </tr> @@ -4651,7 +4603,7 @@ <h4 class="panel-title"> <tr> <th>google-api-python-client</th> - <td>2.35.0</td> + <td>2.36.0</td> <td>TestEbookConvertGDriveKepubify</td> </tr> @@ -4681,7 +4633,7 @@ <h4 class="panel-title"> <tr> <th>comicapi</th> - <td>2.2.0</td> + <td>2.2.1</td> <td>TestEditAdditionalBooks</td> </tr> @@ -4693,7 +4645,7 @@ <h4 class="panel-title"> <tr> <th>google-api-python-client</th> - <td>2.35.0</td> + <td>2.36.0</td> <td>TestEditBooksOnGdrive</td> </tr> @@ -4729,7 +4681,7 @@ <h4 class="panel-title"> <tr> <th>google-api-python-client</th> - <td>2.35.0</td> + <td>2.36.0</td> <td>TestSetupGdrive</td> </tr> @@ -4819,7 +4771,7 @@ <h4 class="panel-title"> </div> <script> - drawCircle(374, 3, 1, 8); + drawCircle(379, 0, 1, 8); showCase(5); </script>
3b216bfa07ecKobo sync token is now also created if accessed from localhost(fixes #1990)
5 files changed · +50 −51
cps/admin.py+6 −6 modified@@ -1426,14 +1426,14 @@ def _delete_user(content): for kobo_entry in kobo_entries: ub.session.delete(kobo_entry) ub.session_commit() - log.info(u"User {} deleted".format(content.name)) - return(_(u"User '%(nick)s' deleted", nick=content.name)) + log.info("User {} deleted".format(content.name)) + return(_("User '%(nick)s' deleted", nick=content.name)) else: - log.warning(_(u"Can't delete Guest User")) - raise Exception(_(u"Can't delete Guest User")) + log.warning(_("Can't delete Guest User")) + raise Exception(_("Can't delete Guest User")) else: - log.warning(u"No admin user remaining, can't delete user") - raise Exception(_(u"No admin user remaining, can't delete user")) + log.warning("No admin user remaining, can't delete user") + raise Exception(_("No admin user remaining, can't delete user")) def _handle_edit_user(to_save, content, languages, translations, kobo_support):
cps/editbooks.py+1 −0 modified@@ -341,6 +341,7 @@ def delete_book_from_table(book_id, book_format, jsonResponse): else: calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\ filter(db.Data.format == book_format).delete() + kobo_sync_status.remove_synced_book(book.id, True) calibre_db.session.commit() except Exception as ex: log.debug_or_exception(ex)
cps/kobo_auth.py+33 −39 modified@@ -118,55 +118,49 @@ def inner(*args, **kwargs): @kobo_auth.route("/generate_auth_token/<int:user_id>") @login_required def generate_auth_token(user_id): + warning = False host_list = request.host.rsplit(':') if len(host_list) == 1: host = ':'.join(host_list) else: host = ':'.join(host_list[0:-1]) - if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f'): - warning = _('PLease access calibre-web from non localhost to get valid api_endpoint for kobo device') - return render_title_template( - "generate_kobo_auth_url.html", - title=_(u"Kobo Setup"), - warning = warning - ) - else: - # Invalidate any prevously generated Kobo Auth token for this user. - auth_token = ub.session.query(ub.RemoteAuthToken).filter( - ub.RemoteAuthToken.user_id == user_id - ).filter(ub.RemoteAuthToken.token_type==1).first() - - if not auth_token: - auth_token = ub.RemoteAuthToken() - auth_token.user_id = user_id - auth_token.expiration = datetime.max - auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8") - auth_token.token_type = 1 - - ub.session.add(auth_token) - ub.session_commit() - - books = calibre_db.session.query(db.Books).join(db.Data).all() - - for book in books: - formats = [data.format for data in book.data] - if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats: - helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name) - - return render_title_template( - "generate_kobo_auth_url.html", - title=_(u"Kobo Setup"), - kobo_auth_url=url_for( - "kobo.TopLevelEndpoint", auth_token=auth_token.auth_token, _external=True - ), - warning = False - ) + if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f') or host == "[::1]": + warning = _('Please access Calibre-Web from non localhost to get valid api_endpoint for kobo device') + + # Generate auth token if none is existing for this user + auth_token = ub.session.query(ub.RemoteAuthToken).filter( + ub.RemoteAuthToken.user_id == user_id + ).filter(ub.RemoteAuthToken.token_type==1).first() + + if not auth_token: + auth_token = ub.RemoteAuthToken() + auth_token.user_id = user_id + auth_token.expiration = datetime.max + auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8") + auth_token.token_type = 1 + + ub.session.add(auth_token) + ub.session_commit() + + books = calibre_db.session.query(db.Books).join(db.Data).all() + + for book in books: + formats = [data.format for data in book.data] + if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats: + helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name) + + return render_title_template( + "generate_kobo_auth_url.html", + title=_(u"Kobo Setup"), + auth_token=auth_token.auth_token, + warning = warning + ) @kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"]) @login_required def delete_auth_token(user_id): - # Invalidate any prevously generated Kobo Auth token for this user. + # Invalidate any previously generated Kobo Auth token for this user ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\ .filter(ub.RemoteAuthToken.token_type==1).delete()
cps/static/js/main.js+1 −0 modified@@ -535,6 +535,7 @@ $(function() { $("#modal_kobo_token") .on("show.bs.modal", function(e) { + $(e.relatedTarget).one('focus', function(e){$(this).blur();}); var $modalBody = $(this).find(".modal-body"); // Prevent static assets from loading multiple times
cps/templates/generate_kobo_auth_url.html+9 −6 modified@@ -1,12 +1,15 @@ {% extends "fragment.html" %} {% block body %} <div class="well"> - <p> - {{_('Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):')}}</a> +<p> + {% if not warning %} + {{_('Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):')}} + </p><p> + api_endpoint={{url_for("kobo.TopLevelEndpoint", auth_token=auth_token, _external=True)}} + {% else %} + {{warning}} + </p><p>{{_('Kobo Token:')}} {{ auth_token }} + {% endif %} </p> - <p> - {% if not warning %}api_endpoint={{kobo_auth_url}}{% else %}{{warning}}{% endif %}</a> - </p> - <p> </div> {% endblock %}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-4w8p-x6g8-fv64ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-0339ghsaADVISORY
- github.com/janeczku/calibre-web/commit/35f6f4c727c887f8f3607fe3233dbc1980d15020ghsaWEB
- github.com/janeczku/calibre-web/commit/3b216bfa07ec7992eff03e55d61732af6df9bb92ghsax_refsource_MISCWEB
- github.com/janeczku/calibre-web/releases/tag/0.6.16ghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/calibreweb/PYSEC-2022-23.yamlghsaWEB
- huntr.dev/bounties/499688c4-6ac4-4047-a868-7922c3eab369ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.