CVE-2021-21369 (Java, published 2021-03-09T18:15Z)
Description: value: Hyperledger Besu is an open-source, MainNet compatible, Ethereum client written in Java. In Besu before version 1.5.1 there is a denial-of-service vulnerability involving the HTTP JSON-RPC API service. If username and password authentication is enabled for the HTTP JSON-RPC API service, then prior to making any requests to an API endpoint the requestor must use the login endpoint to obtain a JSON web token (JWT) using their credentials. A single user can readily overload the login endpoint with invalid requests (incorrect password). As the supplied password is checked for validity on the main vertx event loop and takes a relatively long time this can cause the processing of other valid requests to fail. A valid username is required for this vulnerability to be exposed. This has been fixed in version 1.5.1.
Severity: MEDIUM (CVSS v3 base score: 6.5)
Weakness: CWE-400 \u2014 Uncontrolled Resource Consumption
The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
Repository: https://github.com/hyperledger/besu
Files changed:
ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java
ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/JsonRpcMethod.java
ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceLoginTest.java
ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/NetListeningTest.java
ethereum/api/src/test/resources/JsonRpcHttpService/auth.toml
Commit message: Improvements to authenticated JSON-RPC permissions checking (#1144)

* exit early if matching permission found; add test for *:* permission

Signed-off-by: Sally MacFarlane <sally.macfarlane@consensys.net>

Vulnerable code:
    if (authenticationService.isPresent()) {
      if (optionalUser.isPresent()) {
        User user = optionalUser.get();
        for (String perm : jsonRpcMethod.getPermissions()) {
          user.isAuthorized(
              perm,
              (authed) -> {
                if (authed.result()) {
                  LOG.trace(
                      "user {} authorized : {} via permission {}",
                      user,
                      jsonRpcMethod.getName(),
                      perm);
                  foundMatchingPermission.set(true);
                }
              });
    } else {
      // no auth provider configured thus anything is permitted
      foundMatchingPermission.set(true);
   * Standardised JSON-RPC method name.
   * The list of Permissions that correspond to this JSON-RPC method. e.g. [net/*, net/listening]
    permissions.add(this.getName().replace('_', ':'));
            // eth/accounts not permitted
            // no net permissions
    assertThat(permissions).containsExactlyInAnyOrder("net:*", "net:listening", "*:*");

Fixed code:
    if (authenticationService.isEmpty()) {
      // no auth provider configured thus anything is permitted
      return true;
    }

    if (optionalUser.isPresent()) {
      User user = optionalUser.get();
      for (String perm : jsonRpcMethod.getPermissions()) {
        user.isAuthorized(
            perm,
            (authed) -> {
              if (authed.result()) {
                LOG.trace(
                    "user {} authorized : {} via permission {}",
                    user,
                    jsonRpcMethod.getName(),
                    perm);
                foundMatchingPermission.set(true);
              }
            });
        // exit if a matching permission was found, no need to keep checking
        if (foundMatchingPermission.get()) {
          return foundMatchingPermission.get();
   * Standardized JSON-RPC method name.
   * The list of Permissions that correspond to this JSON-RPC method.
   *
   * <p>e.g. [*:*, net:*, net:listening]
    permissions.add(this.getName().replace('_', ':'));
            // eth/accounts NOT permitted
            // NO net permissions
  @Test
  public void checkJsonRpcMethodsAvailableWithGoodCredentialsAndAllPermissions()
      throws IOException {
    final RequestBody body =
        RequestBody.create(JSON, "{\"username\":\"adminuser\",\"password\":\"pegasys\"}");
    final Request request = new Request.Builder().post(body).url(baseUrl + "/login").build();
    try (final Response resp = client.newCall(request).execute()) {
      assertThat(resp.code()).isEqualTo(200);
      assertThat(resp.message()).isEqualTo("OK");
      assertThat(resp.body().contentType()).isNotNull();
      assertThat(resp.body().contentType().type()).isEqualTo("application");
      assertThat(resp.body().contentType().subtype()).isEqualTo("json");
      final String bodyString = resp.body().string();
      assertThat(bodyString).isNotNull();
      assertThat(bodyString).isNotBlank();

      final JsonObject respBody = new JsonObject(bodyString);
      final String token = respBody.getString("token");
      assertThat(token).isNotNull();

      final JsonRpcMethod ethAccounts = new EthAccounts();
      final JsonRpcMethod netVersion = new NetVersion(Optional.of(BigInteger.valueOf(123)));
      final JsonRpcMethod ethBlockNumber = new EthBlockNumber(blockchainQueries);
      final JsonRpcMethod web3Sha3 = new Web3Sha3();
      final JsonRpcMethod web3ClientVersion = new Web3ClientVersion("777");

      // adminuser has *:* permissions so everything should be allowed
      jwtAuth.authenticate(
          new JsonObject().put("jwt", token),
          (r) -> {
            assertThat(r.succeeded()).isTrue();
            final User user = r.result();
            // single eth/blockNumber method permitted
            Assertions.assertThat(
                    AuthenticationUtils.isPermitted(
                        service.authenticationService, Optional.of(user), ethBlockNumber))
                .isTrue();
            // eth/accounts IS permitted
            assertThat(
                    AuthenticationUtils.isPermitted(
                        service.authenticationService, Optional.of(user), ethAccounts))
                .isTrue();
            // allowed by *:*
            assertThat(
                    AuthenticationUtils.isPermitted(
                        service.authenticationService, Optional.of(user), web3ClientVersion))
                .isTrue();
            assertThat(
                    AuthenticationUtils.isPermitted(
                        service.authenticationService, Optional.of(user), web3Sha3))
                .isTrue();
            // YES net permissions
            assertThat(
                    AuthenticationUtils.isPermitted(
                        service.authenticationService, Optional.of(user), netVersion))
                .isTrue();
          });
    }
  }

    assertThat(permissions).containsExactly("*:*", "net:*", "net:listening");
[Users.adminuser]
password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC"
permissions = ["*:*"]
privacyPublicKey = "A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo="