CVE-2021-21369: Uncontrolled Resource Consumption (CWE-400)
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.
CWE description: 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.
CVSS v3 base score: 6.5
Published: 2021-03-09T18:15Z
Repository: https://github.com/hyperledger/besu
Language: Java
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 date: 2020-06-26 09:07:11 +1000
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>
Diff: diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java
index cac92cf33..92303aa90 100644
--- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java
+++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/authentication/AuthenticationUtils.java
@@ -35,31 +35,35 @@ public class AuthenticationUtils {
       final Optional<User> optionalUser,
       final JsonRpcMethod jsonRpcMethod) {
 
     AtomicBoolean foundMatchingPermission = new AtomicBoolean();
 
-    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);
-                }
-              });
+    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();
         }
       }
-    } else {
-      // no auth provider configured thus anything is permitted
-      foundMatchingPermission.set(true);
     }
 
     if (!foundMatchingPermission.get()) {
       LOG.trace("user NOT authorized : {}", jsonRpcMethod.getName());
     }
diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/JsonRpcMethod.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/JsonRpcMethod.java
index 24faf2386..20e3a521d 100644
--- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/JsonRpcMethod.java
+++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/JsonRpcMethod.java
@@ -21,11 +21,11 @@ import java.util.ArrayList;
 import java.util.List;
 
 public interface JsonRpcMethod {
 
   /**
-   * Standardised JSON-RPC method name.
+   * Standardized JSON-RPC method name.
    *
    * @return identification of the JSON-RPC method.
    */
   String getName();
 
@@ -36,17 +36,19 @@ public interface JsonRpcMethod {
    * @return output from applying the JSON-RPC method to the input.
    */
   JsonRpcResponse response(JsonRpcRequestContext request);
 
   /**
-   * The list of Permissions that correspond to this JSON-RPC method. e.g. [net/*, net/listening]
+   * The list of Permissions that correspond to this JSON-RPC method.
+   *
+   * <p>e.g. [*:*, net:*, net:listening]
    *
    * @return list of permissions that match this method.
    */
   default List<String> getPermissions() {
     List<String> permissions = new ArrayList<>();
     permissions.add("*:*");
-    permissions.add(this.getName().replace('_', ':'));
     permissions.add(this.getName().substring(0, this.getName().indexOf('_')) + ":*");
+    permissions.add(this.getName().replace('_', ':'));
     return permissions;
   };
 }
diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceLoginTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceLoginTest.java
index c06e9a030..a0ed2caf6 100644
--- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceLoginTest.java
+++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceLoginTest.java
@@ -391,11 +391,11 @@ public class JsonRpcHttpServiceLoginTest {
             // single eth/blockNumber method permitted
             Assertions.assertThat(
                     AuthenticationUtils.isPermitted(
                         service.authenticationService, Optional.of(user), ethBlockNumber))
                 .isTrue();
-            // eth/accounts not permitted
+            // eth/accounts NOT permitted
             assertThat(
                     AuthenticationUtils.isPermitted(
                         service.authenticationService, Optional.of(user), ethAccounts))
                 .isFalse();
             // allowed by web3/*
@@ -405,19 +405,79 @@ public class JsonRpcHttpServiceLoginTest {
                 .isTrue();
             assertThat(
                     AuthenticationUtils.isPermitted(
                         service.authenticationService, Optional.of(user), web3Sha3))
                 .isTrue();
-            // no net permissions
+            // NO net permissions
             assertThat(
                     AuthenticationUtils.isPermitted(
                         service.authenticationService, Optional.of(user), netVersion))
                 .isFalse();
           });
     }
   }
 
+  @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();
+          });
+    }
+  }
+
   @Test
   public void checkPermissionsWithEmptyUser() {
     final JsonRpcMethod ethAccounts = new EthAccounts();
 
     assertThat(
diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/NetListeningTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/NetListeningTest.java
index b7d1fe492..fc0e3e216 100644
--- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/NetListeningTest.java
+++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/NetListeningTest.java
@@ -67,11 +67,11 @@ public class NetListeningTest {
   }
 
   @Test
   public void getPermissions() {
     List<String> permissions = method.getPermissions();
-    assertThat(permissions).containsExactlyInAnyOrder("net:*", "net:listening", "*:*");
+    assertThat(permissions).containsExactly("*:*", "net:*", "net:listening");
   }
 
   private JsonRpcRequestContext netListeningRequest() {
     return new JsonRpcRequestContext(new JsonRpcRequest("2.0", "net_listening", new Object[] {}));
   }
diff --git a/ethereum/api/src/test/resources/JsonRpcHttpService/auth.toml b/ethereum/api/src/test/resources/JsonRpcHttpService/auth.toml
index 48e7bf1e5..3de16f86f 100644
--- a/ethereum/api/src/test/resources/JsonRpcHttpService/auth.toml
+++ b/ethereum/api/src/test/resources/JsonRpcHttpService/auth.toml
@@ -1,4 +1,8 @@
 [Users.user]
 password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC"
 permissions = ["fakePermission","eth:blockNumber","eth:subscribe","web3:*"]
 privacyPublicKey = "A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo="
+[Users.adminuser]
+password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC"
+permissions = ["*:*"]
+privacyPublicKey = "A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo="

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="