From f36d3a76be04ae6d899ef0a5bb6070bf785bd4dd Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sat, 2 Apr 2022 17:29:30 +0200 Subject: [PATCH 1/6] Return false if custom columns generate an error during connect database Replaced existing with valid in readme to make it more clear what database is needed --- README.md | 2 +- cps/db.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 94be5b33..c528f09d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # About -Calibre-Web is a web app providing a clean interface for browsing, reading and downloading eBooks using an existing [Calibre](https://calibre-ebook.com) database. +Calibre-Web is a web app providing a clean interface for browsing, reading and downloading eBooks using a valid [Calibre](https://calibre-ebook.com) database. [![GitHub License](https://img.shields.io/github/license/janeczku/calibre-web?style=flat-square)](https://github.com/janeczku/calibre-web/blob/master/LICENSE) [![GitHub commit activity](https://img.shields.io/github/commit-activity/w/janeczku/calibre-web?logo=github&style=flat-square&label=commits)]() diff --git a/cps/db.py b/cps/db.py index 3f64fbb5..d2977e07 100644 --- a/cps/db.py +++ b/cps/db.py @@ -592,6 +592,7 @@ class CalibreDB: cls.setup_db_cc_classes(cc) except OperationalError as e: log.error_or_exception(e) + return False cls.session_factory = scoped_session(sessionmaker(autocommit=False, autoflush=True, From fee76741a02899d2dfdc2ce29cfc27901a06383c Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sun, 3 Apr 2022 13:38:42 +0200 Subject: [PATCH 2/6] Update Testresult --- SECURITY.md | 4 ++++ cps/constants.py | 2 +- setup.cfg | 8 ++++--- test/Calibre-Web TestSummary_Linux.html | 32 +++++++++++++------------ 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 54be54bd..78d5c6e2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -32,8 +32,12 @@ To receive fixes for security vulnerabilities it is required to always upgrade t | V 0.6.16 | JavaScript could get executed on authors page. Thanks to @alicaz || | V 0.6.16 | Localhost can no longer be used to upload covers. Thanks to @scara31 || | V 0.6.16 | Another case where public shelfs could be created without permission is prevented. Thanks to @nhiephon || +| V 0.6.16 | It's prevented to get the name of a private shelfs. Thanks to @nhiephon || | V 0.6.17 | The SSRF Protection can no longer be bypassed via an HTTP redirect. Thanks to @416e6e61 || | V 0.6.17 | The SSRF Protection can no longer be bypassed via 0.0.0.0 and it's ipv6 equivalent. Thanks to @r0hanSH || +| V 0.6.18 | Possible SQL Injection is prevented in user table Thanks to Iman Sharafaldin (Forward Security) || +| V 0.6.18 | The SSRF protection no longer can be bypassed by IPV6/IPV4 embedding. Thanks to @416e6e61 || +| V 0.6.18 | The SSRF protection no longer can be bypassed to connect to other servers in the local network. Thanks to @michaellrowley || ## Statement regarding Log4j (CVE-2021-44228 and related) diff --git a/cps/constants.py b/cps/constants.py index cb5348d5..c2eb0527 100644 --- a/cps/constants.py +++ b/cps/constants.py @@ -154,7 +154,7 @@ def selected_roles(dictionary): BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, ' 'series_id, languages, publisher') -STABLE_VERSION = {'version': '0.6.18 Beta'} +STABLE_VERSION = {'version': '0.6.18'} NIGHTLY_VERSION = dict() NIGHTLY_VERSION[0] = '$Format:%H$' diff --git a/setup.cfg b/setup.cfg index 9ad1164a..49496fc9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,6 +38,7 @@ console_scripts = [options] include_package_data = True install_requires = + werkzeug<2.1.0 Babel>=1.3,<3.0 Flask-Babel>=0.11.1,<2.1.0 Flask-Login>=0.3.2,<0.5.1 @@ -52,9 +53,10 @@ install_requires = tornado>=4.1,<6.2 Wand>=0.4.4,<0.7.0 unidecode>=0.04.19,<1.4.0 - lxml>=3.8.0,<4.8.0 + lxml>=3.8.0,<4.9.0 flask-wtf>=0.14.2,<1.1.0 chardet>=3.0.0,<4.1.0 + advocate>=1.0.0,<1.1.0 [options.extras_require] @@ -71,7 +73,7 @@ gdrive = PyYAML>=3.12 rsa>=3.4.2,<4.9.0 gmail = - google-auth-oauthlib>=0.4.3,<0.5.0 + google-auth-oauthlib>=0.4.3,<0.6.0 google-api-python-client>=1.7.11,<2.43.0 goodreads = goodreads>=0.3.2,<0.4.0 @@ -84,7 +86,7 @@ oauth = SQLAlchemy-Utils>=0.33.5,<0.39.0 metadata = rarfile>=3.2 - scholarly>=1.2.0,<1.6 + scholarly>=1.2.0,<1.7 markdown2>=2.0.0,<2.5.0 html2text>=2020.1.16,<2022.1.1 python-dateutil>=2.1,<2.9.0 diff --git a/test/Calibre-Web TestSummary_Linux.html b/test/Calibre-Web TestSummary_Linux.html index 524db518..392fdbea 100644 --- a/test/Calibre-Web TestSummary_Linux.html +++ b/test/Calibre-Web TestSummary_Linux.html @@ -37,20 +37,20 @@
-

Start Time: 2022-03-28 21:45:14

+

Start Time: 2022-04-03 07:19:10

-

Stop Time: 2022-03-29 03:21:52

+

Stop Time: 2022-04-03 12:55:38

-

Duration: 4h 46 min

+

Duration: 4h 47 min

@@ -1593,9 +1593,11 @@
Traceback (most recent call last):
-  File "/home/ozzie/Development/calibre-web-test/test/test_edit_books_metadata.py", line 167, 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
+ File "/home/ozzie/Development/calibre-web-test/test/test_edit_books_metadata.py", line 235, in test_load_metadata + self.assertEqual("奇想西遊記1", results[3]['title']) +AssertionError: '奇想西遊記1' != '巧讀西遊記' +- 奇想西遊記1 ++ 巧讀西遊記
@@ -4599,7 +4601,7 @@ AssertionError: 0.0 not greater than or equal to 0.05 Platform - Linux 5.13.0-37-generic #42~20.04.1-Ubuntu SMP Tue Mar 15 15:44:28 UTC 2022 x86_64 x86_64 + Linux 5.13.0-39-generic #44~20.04.1-Ubuntu SMP Thu Mar 24 16:43:35 UTC 2022 x86_64 x86_64 Basic @@ -4659,13 +4661,7 @@ AssertionError: 0.0 not greater than or equal to 0.05 Flask-WTF - 1.0.0 - Basic - - - - gevent - 21.12.0 + 1.0.1 Basic @@ -4719,7 +4715,13 @@ AssertionError: 0.0 not greater than or equal to 0.05 SQLAlchemy - 1.4.32 + 1.4.34 + Basic + + + + tornado + 6.1 Basic From 8adae6ed0cd749f52af0300923c74c8c7c28b241 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sun, 3 Apr 2022 20:26:43 +0200 Subject: [PATCH 3/6] Handle permission errors for static files (Fix for #2358) Version bump --- cps/cache_buster.py | 15 +++++++++------ cps/constants.py | 2 +- cps/error_handler.py | 5 +++-- cps/helper.py | 9 ++++++--- cps/render_template.py | 14 +++++++++----- cps/web.py | 6 +++++- 6 files changed, 33 insertions(+), 18 deletions(-) diff --git a/cps/cache_buster.py b/cps/cache_buster.py index 9619d605..ba19afd6 100644 --- a/cps/cache_buster.py +++ b/cps/cache_buster.py @@ -47,13 +47,16 @@ def init_cache_busting(app): for filename in filenames: # compute version component rooted_filename = os.path.join(dirpath, filename) - with open(rooted_filename, 'rb') as f: - file_hash = hashlib.md5(f.read()).hexdigest()[:7] # nosec + try: + with open(rooted_filename, 'rb') as f: + file_hash = hashlib.md5(f.read()).hexdigest()[:7] # nosec + # save version to tables + file_path = rooted_filename.replace(static_folder, "") + file_path = file_path.replace("\\", "/") # Convert Windows path to web path + hash_table[file_path] = file_hash + except PermissionError: + log.error("No permission to access {} file.".format(rooted_filename)) - # save version to tables - file_path = rooted_filename.replace(static_folder, "") - file_path = file_path.replace("\\", "/") # Convert Windows path to web path - hash_table[file_path] = file_hash log.debug('Finished computing cache-busting values') def bust_filename(filename): diff --git a/cps/constants.py b/cps/constants.py index c2eb0527..f40d16b0 100644 --- a/cps/constants.py +++ b/cps/constants.py @@ -154,7 +154,7 @@ def selected_roles(dictionary): BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, ' 'series_id, languages, publisher') -STABLE_VERSION = {'version': '0.6.18'} +STABLE_VERSION = {'version': '0.6.19 Beta'} NIGHTLY_VERSION = dict() NIGHTLY_VERSION[0] = '$Format:%H$' diff --git a/cps/error_handler.py b/cps/error_handler.py index 37b7500e..67252a66 100644 --- a/cps/error_handler.py +++ b/cps/error_handler.py @@ -42,8 +42,9 @@ def error_http(error): def internal_error(error): return render_template('http_error.html', - error_code="Internal Server Error", - error_name=str(error), + error_code="500 Internal Server Error", + error_name='The server encountered an internal error and was unable to complete your ' + 'request. There is an error in the application.', issue=True, unconfigured=False, error_stack=traceback.format_exc().split("\n"), diff --git a/cps/helper.py b/cps/helper.py index 742188d0..5d5cc021 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -698,9 +698,12 @@ def delete_book(book, calibrepath, book_format): def get_cover_on_failure(use_generic_cover): if use_generic_cover: - return send_from_directory(_STATIC_DIR, "generic_cover.jpg") - else: - return None + try: + return send_from_directory(_STATIC_DIR, "generic_cover.jpg") + except PermissionError: + log.error("No permission to access generic_cover.jpg file.") + abort(403) + abort(404) def get_book_cover(book_id): diff --git a/cps/render_template.py b/cps/render_template.py index 0c5423c9..d2f40d6c 100644 --- a/cps/render_template.py +++ b/cps/render_template.py @@ -18,11 +18,11 @@ from flask import render_template, request from flask_babel import gettext as _ -from flask import g +from flask import g, abort from werkzeug.local import LocalProxy from flask_login import current_user -from . import config, constants, ub, logger, db, calibre_db +from . import config, constants, logger from .ub import User @@ -119,6 +119,10 @@ def get_sidebar_config(kwargs=None): # Returns the template for rendering and includes the instance name def render_title_template(*args, **kwargs): sidebar, simple = get_sidebar_config(kwargs) - return render_template(instance=config.config_calibre_web_title, sidebar=sidebar, simple=simple, - accept=constants.EXTENSIONS_UPLOAD, # read_book_ids=get_readbooks_ids(), - *args, **kwargs) + try: + return render_template(instance=config.config_calibre_web_title, sidebar=sidebar, simple=simple, + accept=constants.EXTENSIONS_UPLOAD, + *args, **kwargs) + except PermissionError: + log.error("No permission to access {} file.".format(args[0])) + abort(403) diff --git a/cps/web.py b/cps/web.py index e8bf48e3..a0ecbb8e 100644 --- a/cps/web.py +++ b/cps/web.py @@ -1368,7 +1368,11 @@ def get_cover(book_id): @web.route("/robots.txt") def get_robots(): - return send_from_directory(constants.STATIC_DIR, "robots.txt") + try: + return send_from_directory(constants.STATIC_DIR, "robots.txt") + except PermissionError: + log.error("No permission to access robots.txt file.") + abort(403) @web.route("/show//", defaults={'anyname': 'None'}) From 42b0226f1a2ac9f457ae693d87517252dbb7c2ec Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Mon, 4 Apr 2022 13:58:47 +0200 Subject: [PATCH 4/6] Fix for missing "query" entry in flask_session --- cps/web.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cps/web.py b/cps/web.py index a0ecbb8e..e6ca4d03 100644 --- a/cps/web.py +++ b/cps/web.py @@ -383,7 +383,7 @@ def render_books_list(data, sort_param, book_id, page): offset = int(int(config.config_books_per_page) * (page - 1)) return render_search_results(term, offset, order, config.config_books_per_page) elif data == "advsearch": - term = json.loads(flask_session['query']) + term = json.loads(flask_session.get('query', '{}')) offset = int(int(config.config_books_per_page) * (page - 1)) return render_adv_search_results(term, offset, order, config.config_books_per_page) else: @@ -1556,7 +1556,7 @@ def login(): config.config_is_initial = False return redirect_back(url_for("web.index")) else: - log.warning('Login failed for user "%s" IP-address: %s', form['username'], ip_address) + log.warning('Login failed for user "{}" IP-address: {}'.format(form['username'], ip_address)) flash(_(u"Wrong Username or Password"), category="error") next_url = request.args.get('next', default=url_for("web.index"), type=str) From d912c1c47606ada9b636d56905e4ac714efd9710 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Mon, 4 Apr 2022 14:37:39 +0200 Subject: [PATCH 5/6] Bugfix Quick start section --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c528f09d..267f21ef 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,11 @@ In the Wiki there are also examples for: a [manual installation](https://github. ## Quick start -Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog -Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button\ -Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/Configuration#using-google-drive-integration) -Go to Login page +Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog \ +Login with default admin login \ +Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button \ +Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/Configuration#using-google-drive-integration) \ +Afterwards you can configure your Calibre-Web instance ([Basic Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#basic-configuration) and [UI Configuration](https://github.com/janeczku/calibre-web/wiki/Configuration#ui-configuration) on admin page) #### Default admin login: *Username:* admin\ @@ -67,7 +68,7 @@ Optionally, to enable on-the-fly conversion from one ebook format to another whe [Download and install](https://calibre-ebook.com/download) the Calibre desktop program for your platform and enter the folder including program name (normally /opt/calibre/ebook-convert, or C:\Program Files\calibre\ebook-convert.exe) in the field "calibre's converter tool" on the setup page. -[Download](https://github.com/pgaskin/kepubify/releases/latest) Kepubify tool for your platform and place the binary starting with `kepubify` in Linux: `\opt\kepubify` Windows: `C:\Program Files\kepubify`. +[Download](https://github.com/pgaskin/kepubify/releases/latest) Kepubify tool for your platform and place the binary starting with `kepubify` in Linux: `/opt/kepubify` Windows: `C:\Program Files\kepubify`. ## Docker Images From a63af5882ed9e97284dc81cc198e06267b68ca1a Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Tue, 5 Apr 2022 19:04:42 +0200 Subject: [PATCH 6/6] Fix for #2363 (Handle empty response from lubimyczytac metadata provider) --- cps/metadata_provider/lubimyczytac.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/cps/metadata_provider/lubimyczytac.py b/cps/metadata_provider/lubimyczytac.py index 814a785e..90a6b2d8 100644 --- a/cps/metadata_provider/lubimyczytac.py +++ b/cps/metadata_provider/lubimyczytac.py @@ -113,17 +113,18 @@ class LubimyCzytac(Metadata): ) -> Optional[List[MetaRecord]]: if self.active: result = requests.get(self._prepare_query(title=query)) - root = fromstring(result.text) - lc_parser = LubimyCzytacParser(root=root, metadata=self) - matches = lc_parser.parse_search_results() - if matches: - with ThreadPool(processes=10) as pool: - final_matches = pool.starmap( - lc_parser.parse_single_book, - [(match, generic_cover, locale) for match in matches], - ) - return final_matches - return matches + if result.text: + root = fromstring(result.text) + lc_parser = LubimyCzytacParser(root=root, metadata=self) + matches = lc_parser.parse_search_results() + if matches: + with ThreadPool(processes=10) as pool: + final_matches = pool.starmap( + lc_parser.parse_single_book, + [(match, generic_cover, locale) for match in matches], + ) + return final_matches + return matches def _prepare_query(self, title: str) -> str: query = ""