November 19, 2020

Securing the fight against COVID-19 through open source

Alvaro Muñoz
This blog describes a security vulnerability in the infrastructure that supports Germany’s COVID-19 contact tracing efforts. The mobile (Android/iOS) apps are not affected by the vulnerability and do not collect and/or transmit any personal data other than the device’s IP address. The infrastructure takes active measures to disassociate true positives from client IP addresses.

The GitHub Security Lab continually scans open source projects for vulnerabilities. Using the same CodeQL technology that also drives GitHub code scanning, we hunt for bug patterns that we know lead to exploitable vulnerabilities. We often find and fix vulnerabilities in high-profile projects, and in many cases these discoveries have a positive impact on these projects, on the security of the users’ data, and in some cases, on human lives. This is one of those cases.

This is the story of how a CodeQL query enabled us to find and help fix a Remote Code Execution (RCE) vulnerability in Germany’s COVID-19 contact tracing infrastructure, which powers the German’s Corona-Warn-App for Android and iOS.

In the global fight to slow the spread of COVID-19, many local and country-wide level efforts have turned to track and trace mobile applications. These applications maintain a log of other participating devices that have come within bluetooth range by receiving anonymous identifiers. Such identifiers are generated and broadcast via a privacy-preserving cryptographic protocol. An example of such a protocol is Google and Apple’s Privacy-Preserving Contact Tracing.

For such contact tracing apps to be successful, they need adoption — a lot of it. Since these efforts are generally driven by local and national governments, most users want to be ensured that their governments will not be able to track their location or log any other identifiable information as a result of their participation.The best way to achieve such trust is through the use of open and transparent protocols, software implementations, and back-end infrastructure. Recognizing this fact, many COVID-19 contact tracing applications and their associated infrastructure are open sourced. This is a great example of using open source transparency as a vehicle to establish trust.

A few months ago, the GitHub Security Lab researched the impact of insecure Java Bean Validation API use on Java application security. As the result of this research, we identified Java Bean Validation vulnerability patterns that led to RCE and scanned several open source repositories. As a result, we were able to find and fix a variety of RCE vulnerabilities in collaboration with the affected project maintainers.

Recently, while reviewing an additional set of Java Bean Validation scan results for projects that had been open sourced on GitHub, one of the findings immediately caught my attention. There appeared to be a pre-authentication RCE vulnerability in Corona-Warn-App Server, which drives Germany’s COVID-19 contact tracing application infrastructure. This vulnerability had the potential to affect the integrity of Germany’s COVID-19 response and as such warranted an immediate response from our team.

In this post we will discuss the details of the vulnerability and its potential impact, as well as detail the collaboration with SAP to mitigate this specific problem and share advice on how to prevent future instances of the same bug class from appearing in your own projects.

The affected project

The affected project in question was the Corona-Warn-App Server. The aim of this project is to develop the official contact tracing application for Germany based on the exposure notification API from Apple and Google. The application uses Bluetooth Low Energy (BLE) to exchange encrypted anonymous tokens with other nearby participating mobile devices. The proximity log is stored locally and not in any central location, to ensure that only a minimum of information is shared to any operating government or third party.

The German government commissioned SAP and Deutsche Telekom to develop and host the Corona-Warn-App as open source software, to encourage all interested parties to contribute and become part of its developer community. By open sourcing their application they provided transparency to their end users in terms of what the application is actually doing on their devices as well as allowed people to independently review and verify its security.

The vulnerability

The vulnerable code is located in the Submission Service, which is a micro service developed on top of the Spring Boot framework. The main public entry point is the SubmissionController, which handles the submission of diagnosis keys (more about those later). When a new diagnosis key is sent to the https://server/version/v1/diagnosis-keys endpoint, the request body is unmarshalled and then validated by the ValidSubmissionPayload validator as we can see in the following code snippet:

  /**
   * Handles diagnosis key submission requests.
   *
   * @param exposureKeys The unmarshalled protocol buffers submission payload.
   * @param tan          A tan for diagnosis verification.
   * @return An empty response body.
   */
  @PostMapping(value = SUBMISSION_ROUTE, headers = {"cwa-fake=0"})
  @Timed(description = "Time spent handling submission.")
  public DeferredResult<ResponseEntity<Void>> submitDiagnosisKey(
      @ValidSubmissionPayload @RequestBody SubmissionPayload exposureKeys,
      @RequestHeader("cwa-authorization") String tan) {
    submissionMonitor.incrementRequestCounter();
    submissionMonitor.incrementRealRequestCounter();
    return buildRealDeferredResult(exposureKeys, tan);
  }

The ValidSubmissionPayload validator performs a number of checks to verify that the received key is valid and conforms to the accepted key policy:

    @Override
    public boolean isValid(SubmissionPayload submissionPayload, ConstraintValidatorContext validatorContext) {
      List<TemporaryExposureKey> exposureKeys = submissionPayload.getKeysList();
      validatorContext.disableDefaultConstraintViolation();
      return checkStartIntervalNumberIsAtMidNight(exposureKeys, validatorContext)
          && checkKeyCollectionSize(exposureKeys, validatorContext)
          && checkOriginCountryIsValid(submissionPayload, validatorContext)
          && checkVisitedCountriesAreValid(submissionPayload, validatorContext)
          && checkRequiredFieldsNotMissing(exposureKeys, validatorContext)
          && checkTransmissionRiskLevelIsAcceptable(exposureKeys, validatorContext)
          && checkDaysSinceOnsetOfSymptomsIsInRange(exposureKeys, validatorContext);
    }

As we can see above, the submission validator checks the following against the provided set of exposure keys:

  • The start interval number values must be at midnight (00:00 UTC).
  • There must not be more than the allowed maximum number of keys in a single payload.
  • The country of origin can be missing or the provided value must be a supported country.
  • The visited countries can be missing or the provided values must be supported.
  • Any mandatory required fields are present.
  • The transmission risk levels are acceptable.
  • The number of days since the onset of symptoms is in a range that requires broadcast.

As explained in our previous research on Java Bean Validation vulnerabilities, if any validated bean properties flow into a custom constraint violation template, the attacker-controlled property will be evaluated as an Expressional Language (EL) expression, which allows for the evaluation of arbitrary Java code.

This is the case for two of the validation checks on the user supplied submission payload: checkVisitedCountriesAreValid and checkOriginCountryIsValid.

In the case of checkVisitedCountriesAreValid, each of the countries in the visitedCountriesList property will be checked against an allow list, and those not present will be reflected in the validation message passed to addViolation():

    private boolean checkVisitedCountriesAreValid(SubmissionPayload submissionPayload,
        ConstraintValidatorContext validatorContext) {
      if (submissionPayload.getVisitedCountriesList().isEmpty()) {
        return true;
      }
      Collection<String> invalidVisitedCountries = submissionPayload.getVisitedCountriesList().stream()
          .filter(not(supportedCountries::contains)).collect(toList());
      if (!invalidVisitedCountries.isEmpty()) {
        invalidVisitedCountries.forEach(country -> addViolation(validatorContext,
            "[" + country + "]: Visited country is not part of the supported countries list"));
      }
      return invalidVisitedCountries.isEmpty();
    }

In the case of checkOriginCountryIsValid, the origin property will be checked against a list of supported countries and if not supported, it will be included in the violation message passed to addViolation():

    private boolean checkOriginCountryIsValid(SubmissionPayload submissionPayload,
        ConstraintValidatorContext validatorContext) {
      String originCountry = submissionPayload.getOrigin();
      if (submissionPayload.hasOrigin() && !originCountry.isEmpty()
          && !supportedCountries.contains(originCountry)) {
        addViolation(validatorContext, String.format(
            "Origin country %s is not part of the supported countries list", originCountry));
        return false;
      }
      return true;
    }

The addViolation method is defined as:

    private void addViolation(ConstraintValidatorContext validatorContext, String message) {
      validatorContext.buildConstraintViolationWithTemplate(message).addConstraintViolation();
    }

As you can see, the user-controlled violation messages flow into buildConstraintViolationWithTemplate(message) and therefore they will be treated as a template rather than as literal messages. The main difference is that, by default, templates allow for the interpolation of EL expressions delimited by ${}.

As stated in the official Corona-Warn-App documentation:

The CWA Server exposes only one endpoint – the submission endpoint. The endpoint is public (unauthenticated), and authorization for calls is granted to users who are passing a valid TAN. The TAN verification cannot be done on CWA Server, but the task is delegated to the verification server (see Verification Server chapter in Integration with other Systems).

This means that the vulnerability is located in a public and unauthenticated endpoint. We can further verify this by checking the server's Spring Security policy:

  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .mvcMatchers(HttpMethod.GET, HEALTH_ROUTE, PROMETHEUS_ROUTE, READINESS_ROUTE, LIVENESS_ROUTE).permitAll()
        .mvcMatchers(HttpMethod.POST, SUBMISSION_ROUTE).permitAll()
        .anyRequest().denyAll()
        .and().csrf().disable();
    http.headers().contentSecurityPolicy("default-src 'self'");
  }

Any POST requests sent to the Submission endpoint are allowed by default and require no further authorization or authentication.

In practice, and as stated by the official documentation mentioned above, a user cannot send an invalid diagnosis key since the incorrect TAN (Transaction Authentication Number) will be rejected by the verification service.

But, unfortunately, the Remote Code Execution (RCE) happens before the TAN is actually verified. Even if the TAN were verified before, an attacker with a valid positive COVID test result would be able to trigger the RCE. To understand this trigger flow and the impact of this vulnerability, we need to dig a little deeper into how the contact tracing protocol works and how its backend is architected.

Privacy-preserving contact tracing in a nutshell

The German COVID-19 exposure notification app, like most COVID-19 contact tracing apps, is based on a decentralized approach inspired by DP-3T (Decentralized Privacy-Preserving Proximity Tracing) and based on the aforementioned Privacy-Preserving Contact Tracing specifications by Apple and Google.

In a nutshell, any device running the contact tracing app will generate a daily Temporary Exposure Key (TEK). This TEK is unique and private to the device, and a new one is generated every 24 hours. From this TEK a number of additional keys are derived in a public and known way, which in turn are used to generate a broadcast payload that contains a so-called Rolling Proximity Identifier (RPI) as well as Associated Encrypted Metadata. Every few minutes, the contact tracing App will broadcast these proximity identifier payloads to any nearby devices via BLE. The receiving devices will then store the received broadcast payloads that contain the proximity identifiers, with the date and time when they were received. All received payloads are stored locally on the device for 14 days.

If and when a person using the app develops COVID-19 symptoms and tests positive, they will be able to submit the result of their test to the CWA Server. Along with their positive result, they then upload the set of TEKs that their device generated during the days they were contagious as well as their associated interval numbers. This subset of TEKs and their associated interval numbers is referred to as the Diagnosis Keys. The server will store the Diagnosis Keys in a completely anonymous manner with no information about who actually submitted them.

Other devices will periodically download a signed list of Diagnosis Keys associated to COVID-positive users which is broadcast by the server. Since the Diagnosis Keys contain all the TEKs and interval numbers needed to derive the COVID-positive user’s Rolling Proximity Identifiers as well as decrypt the Associated Encrypted Metadata for any locally stored broadcast payloads that match a derived RPI, the local device can then determine if they were in fact exposed to any user that marked themselves as positive.

A nice comic explaining contact tracing can be found here and is available on GitHub.

Backend architecture

Since we are already familiar with the part that the mobile applications play in the overall architecture, let's take a closer look at some of the other moving parts:

high level diagram

Diagram from the open source repository

  • Corona-Warn-App Server: Exposes the submission endpoint (public and unauthenticated) where infected users will upload their diagnosis keys. To prevent spam, a unique passcode (the TAN) provided by the doctor or test center is also uploaded, and this will be used to check that the diagnosis keys are legitimate.
  • Verification Server: Helps validate whether upload requests from the mobile app are valid or not. It is not exposed to end users.
  • Portal Server: Used by health authorities and hotlines to generate teleTANs, which are used by the mobile app users to upload their diagnostic keys. It is not exposed to end users.

Impact

The vulnerability is accessible through the CWA Server's submission service, which is publically available and, as mentioned before, requires no further authentication or authorization. RCE can be achieved during request validation and this enables attackers to run arbitrary Java code or system commands on the CWA Server.

In order to verify the exploitability of this vulnerability, we deployed a CWA Server in a development environment and crafted a simple exploit that would leak environment variables, including database credentials, to our own controlled server. At this point, the GitHub Security Lab reported the issue to SAP through the SAP Trust Center. We did not perform any live exploitation of the vulnerability on the production CWA Server, which could be additionally hardened.

Addressing the vulnerability

The initial fix was to prevent the injection by not including user-controlled bean properties into 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 perfectly valid but does not prevent similar vulnerabilities from being introduced in the future.

Static analysis, and specifically CodeQL-based GitHub code scanning, can help prevent the (re)introduction of vulnerabilities. Since the initial research, the GitHub Security Lab wrote a CodeQL query to detect and report the Insecure Java Bean Validation vulnerability class. Initially this query was only included in CodeQL’s experimental query set. However, after proving its accuracy with CWA and other projects, it has since graduated to the default LGTM.com and GitHub code scanning query set. That means that any Java project on GitHub that has code scanning enabled is now receiving Java Bean Validation vulnerability checks which will help prevent the (re)introduction of these vulnerabilities.

After a few days later, CWA’s initial vulnerability fix was replaced by a more secure by-default one:

+  /**
+   * 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;
+  }

In this new version of the fix the EL interpolation is disabled fully by only enabling parameter interpolation, as recommended in our previous Java Bean Validation blog post.

Affected forks

To our current knowledge, the only CWA fork that is in use by another country is with the Belgian Covid-Be-App. However, they forked CWA before the Java Bean Validation vulnerability was introduced and therefore it is not vulnerable to the issue. For other countries that may have forked CWA privately and/or anonymously, we recommend that those projects also apply the fixes mentioned above.

Conclusion

Open source software empowers users to help secure critical infrastructure. But as bug hunters, we need all the help we can get. Insecure Java Bean Validation has resulted in many RCE vulnerabilities across a wide range of high-profile and high-impact Java applications in the past few years. By capturing the essence of these vulnerability patterns as CodeQL queries and making them available for all GitHub users through code scanning, we hope to help scale bug hunting efforts across the open source software community, and we hope it inspires you to do the same!

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.