CVE-2021-21369 (published 2021-03-09T18:15Z)

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 v2 base score: 4.0 (AV:N/AC:L/Au:S/C:N/I:N/A:P)
CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-400
cwe_name: Uncontrolled Resource Consumption
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.
extended_description: 
            Limited resources include memory, file system storage, database connection pool entries, and CPU. If an attacker can trigger the allocation of these limited resources, but the number or size of the resources is not controlled, then the attacker could cause a denial of service that consumes all available resources. This would prevent valid users from accessing the product, and it could potentially have an impact on the surrounding environment. For example, a memory exhaustion attack against an application could slow down the application as well as its host operating system.
            There are at least three distinct scenarios which can commonly lead to resource exhaustion:
               
                  Lack of throttling for the number of allocated resources
                  Losing all references to a resource before reaching the shutdown stage
                  Not closing/returning a resource after processing
               
            Resource exhaustion problems are often result due to an incorrect implementation of the following situations:
               
                  Error conditions and other exceptional circumstances.
                  Confusion over which part of the program is responsible for releasing the resource.
               
         
         

Fix information:
repo_url: https://github.com/hyperledger/besu
commit_details: repo_url: https://github.com/hyperledger/besu
author: Sally MacFarlane
author_date: 2020-06-26 09:07:11+10:00
msg: 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>
repository: repo_url: https://github.com/hyperledger/besu
repo_name: hyperledger/besu
description: An enterprise-grade Java-based, Apache 2.0 licensed Ethereum client https://wiki.hyperledger.org/display/besu
repo_language: Java
owner: hyperledger
file_changes: filename: AuthenticationUtils.java
change_type: ModificationType.MODIFY
diff: @@ -37,27 +37,31 @@ public class AuthenticationUtils {
 
     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()) {

programming_language: Java
filename: JsonRpcMethod.java
change_type: ModificationType.MODIFY
diff: @@ -23,7 +23,7 @@ import java.util.List;
 public interface JsonRpcMethod {
 
   /**
-   * Standardised JSON-RPC method name.
+   * Standardized JSON-RPC method name.
    *
    * @return identification of the JSON-RPC method.
    */
@@ -38,15 +38,17 @@ public interface JsonRpcMethod {
   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;
   };
 }

programming_language: Java
filename: JsonRpcHttpServiceLoginTest.java
change_type: ModificationType.MODIFY
diff: @@ -393,7 +393,7 @@ public class JsonRpcHttpServiceLoginTest {
                     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))
@@ -407,7 +407,7 @@ public class JsonRpcHttpServiceLoginTest {
                     AuthenticationUtils.isPermitted(
                         service.authenticationService, Optional.of(user), web3Sha3))
                 .isTrue();
-            // no net permissions
+            // NO net permissions
             assertThat(
                     AuthenticationUtils.isPermitted(
                         service.authenticationService, Optional.of(user), netVersion))
@@ -416,6 +416,66 @@ public class JsonRpcHttpServiceLoginTest {
     }
   }
 
+  @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();

programming_language: Java
filename: NetListeningTest.java
change_type: ModificationType.MODIFY
diff: @@ -69,7 +69,7 @@ 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() {

programming_language: Java
filename: auth.toml
change_type: ModificationType.MODIFY
diff: @@ -2,3 +2,7 @@
 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="

programming_language: Ruby