All Advisories

mercure

SQL Injection in the Bookkeeper Query Endpoints

mercure's bookkeeper builds SQL queries by interpolating request parameters into the query string with Python f-strings, with no parameterization. The task_id parameter is reachable by any authenticated user through the web interface's API proxy, so an authenticated account can read arbitrary data from the database, including stored patient demographics, via blind SQL injection.

Authored byVolker Schönefeld, Simon Weber2026-05-30
SeverityMediumCVSS 6.5CVSS 3.1 VectorAV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NCWECWE-89 (SQL Injection)ProductmercureAffected VersionsAll releases from 0.2.0-beta.1 through 0.4.0-beta.9.Fixed In0.4.1. The bookkeeper queries use bound parameters for task_id and the timezone clause, and the order-direction value is validated against an allowlist.CVEPendingGHSAPending

Description

mercure is an open-source DICOM orchestration platform; we appreciate the maintainers' prompt, constructive response to this report. The bookkeeper records task and series metadata in a PostgreSQL database and exposes query endpoints used by the web interface.

The task_id query parameter is interpolated directly into the SQL WHERE clause with an f-string, without parameterization:

app/bookkeeping/query.py:117-150

@router.get("/task-events")
@requires("authenticated")
async def get_task_events(request) -> JSONResponse:
task_id = request.query_params.get("task_id", "") # unsanitized
# ...
query_string = f"""select *, time {tz_conversion} as local_time from task_events
where task_events.task_id = '{task_id}' {subtask_ids_filter}
order by task_events.task_id, task_events.time
"""
query = sqlalchemy.text(query_string)
results = await db.database.fetch_all(query)

View source →

The endpoint is reachable by any authenticated user through the web interface's API proxy (/api/get-task-events?task_id=...), which forwards the parameter verbatim. Two further sinks exist: the ORDER BY direction (also from a request parameter) and the timezone clause (from stored admin configuration).

Because the value lands unescaped in the query, an authenticated user can use error-based and time-based blind injection to read arbitrary tables one condition per request. We are not publishing a working payload.

The dicom_series table holds patient demographics: patient name, patient ID, birth date, accession number, and referring physician. The proof of concept enumerates the schema, confirms these columns, and confirms that patient records are present:

Proof-of-concept output

Database: PostgreSQL 14.0 (user=mercure, db=mercure)
Tables: task_events, tasks, dicom_series, dicom_files, ...
dicom_series columns: tag_patientname, tag_patientid,
tag_patientbirthdate, tag_accessionnumber, tag_referringphysicianname
Patient records present.

Impact

  • Any authenticated user, including a non-admin, can read arbitrary data from the bookkeeper database through blind SQL injection.
  • This includes the dicom_series table, which stores patient demographics (name, ID, birth date, accession number). A non-admin account can therefore read records from this table that it was never meant to see.
  • Stacked write queries are not available through this driver, so the impact is read access to the database, not modification.
  • For a hospital or clinic, this means any staff member with a mercure login can read the stored patient-demographics records, regardless of their assigned role.

Mitigation

Upgrade to mercure 0.4.1, which uses bound parameters for the task_id and timezone values and validates the order direction against an allowlist. In general, build SQL with parameter binding rather than string interpolation, which is the standard remedy for this pattern.

Defender's Checklist

  • Upgrade to 0.4.1 or later

    Deploy the patched release, which parameterizes the bookkeeper queries.

  • Review database access

    If exposure is suspected, review bookkeeper query logs for anomalous task_id values.

  • Limit accounts

    Restrict who holds mercure accounts; any authenticated account can reach this endpoint on affected versions.

Severity Reasoning

AV:NThe query endpoint is reached over the network through the web interface's API proxy.AC:LEach conditional read is a single request; no special conditions.PR:LAny authenticated account suffices; admin is not required.UI:NNo user interaction.S:UThe impact stays within the bookkeeper's database scope.C:HArbitrary database read, including patient demographics.I/A:NThe driver does not allow stacked write queries through this path, so integrity and availability are not affected.

References

How We Can Help

Who We Are

The security researchers behind this advisory.

Dr. Simon Weber Profile

Dr. rer. nat. Simon Weber

Senior Pentester & MedSec Researcher

I evaluate your SaMD with the same industry-defining security insight I contributed to the BAK MV for the revision of the B3S standard.

  • PhD on Hospital Cybersecurity
  • Critical vulnerabilities found in hospital systems
  • Alumni of THB MedSec Research Group
  • gematik Security Hero
Volker Schönefeld Profile

Dipl.-Inf. Volker Schönefeld

Senior Application Security Expert

As a former CTO and developer turned pentester, I work alongside your team to uncover vulnerabilities and find solutions that fit your architecture.

  • 20+ years as CTO, 50M+ app downloads
  • Architected and secured large-scale IoT fleets
  • Certified Web Exploitation Specialist
  • gematik Security Hero

Looking for a Penetration Test?

Machine Spirits specializes in security assessments for medical devices and healthcare IT. From MDR penetration testing to C5 cloud compliance, we help MedTech companies meet regulatory requirements.