All Advisories

fbeta ePA3-Service-OpenSource

HTTP Header Injection in VAU Inner Requests

fbeta's ePA3-Service builds inner-VAU HTTP requests by f-string-interpolating caller-controlled values (insurantId KVNR, USER_AGENT environment variable, boundary_string argument) directly into raw header lines, bypassing the requests library's CRLF validation. An authenticated DiGA user can smuggle a second x-insurantid header into the encrypted VAU channel by supplying a KVNR with embedded CRLF.

SeverityMediumCVSS 6.8CVSS 3.1 VectorAV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:NCWECWE-113 (Improper Neutralization of CRLF Sequences in HTTP Headers)Productfbeta ePA3-Service-OpenSourceAffected VersionsAll versions prior to 1.3.0 (initial commit `a61f91d` through 1.0.1)Fixed In1.3.0 (incorporates pull request #11)GHSAGHSA-j8jg-7fqf-4xx9

Description

All four methods that build inner-VAU HTTP requests interpolate caller-controlled parameters directly into raw HTTP headers using f-strings, then assemble the request as a UTF-8 byte string for AES-GCM encryption. The standard requests library, which rejects CRLF in header values, is bypassed entirely; no CRLF validation occurs anywhere in the inner-request construction pipeline.

VAUProtokoll.py:507-515 (pattern repeated in all 4 methods)

inner_request = "GET /epa/authz/v1/getNonce HTTP/1.1\r\n"
inner_request += f"Host: {self.host}\r\n"
inner_request += "Accept: application/json\r\n"
inner_request += "Content-Type: application/json\r\n"
inner_request += f"x-useragent: {USER_AGENT}\r\n"
inner_request += f"x-insurantid: {insurant_id}\r\n"
inner_request += "\r\n"
inner_request = inner_request.encode("utf-8")

View source →

The injectable parameters are:

Injectable parameters

insurant_id : metadata['insurantId'], validated only as JSON-Schema string
USER_AGENT : os.getenv('USER_AGENT'); poisons every request in the session
boundary_string : caller-provided in upload_document(); enables Content-Type breakout
self.host : server host string in every request

The four affected methods are get_nonce() (507-515), send_authorization_request_sc() (542-552), send_authcode_sc() (634-644), and upload_document() (688-698). The insurantId value comes from the DiGA's metadata['insurantId'] field, validated only as "type": "string" with no pattern constraint; the KVNR format is strictly [A-Z][0-9]{9} but the JSON schema does not enforce it.

During testing, a KVNR containing CRLF (X123456789\r\nX-Injected: ...) was supplied through the DiGA interface. The relay decrypted the inner HTTP and observed both headers passing through to the ePA server's inner HTTP handler:

Decrypted inner HTTP after CRLF injection

Decrypted inner HTTP (REQUEST):
GET /epa/authz/v1/getNonce HTTP/1.1
Host: epa-as-2.dev.epa4all.de
Accept: application/json
Content-Type: application/json
x-useragent: usrAgent/1.1.0
x-insurantid: X000000000
X-Injected: CRLF-header-injection-confirmed
CRLF in insurantId passed through to inner HTTP.
f-string interpolation, no sanitization.

Impact

  • Cross-patient data exposure: injecting a second x-insurantid header with a different KVNR may cause the ePA server to return records for a different patient, depending on the server's header-precedence behaviour (first-wins vs. last-wins).
  • Authorization bypass: an injected Authorization: Bearer <token> header could bypass the normal authentication flow on the ePA server's inner HTTP handler.
  • Session-wide poisoning: a malicious USER_AGENT environment variable injects headers into every inner HTTP request for the session lifetime.
  • Request smuggling: in send_authcode_sc(), the Content-Length header is computed from len(body_json) before any injection occurs, creating an HTTP-desync surface when injected data shifts the header/body boundary.
  • If gematik: VAU Handshake Performs Only 2 of 6 Required Server-Key Checks or fbeta: VAU Server Authentication Bypass via Circular Certificate Trust is also unmitigated, a MITM owns the inner-VAU plaintext without DiGA-user privilege; the injected x-insurantid arrives by relay, not by authenticated request, and PR collapses to N.

Mitigation

Update fbeta ePA3-Service-OpenSource to 1.3.0 or later. The fix sanitises every caller-controlled value before interpolation (rejecting any string that contains \r or \n) and validates insurantId against the KVNR format ^[A-Z]\d{9}$ at the library boundary. The longer-term remediation in the PR replaces manual string assembly for inner HTTP requests with a library that provides header validation, correct Content-Length computation, and proper encoding.

Defender's Checklist

  • Update to fbeta ePA3-Service-OpenSource 1.3.0 or later.

    All versions before 1.3.0 are affected. The fix lives in pull request #11.

  • Validate KVNR at the DiGA boundary, not only in the library.

    The KVNR format ^[A-Z]\d{9}$ is strictly defined. Apply the regex at the earliest point the value crosses your trust boundary (form input, API request body), so an invalid KVNR fails before it can reach this library.

  • Audit your environment variables.

    The USER_AGENT env var was a poisoning vector: a value containing CRLF poisoned every request for the session lifetime. If your deployment reads any other env vars into inner-HTTP headers, validate them at startup, not at use time.

  • Don't hand-build HTTP.

    Where you control the code, replace manual inner_request += f"..." patterns with a library that produces a well-formed HTTP message. The bug here would not have been possible if header values had passed through any validation layer between the caller and the encryption step.

Severity Reasoning

AV:NThe injection vector is the KVNR field in a DiGA request, reachable over the network through the DiGA interface.AC:HRequires the ePA server's inner HTTP handler to honour a duplicated x-insurantid (or other smuggled header) in a way that returns or modifies records; not guaranteed in all server configurations. AC:H carries the uncertainty in the server-side step.PR:LAn authenticated DiGA user is required to supply the malicious KVNR through the DiGA interface.UI:NNo interaction by a second user is needed.S:UImpact is bounded to records reachable through the ePA server's inner HTTP handler.C:HWorst-case cross-patient data exposure if a duplicated x-insurantid causes another patient's records to be returned. Server-side header precedence is the uncertain step, carried by AC:H; fbeta's own GHSA scores the same vector.I:HWorst-case modification of inner-HTTP request semantics via authorization-bypass and request-smuggling surfaces. The dependent server-side behaviour is the uncertain step, carried by AC:H.A:NNo availability impact.

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.