Skip to content

Sync with upstream#370

Merged
holta merged 28 commits intoiiab:masterfrom
janeczku:master
Apr 23, 2026
Merged

Sync with upstream#370
holta merged 28 commits intoiiab:masterfrom
janeczku:master

Conversation

@deldesir
Copy link
Copy Markdown
Collaborator

This PR includes several critical security enhancements, bug fixes, and general stability improvements.

Security Fixes:

  • Patched multiple vulnerabilities including SQLi via dbpath (b5da0df), LDAP injection (cde3888), and XXE in epub/fb2/goodreads APIs (224915b).
  • Resolved access bypass on /show/ (8477731) and prevented unauthorized shelf editing (c451daa).
  • Fixed an IDOR in Kobo tokens (d598054) and prevented OAuth relinking (387678a).
  • Stopped credential leakage in debug_info (d85bef6) and applied sane permissions to the encryption key file (0959f84).

Bug Fixes & Stability:

  • Fixed the OPDS feed to ensure atom:updated correctly reflects the last modification date (ab17992).
  • Resolved crashes during metadata searches when ComicVine or Douban returns None (0ca6e86).
  • Improved error handling for sorting parameters (0ed4d81) and series display in the basic view (0887789).
  • Fixed a type condition issue in gdrive.py (8ad9f4e) and ensured strings in comments are cleaned before displaying (7c715f3).

OzzieIsaacs and others added 28 commits April 3, 2026 09:01
Authenticated users shouldn't be able to generate/delete kobo auth tokens for
other users if they're not admin.
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™.
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.
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.
No need to dump Gmail OAuth client_secret, refresh_token, and
access_token in the debug ZIP in plaintext.
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.
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.
This is reachable only by the admin users, but is still a straightforward RCE
vector, if only via SQLite's `ATTACH DATABASE` trick
Books.atom_timestamp returned Books.timestamp (date added), which is
set at import and never changes. OPDS clients use atom:updated to
decide whether a book has changed on the server, so cover swaps,
metadata edits, and any other post-import change were invisible to
sync clients; they would keep serving the stale cover and title
until a manual refresh.

Atom RFC 4287 defines updated as "the most recent instant in time
when an entry or feed was modified", and Calibre already tracks that
field as last_modified, bumping it on every metadata and cover edit.
Switching the property to return last_modified (with a fallback to
timestamp when last_modified is NULL) aligns Calibre-Web's behaviour
with the Atom contract.

This change only affects the OPDS feed's atom:updated element. Kobo
sync uses its own last_modified comparison path, so it is unaffected.
Both comicvine.py and douban.py return None instead of [] when an HTTP
error occurs during a metadata search. The consumer in search_metadata.py
iterates the result directly, which raises TypeError: 'NoneType' object
is not iterable and crashes the server on single-threaded deployments.

Fixes #3606
@holta holta added the enhancement New feature or request label Apr 23, 2026
@holta holta requested a review from chapmanjacobd April 23, 2026 03:54
@chapmanjacobd
Copy link
Copy Markdown
Member

Interesting:

File/line(s): cps/gdrive.py:141-143
The new comparison changed hashlib.md5(dbpath) to hashlib.md5(dbpath).hexdigest(), but dbpath is still the pathname
string
(.../metadata.db) encoded as bytes, not the file contents. response['file']['md5Checksum'] is the remote file-content checksum, so this comparison is still meaningless and will almost never match. In practice, every watch event for metadata.db is still treated as “database changed”, causing unnecessary backup/download/reconnect churn...

@holta
Copy link
Copy Markdown
Member

holta commented Apr 23, 2026

CI workflow https://github.com/iiab/calibre-web/actions/runs/24815679973/job/72631069725?pr=370 failed as follows: (@Akatama does this look familiar?!)

[*] Updating pip package...
/usr/bin/python -m pip install --upgrade pip
Defaulting to user installation because normal site-packages is not writeable
Requirement already satisfied: pip in /usr/lib/python3/dist-packages (24.0)
Collecting pip
  Downloading pip-26.0.1-py3-none-any.whl.metadata (4.7 kB)
Downloading pip-26.0.1-py3-none-any.whl (1.8 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.8/1.8 MB 56.2 MB/s eta 0:00:00
Installing collected packages: pip
Successfully installed pip-26.0.1
[*] Installing Python dependencies...
/usr/bin/python -m pip install -r /usr/local/calibre-web-py3/integration-tests-requirements.txt
Defaulting to user installation because normal site-packages is not writeable
ERROR: Could not open requirements file: [Errno 2] No such file or directory: '/usr/local/calibre-web-py3/integration-tests-requirements.txt'
Error: ERROR: Action failed during dependency installation attempt with error: The process '/usr/bin/python' failed with exit code 1

@Akatama
Copy link
Copy Markdown

Akatama commented Apr 23, 2026

@holta Yes. Upstream does not have integration-tests-requirements.txt. So that step will fail.

@holta holta merged commit cbc48f6 into iiab:master Apr 23, 2026
3 of 7 checks passed
@holta
Copy link
Copy Markdown
Member

holta commented Apr 23, 2026

Thanks @deldesir.

Thanks @chapmanjacobd for reviewing.

And thanks @Akatama for the reminder — confirmed after merge:

"Calibre-Web Smoke test with IIAB install / test-install" (8 min)

https://github.com/iiab/calibre-web/actions/runs/24831221738/job/72679947319

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants