Skip to content

Commit

Permalink
Merge branch 'master' into Develop
Browse files Browse the repository at this point in the history
# Conflicts:
#	cps/templates/detail.html
#	test/Calibre-Web TestSummary_Linux.html
  • Loading branch information
OzzieIsaacs committed Jan 23, 2022
2 parents 1a65793 + ede273a commit 127bf98
Show file tree
Hide file tree
Showing 89 changed files with 8,593 additions and 7,554 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
- full graphical setup
- User management with fine-grained per-user permissions
- Admin interface
- User Interface in brazilian, czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, polish, russian, simplified and traditional chinese, spanish, swedish, turkish, ukrainian
- User Interface in brazilian, czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, korean, polish, russian, simplified and traditional chinese, spanish, swedish, turkish, ukrainian
- OPDS feed for eBook reader apps
- Filter and search by titles, authors, tags, series and language
- Create a custom book collection (shelves)
Expand Down
34 changes: 19 additions & 15 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,25 @@ To receive fixes for security vulnerabilities it is required to always upgrade t

## History

| Fixed in | Description |CVE number |
| ---------- |---------|---------|
| 3rd July 2018 | Guest access acts as a backdoor||
| V 0.6.7 |Hardcoded secret key for sessions |CVE-2020-12627 |
| V 0.6.13|Calibre-Web Metadata cross site scripting |CVE-2021-25964|
| V 0.6.13|Name of Shelves are only visible to users who can access the corresponding shelf Thanks to @ibarrionuevo||
| V 0.6.13|JavaScript could get executed in the description field. Thanks to @ranjit-git and Hagai Wechsler (WhiteSource)||
| V 0.6.13|JavaScript could get executed in a custom column of type "comment" field ||
| V 0.6.13|JavaScript could get executed after converting a book to another format with a title containing javascript code||
| V 0.6.13|JavaScript could get executed after converting a book to another format with a username containing javascript code||
| V 0.6.13|JavaScript could get executed in the description series, categories or publishers title||
| V 0.6.13|JavaScript could get executed in the shelf title||
| V 0.6.13|Login with the old session cookie after logout. Thanks to @ibarrionuevo||
| V 0.6.14|CSRF was possible. Thanks to @mik317 and Hagai Wechsler (WhiteSource) |CVE-2021-25965|
| V 0.6.14|Cross-Site Scripting vulnerability on typeahead inputs. Thanks to @notdodo||
| Fixed in | Description |CVE number |
|---------------|--------------------------------------------------------------------------------------------------------------------|---------|
| 3rd July 2018 | Guest access acts as a backdoor ||
| V 0.6.7 | Hardcoded secret key for sessions |CVE-2020-12627 |
| V 0.6.13 | Calibre-Web Metadata cross site scripting |CVE-2021-25964|
| V 0.6.13 | Name of Shelves are only visible to users who can access the corresponding shelf Thanks to @ibarrionuevo ||
| V 0.6.13 | JavaScript could get executed in the description field. Thanks to @ranjit-git and Hagai Wechsler (WhiteSource) ||
| V 0.6.13 | JavaScript could get executed in a custom column of type "comment" field ||
| V 0.6.13 | JavaScript could get executed after converting a book to another format with a title containing javascript code ||
| V 0.6.13 | JavaScript could get executed after converting a book to another format with a username containing javascript code ||
| V 0.6.13 | JavaScript could get executed in the description series, categories or publishers title ||
| V 0.6.13 | JavaScript could get executed in the shelf title ||
| V 0.6.13 | Login with the old session cookie after logout. Thanks to @ibarrionuevo ||
| V 0.6.14 | CSRF was possible. Thanks to @mik317 and Hagai Wechsler (WhiteSource) |CVE-2021-25965|
| V 0.6.14 | Migrated some routes to POST-requests (CSRF protection). Thanks to @scara31 ||
| V 0.6.15 | Fix for "javascript:" script links in identifier. Thanks to @scara31 ||
| V 0.6.15 | Cross-Site Scripting vulnerability on uploaded cover file names. Thanks to @ibarrionuevo ||
| V 0.6.15 | Creating public shelfs is now denied if user is missing the edit public shelf right. Thanks to @ibarrionuevo ||
| V 0.6.15 | Changed error message in case of trying to delete a shelf unauthorized. Thanks to @ibarrionuevo ||


## Staement regarding Log4j (CVE-2021-44228 and related)
Expand Down
19 changes: 13 additions & 6 deletions cps/about.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,22 @@
for i in (req + opt):
ret[i[1]] = i[0]

if constants.NIGHTLY_VERSION[0] == "$Format:%H$":
calibre_web_version = constants.STABLE_VERSION['version']
else:
calibre_web_version = (constants.STABLE_VERSION['version'] + ' - '
+ constants.NIGHTLY_VERSION[0].replace('%','%%') + ' - '
+ constants.NIGHTLY_VERSION[1].replace('%','%%'))
if getattr(sys, 'frozen', False):
calibre_web_version += " - Exe-Version"
elif constants.HOME_CONFIG:
calibre_web_version += " - pyPi"

if not ret:
_VERSIONS = OrderedDict(
Platform = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
Python=sys.version,
Calibre_Web=constants.STABLE_VERSION['version'] + ' - '
+ constants.NIGHTLY_VERSION[0].replace('%','%%') + ' - '
+ constants.NIGHTLY_VERSION[1].replace('%','%%'),
Calibre_Web=calibre_web_version,
WebServer=server.VERSION,
Flask=flask.__version__,
Flask_Login=flask_loginVersion,
Expand Down Expand Up @@ -110,9 +119,7 @@
_VERSIONS = OrderedDict(
Platform = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
Python = sys.version,
Calibre_Web = constants.STABLE_VERSION['version'] + ' - '
+ constants.NIGHTLY_VERSION[0].replace('%', '%%') + ' - '
+ constants.NIGHTLY_VERSION[1].replace('%', '%%'),
Calibre_Web=calibre_web_version,
Werkzeug = werkzeug.__version__,
Jinja2=jinja2.__version__,
pySqlite = sqlite3.version,
Expand Down
46 changes: 33 additions & 13 deletions cps/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,11 @@ def admin_forbidden():
abort(403)


@admi.route("/shutdown")
@admi.route("/shutdown", methods=["POST"])
@login_required
@admin_required
def shutdown():
task = int(request.args.get("parameter").strip())
task = request.get_json().get('parameter', -1)
showtext = {}
if task in (0, 1): # valid commandos received
# close all database connections
Expand Down Expand Up @@ -756,7 +756,12 @@ def prepare_tags(user, action, tags_name, id_list):
return ",".join(saved_tags_list)


@admi.route("/ajax/addrestriction/<int:res_type>", defaults={"user_id": 0}, methods=['POST'])
@admi.route("/ajax/addrestriction/<int:res_type>", methods=['POST'])
@login_required
@admin_required
def add_user_0_restriction(res_type):
return add_restriction(res_type, 0)

@admi.route("/ajax/addrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
@login_required
@admin_required
Expand Down Expand Up @@ -803,7 +808,13 @@ def add_restriction(res_type, user_id):
return ""


@admi.route("/ajax/deleterestriction/<int:res_type>", defaults={"user_id": 0}, methods=['POST'])
@admi.route("/ajax/deleterestriction/<int:res_type>", methods=['POST'])
@login_required
@admin_required
def delete_user_0_restriction(res_type):
return delete_restriction(res_type, 0)


@admi.route("/ajax/deleterestriction/<int:res_type>/<int:user_id>", methods=['POST'])
@login_required
@admin_required
Expand Down Expand Up @@ -895,7 +906,7 @@ def list_restriction(res_type, user_id):
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response

@admi.route("/ajax/fullsync")
@admi.route("/ajax/fullsync", methods=["POST"])
@login_required
def ajax_fullsync():
count = ub.session.query(ub.KoboSyncedBooks).filter(current_user.id == ub.KoboSyncedBooks.user_id).delete()
Expand Down Expand Up @@ -1404,16 +1415,25 @@ def _delete_user(content):
for us in ub.session.query(ub.Shelf).filter(content.id == ub.Shelf.user_id):
ub.session.query(ub.BookShelf).filter(us.id == ub.BookShelf.shelf).delete()
ub.session.query(ub.Shelf).filter(content.id == ub.Shelf.user_id).delete()
ub.session.query(ub.Bookmark).filter(content.id == ub.Bookmark.user_id).delete()
ub.session.query(ub.User).filter(ub.User.id == content.id).delete()
ub.session.query(ub.ArchivedBook).filter(ub.ArchivedBook.user_id == content.id).delete()
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == content.id).delete()
ub.session.query(ub.User_Sessions).filter(ub.User_Sessions.user_id == content.id).delete()
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.user_id == content.id).delete()
# delete KoboReadingState and all it's children
kobo_entries = ub.session.query(ub.KoboReadingState).filter(ub.KoboReadingState.user_id == content.id).all()
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):
Expand Down Expand Up @@ -1615,7 +1635,7 @@ def edit_user(user_id):
page="edituser")


@admi.route("/admin/resetpassword/<int:user_id>")
@admi.route("/admin/resetpassword/<int:user_id>", methods=["POST"])
@login_required
@admin_required
def reset_user_password(user_id):
Expand Down Expand Up @@ -1791,7 +1811,7 @@ def ldap_import_create_user(user, user_data):
return 0, message


@admi.route('/import_ldap_users')
@admi.route('/import_ldap_users', methods=["POST"])
@login_required
@admin_required
def import_ldap_users():
Expand Down
3 changes: 3 additions & 0 deletions cps/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def version_info():
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
parser.add_argument('-s', metavar='user:pass', help='Sets specific username to new password')
parser.add_argument('-f', action='store_true', help='Flag is depreciated and will be removed in next version')
parser.add_argument('-l', action='store_true', help='Allow loading covers from localhost')
args = parser.parse_args()

settingspath = args.p or os.path.join(_CONFIG_DIR, "app.db")
Expand Down Expand Up @@ -77,6 +78,8 @@ def version_info():
if args.k == "":
keyfilepath = ""

# load covers from localhost
allow_localhost = args.l or None
# handle and check ip address argument
ip_address = args.i or None
if ip_address:
Expand Down
28 changes: 14 additions & 14 deletions cps/comic.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,25 +56,25 @@

def _cover_processing(tmp_file_name, img, extension):
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg')
if use_IM:
# convert to jpg because calibre only supports jpg
if extension in NO_JPEG_EXTENSIONS:
with Image(filename=tmp_file_name) as imgc:
if extension in NO_JPEG_EXTENSIONS:
if use_IM:
with Image(blob=img) as imgc:
imgc.format = 'jpeg'
imgc.transform_colorspace('rgb')
imgc.save(tmp_cover_name)
imgc.save(filename=tmp_cover_name)
return tmp_cover_name

if not img:
else:
return None
if img:
with open(tmp_cover_name, 'wb') as f:
f.write(img)
return tmp_cover_name
else:
return None

with open(tmp_cover_name, 'wb') as f:
f.write(img)
return tmp_cover_name


def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable):
cover_data = None
cover_data = extension = None
if original_file_extension.upper() == '.CBZ':
cf = zipfile.ZipFile(tmp_file_name)
for name in cf.namelist():
Expand Down Expand Up @@ -106,7 +106,7 @@ def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecu
break
except Exception as ex:
log.debug('Rarfile failed with error: %s', ex)
return cover_data
return cover_data, extension


def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
Expand All @@ -121,7 +121,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
cover_data = archive.getPage(index)
break
else:
cover_data = _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable)
cover_data, extension = _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable)
return _cover_processing(tmp_file_name, cover_data, extension)


Expand Down
2 changes: 1 addition & 1 deletion cps/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,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.15 Beta'}
STABLE_VERSION = {'version': '0.6.16 Beta'}

NIGHTLY_VERSION = {}
NIGHTLY_VERSION[0] = '$Format:%H$'
Expand Down
22 changes: 13 additions & 9 deletions cps/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import ast
import json
from datetime import datetime
from urllib.parse import quote

from sqlalchemy import create_engine
from sqlalchemy import Table, Column, ForeignKey, CheckConstraint
Expand Down Expand Up @@ -164,6 +165,8 @@ def __repr__(self):
return u"https://portal.issn.org/resource/ISSN/{0}".format(self.val)
elif format_type == "isfdb":
return u"http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val)
elif self.val.lower().startswith("javascript:"):
return quote(self.val)
else:
return u"{0}".format(self.val)

Expand All @@ -172,8 +175,8 @@ class Comments(Base):
__tablename__ = 'comments'

id = Column(Integer, primary_key=True)
book = Column(Integer, ForeignKey('books.id'), nullable=False, unique=True)
text = Column(String(collation='NOCASE'), nullable=False)
book = Column(Integer, ForeignKey('books.id'), nullable=False)

def __init__(self, text, book):
self.text = text
Expand Down Expand Up @@ -872,23 +875,24 @@ def get_search_results(self, term, offset=None, order=None, limit=None, allow_sh
def speaking_language(self, languages=None, return_all_languages=False, with_count=False, reverse_order=False):
from . import get_locale

if not languages:
if with_count:
if with_count:
if not languages:
languages = self.session.query(Languages, func.count('books_languages_link.book'))\
.join(books_languages_link).join(Books)\
.filter(self.common_filters(return_all_languages=return_all_languages)) \
.group_by(text('books_languages_link.lang_code')).all()
for lang in languages:
lang[0].name = isoLanguages.get_language_name(get_locale(), lang[0].lang_code)
return sorted(languages, key=lambda x: x[0].name, reverse=reverse_order)
else:
for lang in languages:
lang[0].name = isoLanguages.get_language_name(get_locale(), lang[0].lang_code)
return sorted(languages, key=lambda x: x[0].name, reverse=reverse_order)
else:
if not languages:
languages = self.session.query(Languages) \
.join(books_languages_link) \
.join(Books) \
.filter(self.common_filters(return_all_languages=return_all_languages)) \
.group_by(text('books_languages_link.lang_code')).all()
for lang in languages:
lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
for lang in languages:
lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
return sorted(languages, key=lambda x: x.name, reverse=reverse_order)


Expand Down
Loading

0 comments on commit 127bf98

Please sign in to comment.