CVE-2021-41130 (published 2021-10-07T19:15Z)

Extensible Service Proxy, a.k.a. ESP is a proxy which enables API management capabilities for JSON/REST or gRPC API services. ESPv1 can be configured to authenticate a JWT token. Its verified JWT claim is passed to the application by HTTP header "X-Endpoint-API-UserInfo", the application can use it to do authorization. But if there are two "X-Endpoint-API-UserInfo" headers from the client, ESPv1 only replaces the first one, the 2nd one will be passed to the application. An attacker can send two "X-Endpoint-API-UserInfo" headers, the second one with a fake JWT claim. Application may use the fake JWT claim to do the authorization. This impacts following ESPv1 usages: 1) Users have configured ESPv1 to do JWT authentication with Google ID Token as described in the referenced google endpoint document. 2) Users backend application is using the info in the "X-Endpoint-API-UserInfo" header to do the authorization. It has been fixed by v1.58.0. You need to patch it in the following ways: * If your docker image is using tag ":1", needs to re-start the container to pick up the new version. The tag ":1" will automatically point to the latest version. * If your docker image tag pings to a specific minor version, e.g. ":1.57". You need to update it to ":1.58" and re-start the container. There are no workaround for this issue.

Severity: MEDIUM
CVSS v2 base score: 4.9 (AV:N/AC:M/Au:S/C:P/I:P/A:N)
CVSS v3 base score: 5.4, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-290
cwe_name: Authentication Bypass by Spoofing
description: This attack-focused weakness is caused by incorrectly implemented authentication schemes that are subject to spoofing attacks.
extended_description: 
            
            
         
         

Fix information:
repo_url: https://github.com/cloudendpoints/esp
commit_details: repo_url: https://github.com/cloudendpoints/esp
author: Shuo Yang
author_date: 2021-08-30 22:05:31-07:00
msg: Remove X-Endpoint-API-UserInfo for all header occurrences. (#845)
repository: repo_url: https://github.com/cloudendpoints/esp
repo_name: cloudendpoints/esp
description: Extensible Service Proxy
repo_language: C++
owner: cloudendpoints
file_changes: filename: request.h
change_type: ModificationType.MODIFY
diff: @@ -74,7 +74,8 @@ class Request {
 
   // Adds a header to backend. If the header exists, overwrite its value
   virtual utils::Status AddHeaderToBackend(const std::string &key,
-                                           const std::string &value) = 0;
+                                           const std::string &value,
+                                           bool changeAllOccurrence) = 0;
 };
 
 }  // namespace api_manager

programming_language: C++
filename: check_auth.cc
change_type: ModificationType.MODIFY
diff: @@ -393,7 +393,7 @@ void AuthChecker::PassUserInfoOnSuccess() {
   char *base64_json_buf = auth::esp_base64_encode(
       json_buf, strlen(json_buf), true, false, true /*padding*/);
   context_->request()->AddHeaderToBackend(auth::kEndpointApiUserInfo,
-                                          base64_json_buf);
+                                          base64_json_buf, false);
   auth::esp_grpc_free(json_buf);
   auth::esp_grpc_free(base64_json_buf);
 

programming_language: C++
filename: check_auth_test.cc
change_type: ModificationType.MODIFY
diff: @@ -458,7 +458,7 @@ void CheckAuthTest::TestValidToken(const std::string &auth_token,
       }));
   std::cout << "need be replaced: " << user_info << std::endl;
   EXPECT_CALL(*raw_request_,
-              AddHeaderToBackend(kEndpointApiUserInfo, user_info))
+              AddHeaderToBackend(kEndpointApiUserInfo, user_info, false))
       .WillOnce(Return(utils::Status::OK));
 
   CheckAuth(context_, [](Status status) { ASSERT_TRUE(status.ok()); });
@@ -490,8 +490,8 @@ TEST_F(CheckAuthTest, TestOKAuth) {
       }));
   EXPECT_CALL(*raw_request_, SetAuthToken(kToken)).Times(1);
   EXPECT_CALL(*raw_env_, DoRunHTTPRequest(_)).Times(0);
-  EXPECT_CALL(*raw_request_,
-              AddHeaderToBackend(kEndpointApiUserInfo, kUserInfo_kSub_kIss))
+  EXPECT_CALL(*raw_request_, AddHeaderToBackend(kEndpointApiUserInfo,
+                                                kUserInfo_kSub_kIss, false))
       .WillOnce(Return(utils::Status::OK));
 
   CheckAuth(context_, [](Status status) { ASSERT_TRUE(status.ok()); });
@@ -513,8 +513,8 @@ TEST_F(CheckAuthTest, TestOKAuth) {
       }));
   EXPECT_CALL(*raw_request_, SetAuthToken(kToken2)).Times(1);
   EXPECT_CALL(*raw_env_, DoRunHTTPRequest(_)).Times(0);
-  EXPECT_CALL(*raw_request_,
-              AddHeaderToBackend(kEndpointApiUserInfo, kUserInfo_kSub2_kIss2))
+  EXPECT_CALL(*raw_request_, AddHeaderToBackend(kEndpointApiUserInfo,
+                                                kUserInfo_kSub2_kIss2, false))
       .WillOnce(Return(utils::Status::OK));
 
   CheckAuth(context_, [](Status status) { ASSERT_TRUE(status.ok()); });
@@ -605,8 +605,8 @@ TEST_F(CheckAuthTest, TestNoOpenId) {
         std::map<std::string, std::string> empty;
         req->OnComplete(Status::OK, std::move(empty), std::move(body));
       }));
-  EXPECT_CALL(*raw_request_,
-              AddHeaderToBackend(kEndpointApiUserInfo, kUserInfo_kSub_kIss2))
+  EXPECT_CALL(*raw_request_, AddHeaderToBackend(kEndpointApiUserInfo,
+                                                kUserInfo_kSub_kIss2, false))
       .WillOnce(Return(utils::Status::OK));
 
   CheckAuth(context_, [](Status status) { ASSERT_TRUE(status.ok()); });

programming_language: C++
filename: check_service_control.cc
change_type: ModificationType.MODIFY
diff: @@ -15,6 +15,7 @@
 ////////////////////////////////////////////////////////////////////////////////
 // includes should be ordered. This seems like a bug in clang-format?
 #include "src/api_manager/check_service_control.h"
+
 #include "google/protobuf/stubs/status.h"
 #include "src/api_manager/cloud_trace/cloud_trace.h"
 
@@ -85,8 +86,8 @@ void CheckServiceControl(std::shared_ptr<context::RequestContext> context,
 
         // update consumer_project_id to service context
         if (!info.consumer_project_id.empty()) {
-          context->request()->AddHeaderToBackend(kConsumerProjecId,
-                                                 info.consumer_project_id);
+          context->request()->AddHeaderToBackend(
+              kConsumerProjecId, info.consumer_project_id, false);
         }
 
         continuation(status);

programming_language: C++
filename: client_ip_extraction_test.cc
change_type: ModificationType.MODIFY
diff: @@ -117,8 +117,8 @@ class MockRequest : public Request {
     return false;
   }
 
-  MOCK_METHOD2(AddHeaderToBackend,
-               utils::Status(const std::string &, const std::string &));
+  MOCK_METHOD3(AddHeaderToBackend,
+               utils::Status(const std::string &, const std::string &, bool));
   MOCK_METHOD1(SetAuthToken, void(const std::string &));
   MOCK_METHOD0(GetFrontendProtocol,
                ::google::api_manager::protocol::Protocol());

programming_language: C++
filename: request_context.cc
change_type: ModificationType.MODIFY
diff: @@ -16,15 +16,17 @@
 //
 
 #include "src/api_manager/context/request_context.h"
-#include "google/api/backend.pb.h"
-#include "src/api_manager/auth/lib/json_util.h"
-#include "src/api_manager/utils/str_util.h"
 
 #include <uuid/uuid.h>
+
 #include <numeric>
 #include <sstream>
 #include <vector>
 
+#include "google/api/backend.pb.h"
+#include "src/api_manager/auth/lib/json_util.h"
+#include "src/api_manager/utils/str_util.h"
+
 using ::google::api_manager::auth::GetPrimitiveFieldValue;
 using ::google::api_manager::cloud_trace::HeaderType;
 using ::google::api_manager::utils::Status;
@@ -208,7 +210,7 @@ void RequestContext::ExtractApiKey() {
 }
 
 void RequestContext::SetApiKeyHeader() {
-  request_->AddHeaderToBackend(kDefaultApiKeyHeaderName, api_key_);
+  request_->AddHeaderToBackend(kDefaultApiKeyHeaderName, api_key_, false);
 }
 
 void RequestContext::CompleteCheck(Status status) {
@@ -434,7 +436,7 @@ void RequestContext::StartBackendSpanAndSetTraceContext() {
       cloud_trace()->header_type() == HeaderType::CLOUD_TRACE_CONTEXT
           ? kCloudTraceContextHeader
           : kGRpcTraceContextHeader,
-      trace_context_header);
+      trace_context_header, false);
   if (!status.ok()) {
     service_context()->env()->LogError(
         "Failed to set trace context header to backend.");
@@ -535,14 +537,14 @@ void RequestContext::AddInstanceIdentityToken() {
       std::string origin_auth_header;
       if (request()->FindHeader(kAuthorizationHeader, &origin_auth_header)) {
         Status status = request()->AddHeaderToBackend(
-            kXForwardedAuthorizationHeader, origin_auth_header);
+            kXForwardedAuthorizationHeader, origin_auth_header, false);
         if (!status.ok()) {
           service_context()->env()->LogError(
               "Failed to set X-Forwarded-Authorization header to backend.");
         }
       }
-      Status status = request()->AddHeaderToBackend(kAuthorizationHeader,
-                                                    kBearerPrefix + token);
+      Status status = request()->AddHeaderToBackend(
+          kAuthorizationHeader, kBearerPrefix + token, false);
       if (!status.ok()) {
         service_context()->env()->LogError(
             "Failed to set authorization header to backend.");

programming_language: C++
filename: mock_request.h
change_type: ModificationType.MODIFY
diff: @@ -25,8 +25,8 @@ class MockRequest : public Request {
  public:
   MOCK_METHOD2(FindQuery, bool(const std::string &, std::string *));
   MOCK_METHOD2(FindHeader, bool(const std::string &, std::string *));
-  MOCK_METHOD2(AddHeaderToBackend,
-               utils::Status(const std::string &, const std::string &));
+  MOCK_METHOD3(AddHeaderToBackend,
+               utils::Status(const std::string &, const std::string &, bool));
   MOCK_METHOD1(SetAuthToken, void(const std::string &));
   MOCK_METHOD0(GetRequestHTTPMethod, std::string());
   MOCK_METHOD0(GetQueryParameters, std::string());

programming_language: C++
filename: request_handler.cc
change_type: ModificationType.MODIFY
diff: @@ -43,7 +43,7 @@ RequestHandler::RequestHandler(
   if (context_->request()->FindHeader(
           google::api_manager::auth::kEndpointApiUserInfo, &buffer)) {
     context_->request()->AddHeaderToBackend(
-        google::api_manager::auth::kEndpointApiUserInfo, "");
+        google::api_manager::auth::kEndpointApiUserInfo, "", true);
   }
 }
 

programming_language: C++
filename: request.cc
change_type: ModificationType.MODIFY
diff: @@ -146,18 +146,21 @@ void NgxEspRequest::SetAuthToken(const std::string &auth_token) {
 }
 
 utils::Status NgxEspRequest::AddHeaderToBackend(const std::string &key,
-                                                const std::string &value) {
-  ngx_table_elt_t *h = nullptr;
+                                                const std::string &value,
+                                                bool changeAllOccurrence) {
+  std::vector<ngx_table_elt_t *> headers;
   for (auto &h_in : r_->headers_in) {
     if (key.size() == h_in.key.len &&
         strncasecmp(key.c_str(), reinterpret_cast<const char *>(h_in.key.data),
                     h_in.key.len) == 0) {
-      h = &h_in;
-      break;
+      headers.push_back(&h_in);
+      if (!changeAllOccurrence) {
+        break;
+      }
     }
   }
-  if (h == nullptr) {
-    h = reinterpret_cast<ngx_table_elt_t *>(
+  if (headers.empty()) {
+    ngx_table_elt_t *h = reinterpret_cast<ngx_table_elt_t *>(
         ngx_list_push(&r_->headers_in.headers));
     if (h == nullptr) {
       return utils::Status(Code::INTERNAL, "Out of memory");
@@ -172,14 +175,21 @@ utils::Status NgxEspRequest::AddHeaderToBackend(const std::string &key,
         h->lowcase_key,
         reinterpret_cast<u_char *>(const_cast<char *>(key.c_str())),
         key.size());
+    headers.push_back(h);
   }
 
-  if (ngx_str_copy_from_std(r_->pool, key, &h->key) != NGX_OK ||
-      ngx_str_copy_from_std(r_->pool, value, &h->value) != NGX_OK) {
-    return utils::Status(Code::INTERNAL, "Out of memory");
+  for (size_t i = 0; i < headers.size(); ++i) {
+    ngx_table_elt_t *it = headers[i];
+    if (ngx_str_copy_from_std(r_->pool, key, &it->key) != NGX_OK ||
+        ngx_str_copy_from_std(r_->pool, value, &it->value) != NGX_OK) {
+      return utils::Status(Code::INTERNAL, "Out of memory");
+    }
   }
-  ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r_->connection->log, 0,
-                 "updates header to backend: \"%V: %V\"", &h->key, &h->value);
+
+  ngx_log_debug3(
+      NGX_LOG_DEBUG_HTTP, r_->connection->log, 0,
+      "updates header to backend, changeAllOccurrence: '%t', \"%V: %V\"",
+      changeAllOccurrence, &h->key, &h->value);
   return utils::Status::OK;
 }
 

programming_language: C++
filename: request.h
change_type: ModificationType.MODIFY
diff: @@ -57,7 +57,8 @@ class NgxEspRequest : public Request {
 
   virtual void SetAuthToken(const std::string &auth_token);
   virtual utils::Status AddHeaderToBackend(const std::string &key,
-                                           const std::string &value);
+                                           const std::string &value,
+                                           bool changeAllOccurrence);
   virtual bool FindQuery(const std::string &name, std::string *query);
   virtual bool FindHeader(const std::string &name, std::string *header);
 

programming_language: C++
filename: ApiManager.pm
change_type: ModificationType.MODIFY
diff: @@ -456,6 +456,7 @@ sub read_http_stream {
       'path' => $path,
       'uri' => $uri,
       'headers' => \%headers,
+      'header_lines' => \@header_lines,
       'body' => $body
     };
 

programming_language: Perl
filename: auth_remove_user_info.t
change_type: ModificationType.MODIFY
diff: @@ -42,7 +42,7 @@ my $NginxPort = ApiManager::pick_port();
 my $BackendPort = ApiManager::pick_port();
 my $ServiceControlPort = ApiManager::pick_port();
 
-my $t = Test::Nginx->new()->has(qw/http proxy/)->plan(14);
+my $t = Test::Nginx->new()->has(qw/http proxy/)->plan(15);
 
 # Save service name in the service configuration protocol buffer file.
 
@@ -88,6 +88,7 @@ my $response = ApiManager::http($NginxPort,<<'EOF');
 GET /shelves?key=this-is-an-api-key HTTP/1.0
 Host: localhost
 x-endpoint-api-userinfo: Should be removed
+x-endpoint-api-userinfo: Multiple userinfo should all be removed
 
 EOF
 
@@ -114,6 +115,14 @@ is($r->{uri}, '/shelves?key=this-is-an-api-key', 'Backend uri was /shelves');
 is($r->{headers}->{host}, "127.0.0.1:${BackendPort}", 'Host header was set');
 is($r->{headers}->{'x-endpoint-api-userinfo'}, '',
     'X-Endpoint-API-UserInfo should be removed from the request headers');
+my $arr = $r->{header_lines};
+my @userinfo_list = ();
+
+foreach (@$arr) {
+  push(@userinfo_list, $_) if index($_, 'X-Endpoint-API-UserInfo') != -1;
+}
+is(join(",", @userinfo_list), 'X-Endpoint-API-UserInfo: ,X-Endpoint-API-UserInfo: ',
+   "Multiple userinfo should all be removed");
 
 @requests = ApiManager::read_http_stream($t, 'servicecontrol.log');
 is(scalar @requests, 1, 'Service control received one request');

programming_language: Perl