A CodeQL catch in Germany’s contact tracing backend

The GitHub Security Lab continuously scans open source repositories with CodeQL, hunting for bug patterns known to lead to exploitable vulnerabilities. One such scan recently flagged a pre-authentication Remote Code Execution (RCE) vulnerability in the Corona-Warn-App Server, the backend for Germany’s official COVID-19 contact tracing application. Given the project’s role in a national health response, the finding triggered immediate coordinated action with SAP, which develops and hosts the app alongside Deutsche Telekom.

The vulnerability stemmed from insecure use of Java Bean Validation—a pattern the Security Lab had previously catalogued in its research on Bean Validation RCE—and illustrates how an otherwise well-intentioned open source security posture can still harbor subtle, high-impact flaws.

The target: an open, public submission service

The Corona-Warn-App Server is a Spring Boot-based backend implementing a decentralized contact tracing design similar to DP-3T and the Apple/Google Exposure Notification API. Participating devices broadcast anonymous identifiers over Bluetooth Low Energy; those identifiers are logged locally and never stored centrally. Infected users upload their Temporary Exposure Keys (TEKs) as diagnosis keys via a single public, unauthenticated submission endpoint—the system’s only externally exposed surface—after obtaining a Transaction Authentication Number (TAN) from health authorities.

As documented by the project, authorization for submission relies on TAN verification delegated to a separate Verification Server, which is not exposed to end users. The portal server, used by health authorities for teleTAN generation, is likewise internal. That leaves the submission controller’s POST endpoint, which any anonymous user can reach, as the front door to the system.

The flaw: user input flowing into EL templates

The vulnerable code sat in the submission micro service’s input validation layer. When a diagnosis key payload arrives at /version/v1/diagnosis-keys, it is unmarshalled and passed to the ValidSubmissionPayload constraint validator, which enforces a policy on the submitted keys: start intervals at midnight UTC, no more than the maximum allowed keys, allowed origin and visited countries, presence of mandatory fields, acceptable risk levels, and reasonable symptom onset ranges.

Two checks—checkVisitedCountriesAreValid and checkOriginCountryIsValid—failed to handle user-supplied country values safely. In each case, a supplied country not on the allow list was concatenated directly into a violation message and handed to addViolation(), which passes it into buildConstraintViolationWithTemplate(message). Bean Validation treats such inputs as templates, not literal strings, meaning any ${...} expression embedded by an attacker gets evaluated as an Expression Language (EL) expression, allowing arbitrary Java code execution.

The end-to-end path was alarmingly direct. The submission endpoint is public and unauthenticated; the official documentation itself notes that any user passing a valid TAN is authorized. Although a malicious user without a valid TAN would normally be rejected by the Verification Server, the RCE trigger occurred before TAN validation. Even worse, an attacker who has tested positive and holds a legitimate TAN—perhaps a disgruntled or coerced individual—could trigger the vulnerability without any additional authentication barrier.

Why the decentralized design amplifies the risk

To grasp why an RCE on the submission endpoint is so critical, it helps to trace the contact tracing flow. A device generates a daily Temporary Exposure Key (TEK) and derives rolling proximity identifiers and encrypted metadata from it, broadcasting those via BLE every few minutes. Receiving devices store these broadcast payloads locally for 14 days. If a user tests positive, they submit their diagnosis keys—the TEKs and interval numbers from their contagious period—to the server. Other devices periodically pull a signed list of such keys and locally determine matches against their stored broadcast payloads.

The server also periodically downloads signed diagnosis key lists that can then be served to clients, making the diagnosis key database a central dependency in the privacy model. The server stores submission data anonymously, but an RCE would compromise the integrity of the key distribution channels and consequently Germany’s exposure notification system.

Collaboration and lessons

Following the finding, GitHub’s Security Lab coordinated with the project maintainers to mitigate the vulnerability promptly. The specific fixes and deployment timeline are documented in the project’s public repositories.

This case underscores two broader points. First, open source transparency provides a genuine security benefit: the very code that is independently inspectable is the code whose defects can be found and patched before they are exploited at scale. Second, the Bean Validation pattern that caused this flaw is not unique to a single project—it is a recurring bug class. Specifically, if an application passes attacker-influenced properties into a buildConstraintViolationWithTemplate() call without escaping, any framework that resolves EL expressions will interpret them as code.

Developers should treat any user-supplied content that flows into a validation message template as an injection risk. Where template interpolation is needed, apply strict allow lists and validate input before ever building a message string. For teams relying on Spring Boot or other Java frameworks that support Bean Validation, reviewing custom constraint validators for such patterns—and running CodeQL scans over their codebases—is a straightforward first step toward hardening apps against this entire class of vulnerabilities.

Attack Impact and Proof of Concept

The root cause of this vulnerability is a remote code execution (RCE) flaw in the CWA Server. Because the project depends on GitHub-hosted infrastructure and open source components, developers must remain vigilant about security risks originating in their dependencies. In this case, the vulnerability is accessible through the CWA Server’s submission service, which is publicly available and requires no further authentication or authorization. During request validation, an attacker can achieve RCE and execute arbitrary Java code or system commands on the server.

To verify that the vulnerability was exploitable, the research team deployed a CWA Server in an isolated development environment and crafted a simple exploit. This exploit leaked environment variables—including database credentials—to an attacker-controlled server. No live exploitation was performed against the production CWA Server, which could have additional hardening controls in place. The issue was reported to SAP through the SAP Trust Center.

The Patch and its Evolution

The initial fix targeted the root cause directly by preventing the injection. User-controlled bean properties were no longer included in violation message templates:

-        addViolation(validatorContext, String.format(
-            "Origin country %s is not part of the supported countries list", originCountry));
+        addViolation(validatorContext,
+            "Key contains origin country which is not part of the supported countries list");

and

-            "[" + country + "]: Visited country is not part of the supported countries list"));
+            "Key contains visited country which is not part of the supported countries list"));

This fix is valid, but it does not prevent similar flaws from being introduced in future code changes. To address that gap, the GitHub Security Lab developed a CodeQL query specifically designed to detect the Insecure Java Bean Validation vulnerability class. Initially, the query was part of CodeQL’s experimental set. After validating its accuracy on the CWA project and other codebases, the query graduated to the default query set within CodeQL and GitHub code scanning. Java projects on GitHub that enable code scanning now receive these security checks automatically.

Several days after the initial patch, the CWA developers replaced it with a more secure-by-default approach:

+  /**
+   * Validation factory bean is configured here because its message interpolation mechanism
+   * is considered a potential threat if enabled.
+   */
+  @Bean
+  public static LocalValidatorFactoryBean defaultValidator() {
+    LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
+    factoryBean.setMessageInterpolator(new ParameterMessageInterpolator());
+    return factoryBean;
+  }

The revised version fully disables Expression Language interpolation by only enabling parameter interpolation. This follows the recommendation in the original Java Bean Validation research and limits the attack surface more comprehensively than the first patch.

Project Forks and Similar Deployments

The primary known fork of the CWA project is the Belgian Covid-Be-App. That codebase was forked before the introduction of the vulnerable code, so it is not affected by this issue. For other countries that may have forked CWA privately or anonymously, the recommendation is to apply the same fixes described above to ensure their instances are also protected.

Disclosure Timeline

  • 10/21/2020: Reported through SAP Trust Center.
  • 10/22/2020: Issue reception is acknowledged.
  • 10/23/2020: Issue is fixed in public repo.
  • 10/28/2020: SAP confirms that the issue is fixed in release 1.5.1 which was deployed on 10/27/2020. SAP also informs GitHub Security Lab that Bundesamt für Sicherheit in der Informationstechnik (BSI) is currently testing the fix and asks to keep the issue confidential till BSI has done their tests and has confirmed that the fix is okay.
  • 11/01/2020: A more robust fix is merged.
  • 11/09/2020: SAP reports back to GitHub Security Lab that BSI has confirmed the fix.