From c2da43a91ae79d2b48e9d219a065926ef176c623 Mon Sep 17 00:00:00 2001 From: Hassan Raza Date: Sat, 23 May 2026 13:31:15 +0500 Subject: [PATCH] Use rooted paths for served local files --- src/calibre/gui2/viewer/web_view.py | 33 +++++++++++++++++------------ src/calibre/srv/books.py | 9 ++++---- src/calibre/srv/content.py | 27 ++++++++++++----------- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py index 42931420e7..67b2d592fd 100644 --- a/src/calibre/gui2/viewer/web_view.py +++ b/src/calibre/gui2/viewer/web_view.py @@ -44,7 +44,7 @@ from calibre.gui2.viewer import link_prefix_for_location_links, performance_moni from calibre.gui2.viewer.config import get_session_pref, load_viewer_profiles, save_viewer_profile, viewer_config_dir, vprefs from calibre.gui2.viewer.tts import TTS from calibre.srv.code import get_translations_data -from calibre.utils.filenames import make_long_path_useable +from calibre.utils.filenames import make_long_path_useable, path_from_root from calibre.utils.localization import _, localize_user_manual_link from calibre.utils.resources import get_path as P from calibre.utils.serialize import json_loads @@ -74,9 +74,10 @@ def get_path_for_name(name): bdir = getattr(set_book_path, 'path', None) if bdir is None: return - path = os.path.abspath(os.path.join(bdir, name)) - if path.startswith(bdir): - return path + try: + return path_from_root(bdir, name) + except ValueError: + pass def get_data(name): @@ -103,12 +104,12 @@ def background_image(encoded_fname=''): return mt, data except FileNotFoundError: return 'image/jpeg', b'' - fname = bytes.fromhex(encoded_fname).decode() - base = os.path.abspath(os.path.join(viewer_config_dir, 'background-images')) + os.sep - img_path = os.path.abspath(os.path.join(base, fname)) + try: + fname = bytes.fromhex(encoded_fname).decode('utf-8') + img_path = path_from_root(os.path.abspath(os.path.join(viewer_config_dir, 'background-images')), fname, reject_colon=True) + except (ValueError, UnicodeDecodeError): + return 'image/jpeg', b'' 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() @@ -123,8 +124,12 @@ def get_mathjax_dir(): def handle_mathjax_request(rq, name): mathjax_dir = get_mathjax_dir() - path = os.path.abspath(os.path.join(mathjax_dir, '..', name)) - if path.startswith(mathjax_dir): + mathjax_name = name.partition('/')[2] + try: + path = path_from_root(mathjax_dir, mathjax_name) + except ValueError: + pass + else: mt = guess_type(name) try: with open(path, 'rb') as f: @@ -136,9 +141,9 @@ def handle_mathjax_request(rq, name): if name.endswith('/startup.js'): raw = P('pdf-mathjax-loader.js', data=True, allow_user_override=False) + raw send_reply(rq, mt, raw) - else: - prints(f'Failed to get mathjax file: {name} outside mathjax directory', file=sys.stderr) - rq.fail(QWebEngineUrlRequestJob.Error.RequestFailed) + return + prints(f'Failed to get mathjax file: {name} outside mathjax directory', file=sys.stderr) + rq.fail(QWebEngineUrlRequestJob.Error.RequestFailed) class UrlSchemeHandler(QWebEngineUrlSchemeHandler): diff --git a/src/calibre/srv/books.py b/src/calibre/srv/books.py index 035f6f1028..11c5742cbb 100644 --- a/src/calibre/srv/books.py +++ b/src/calibre/srv/books.py @@ -20,7 +20,7 @@ from calibre.srv.metadata import book_as_json from calibre.srv.render_book import RENDER_VERSION from calibre.srv.routes import endpoint, json from calibre.srv.utils import get_db, get_library_data -from calibre.utils.filenames import rmtree +from calibre.utils.filenames import path_from_root, rmtree from calibre.utils.localization import _ from calibre.utils.resources import get_path as P from calibre.utils.serialize import json_dumps @@ -187,9 +187,10 @@ def book_file(ctx, rd, book_id, fmt, size, mtime, name): if not ctx.has_id(rd, db, book_id): raise BookNotFound(book_id, db) bhash = book_hash(db.library_id, book_id, fmt, size, mtime) - base = abspath(os.path.join(books_cache_dir(), 'f')) - mpath = abspath(os.path.join(base, bhash, name)) - if not mpath.startswith(base): + base = abspath(os.path.join(books_cache_dir(), 'f', bhash)) + try: + mpath = path_from_root(base, name) + except ValueError: raise HTTPNotFound(f'No book file with hash: {bhash} and name: {name}') try: return rd.filesystem_file_with_custom_etag(open(mpath, 'rb'), bhash, name) diff --git a/src/calibre/srv/content.py b/src/calibre/srv/content.py index 810cf42baf..f8ef75fa50 100644 --- a/src/calibre/srv/content.py +++ b/src/calibre/srv/content.py @@ -31,7 +31,7 @@ from calibre.srv.routes import endpoint, json from calibre.srv.utils import get_db, get_use_roman, http_date from calibre.utils.config_base import tweaks from calibre.utils.date import timestampfromdt -from calibre.utils.filenames import ascii_filename, atomic_rename, make_long_path_useable +from calibre.utils.filenames import ascii_filename, atomic_rename, make_long_path_useable, path_from_root from calibre.utils.img import image_from_data, scale_image from calibre.utils.localization import _ from calibre.utils.resources import get_image_path as I @@ -236,11 +236,10 @@ def static(ctx, rd, what): if not what: raise HTTPNotFound() base = P('content-server', allow_user_override=False) - path = os.path.abspath(os.path.join(base, *what.split('/'))) - if not path.startswith(base) or ':' in what: + try: + path = path_from_root(base, what, reject_colon=True) + except ValueError: raise HTTPNotFound('Naughty, naughty!') - path = os.path.relpath(path, base).replace(os.sep, '/') - path = P('content-server/' + path) try: return share_open(path, 'rb') except OSError: @@ -269,16 +268,16 @@ def icon(ctx, rd, which): raise HTTPNotFound() if which.startswith('_'): base = os.path.join(config_dir, 'tb_icons') - path = os.path.abspath(os.path.join(base, *which[1:].split('/'))) - if not path.startswith(base) or ':' in which: + try: + path = path_from_root(base, which[1:], reject_colon=True) + except ValueError: raise HTTPNotFound('Naughty, naughty!') else: base = P('images', allow_user_override=False) - path = os.path.abspath(os.path.join(base, *which.split('/'))) - if not path.startswith(base) or ':' in which: + try: + path = path_from_root(base, which, reject_colon=True) + except ValueError: raise HTTPNotFound('Naughty, naughty!') - path = os.path.relpath(path, base).replace(os.sep, '/') - path = P('images/' + path) if sz == 'full': try: return share_open(path, 'rb') @@ -309,9 +308,9 @@ 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.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): + try: + q = path_from_root(base, bytes.fromhex(encoded_fname).decode('utf-8'), reject_colon=True) + except (ValueError, UnicodeDecodeError): raise HTTPNotFound(f'Reader background {encoded_fname} not found') try: return share_open(make_long_path_useable(q), 'rb')