Multiple Kobo API endpoints use calibre_db.get_book_by_uuid() at db.py:748-749,
which performs a raw unfiltered query, meaning that common_filters() isn't
applied, so tag-based ACLs, language restrictions, and archived-book filtering
are all bypassed.
Custom columns of type comments are rendered with `|safe` (disabling Jinja2
auto-escaping) and no `clean_string` sanitization. Compare with regular book
comments which correctly use `{{ entry.comments[0].text|clean_string|safe }}`.
Any user with edit permissions can set a custom comment column to
`<script>alert(document.cookie)</script>` and it will execute for every user who
views the book detail page or the OPDS feed. This is stored XSS with no
authentication barrier beyond edit permission.
Remote login tokens are generated from only 4 bytes of randomness (32 bits = ~4
billion possibilities, 8 hex characters). The /ajax/verify_token endpoint at
remotelogin.py:98 has no rate limiting. The token is valid for 10 minutes.
At even modest request rates (10,000 req/sec), an attacker can test ~6 million
tokens during the 10-minute window , which isn't enough to exhaust the full
space, sure, but combined with multiple concurrent login sessions (each
generating a new token), or if the attacker can trigger the victim to initiate
remote login, the attack becomes more feasible. Compare with the Kobo auth
token which uses urandom(16) (128 bits).
The typical Linux umask of 0022, meaning the encrypted file is world-readable
(-rw-r--r--). Any OS-level user on the same system can read the key and decrypt
the encrypted credentials from app.db.
The `serve_book` function uses `get_book()` which performs no access filtering:
it simply fetches by ID. Compare with `read_book` at web.py:1562 which
correctly uses `get_filtered_book()`. The `common_filters()` function enforces
per-user tag restrictions, language restrictions, and hidden-book rules.
hashlib.md5(dbpath) returns a hash object, not a hex string. Comparing a string
(md5Checksum) to a hash object with != always returns True. This means the
DB-replacement code path is always entered, allowing an attacker who sends a
forged notification (with the known static token) to trigger an arbitrary
metadata.db download from GDrive, replacing the live database.
When an OAuth provider_user_id is already linked to User A, and User B
authenticates with the same OAuth identity, User B is silently logged in as
User A. This is by design for single-user OAuth, but in a multi-user
environment it means: if an attacker gains access to the same OAuth provider
account (e.g., a shared GitHub org account, or by compromising the OAuth
provider), they can log in as the linked Calibre-Web user with no password
needed.
The lxml.etree.fromstring() function use the default XML parser, which resolves
external entities because XML handling defaults in Python sucks. There is no
need for such dangerous misfeatures in calibre-web, so let's disable it.
A user able to upload epub/fb2 could add something like this to the file:
```xml
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<container><rootfiles><rootfile full-path="&xxe;"/></rootfiles></container>
```
and obtain the content of the `/etc/passwd` file, which is bad™.
It should fix the following stacktrace:
```
[2021-02-18 14:46:14,771] ERROR {cps:1891} Exception on / [GET]
Traceback (most recent call last):
File "/opt/calibre/vendor/flask/app.py", line 2447, in wsgi_app
response = self.full_dispatch_request()
File "/opt/calibre/vendor/flask/app.py", line 1952, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/opt/calibre/vendor/flask/app.py", line 1821, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/opt/calibre/vendor/flask/_compat.py", line 39, in reraise
raise value
File "/opt/calibre/vendor/flask/app.py", line 1950, in full_dispatch_request
rv = self.dispatch_request()
File "/opt/calibre/vendor/flask/app.py", line 1936, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/opt/calibre/cps/usermanagement.py", line 38, in decorated_view
return login_required(func)(*args, **kwargs)
File "/opt/calibre/vendor/flask_login/utils.py", line 272, in decorated_view
return func(*args, **kwargs)
File "/opt/calibre/cps/web.py", line 719, in index
return render_books_list("newest", sort_param, 1, page)
File "/opt/calibre/cps/web.py", line 422, in render_books_list
entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, order)
File "/opt/calibre/cps/db.py", line 610, in fill_indexpage
return self.fill_indexpage_with_archived_books(page, pagesize, database, db_filter, order, False, *join)
File "/opt/calibre/cps/db.py", line 635, in fill_indexpage_with_archived_books
# book = self.order_authors(book)
UnboundLocalError: local variable 'entries' referenced before assignment
```