From e1c02fe87bdb79818cd54404db8b38dd0a688b61 Mon Sep 17 00:00:00 2001 From: AKASHDEEP KAUR Date: Wed, 2 Sep 2026 18:25:38 +0000 Subject: [PATCH] Fix GCP Service Agent Token Theft vulnerability in connection testing and plugin validation Modified GCPUtils.loadServiceAccountCredentials to use ServiceAccountCredentials.fromStream() instead of GoogleCredentials.fromStream(). Previously, GoogleCredentials.fromStream() would implicitly parse various credential types. This allowed a malicious user to supply an external_account payload with a crafted credential_source (pointing to the GCP metadata server) and an external token_url, triggering an unintended Workload Identity Federation exchange and exfiltrating the Data Fusion Service Agent's token. By explicitly restricting the parser to ServiceAccountCredentials, external and non-service-account configurations now instantly throw an IOException prior to any external token fetches and effectively blocks the Privilege Escalation attack. Also added GCPUtilsTest.java to explicitly test that only service_account credential types can be loaded, securing regression coverage against external account injections. Fixes: b/501546932, b/501543027 --- .../io/cdap/plugin/gcp/common/GCPUtils.java | 2 +- .../cdap/plugin/gcp/common/GCPUtilsTest.java | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/test/java/io/cdap/plugin/gcp/common/GCPUtilsTest.java diff --git a/src/main/java/io/cdap/plugin/gcp/common/GCPUtils.java b/src/main/java/io/cdap/plugin/gcp/common/GCPUtils.java index 4ebae9bb00..a1ef45f6ef 100644 --- a/src/main/java/io/cdap/plugin/gcp/common/GCPUtils.java +++ b/src/main/java/io/cdap/plugin/gcp/common/GCPUtils.java @@ -118,7 +118,7 @@ public static GoogleCredentials loadServiceAccountCredentials(String serviceAcco boolean isServiceAccountFilePath) throws IOException { try (InputStream inputStream = openServiceAccount(serviceAccount, isServiceAccountFilePath)) { - return GoogleCredentials.fromStream(inputStream); + return ServiceAccountCredentials.fromStream(inputStream); } } diff --git a/src/test/java/io/cdap/plugin/gcp/common/GCPUtilsTest.java b/src/test/java/io/cdap/plugin/gcp/common/GCPUtilsTest.java new file mode 100644 index 0000000000..a0a35062fa --- /dev/null +++ b/src/test/java/io/cdap/plugin/gcp/common/GCPUtilsTest.java @@ -0,0 +1,67 @@ +/* + * Copyright © 2026 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package io.cdap.plugin.gcp.common; + +import com.google.auth.oauth2.GoogleCredentials; +import org.junit.Assert; +import org.junit.Test; +import java.io.IOException; + +public class GCPUtilsTest { + + @Test + public void testLoadServiceAccountCredentialsNegative() { + // Negative Scenario: type is set to "external_account" (simulating the external + // payload attack) + String externalAccountJson = "{\n" + + " \"type\": \"external_account\"\n" + + "}"; + + try { + GCPUtils.loadServiceAccountCredentials(externalAccountJson, false); + Assert.fail("Expected IOException when loading non-service-account credentials"); + } catch (IOException e) { + // Expected exception because the JSON does not represent a valid service + // account. + // ServiceAccountCredentials.fromStream throws a specific exception indicating + // the type mismatch. + Assert.assertNotNull("Exception message should indicate valid parse failure", e.getMessage()); + } + } + + @Test + public void testLoadServiceAccountCredentialsPositive() throws Exception { + java.security.KeyPairGenerator kpg = java.security.KeyPairGenerator.getInstance("RSA"); + kpg.initialize(1024); + java.security.KeyPair kp = kpg.generateKeyPair(); + String encodedKey = java.util.Base64.getEncoder().encodeToString(kp.getPrivate().getEncoded()); + String pemKey = "-----BEGIN PRIVATE KEY-----\\n" + encodedKey + "\\n-----END PRIVATE KEY-----\\n"; + String validServiceAccountJson = "{\n" + + " \"type\": \"service_account\",\n" + + " \"project_id\": \"test-project\",\n" + + " \"private_key_id\": \"dummy-id\",\n" + + " \"private_key\": \"" + pemKey + "\",\n" + + " \"client_email\": \"test@test-project.iam.gserviceaccount.com\",\n" + + " \"client_id\": \"12345\"\n" + + "}"; + + GoogleCredentials creds = GCPUtils.loadServiceAccountCredentials(validServiceAccountJson, false); + Assert.assertNotNull(creds); + Assert.assertTrue("Credentials should be an instance of ServiceAccountCredentials", + creds instanceof com.google.auth.oauth2.ServiceAccountCredentials); + } +}