Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/java/io/cdap/plugin/gcp/common/GCPUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing from GoogleCredentials.fromStream() to ServiceAccountCredentials.fromStream() restricts your application to accepting only traditional, long-lived service account key JSON files.

The primary impacts across your application include:

  • Breaks Workload Identity Federation (external_account): Workloads authenticating from AWS, Azure, GitHub Actions, or on-premises environments via keyless federation will crash with an IOException or IllegalArgumentException because an external_account JSON cannot be parsed by ServiceAccountCredentials.
  • Breaks Local Developer Workflows (authorized_user): Developers using credential files generated by gcloud auth application-default login or user OAuth flows will no longer be able to supply those files to your code.
  • Blocks Impersonated Account Files (impersonated_service_account): Configuration files designed to dynamically impersonate a service account without possessing its private key will fail to load.
  • Enforces Security Anti-Patterns: It forces consumers of your application or plugin to create, download, store, and rotate static service account private keys ("type": "service_account"), increasing the risk of leaked credentials.
  • API Incompatibility / Code Changes: Any calling code expecting the base GoogleCredentials type may need adjustments if it relies on polymorphic credential handling, though ServiceAccountCredentials is assignable to GoogleCredentials.
  • Gains Direct Access to Service Account Methods: On the positive side, you gain compile-time access to service account-specific methods on the returned object without casting—such as .createDelegated(userEmail) for Google Workspace Domain-Wide Delegation, .getClientEmail(), and .getPrivateKey().

Would it be fine?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To remediate you may consider implementing stricter input validation for Google Cloud connector configurations.

Example:

  • Implement Stricter Input Validation: Add validation checks in the backend handlers (such as ConnectionHandler.java in CDAP and individual plugin connectors like GCSConnector.java) for the serviceAccountJSON payload.
  • Restrict Credential Type Properties: Do not allow the credential_source URL to point to the local metadata server (http://metadata.google.internal/computeMetadata/v1/...) or internal loopback IP addresses when testing connections with external accounts.
  • Validate Token URLs: Allowlist or validate the token_url domain to ensure it points to legitimate identity providers, rather than arbitrary attacker-controlled destinations.
  • Audit All Google Connectors: Ensure the validation applies systemically across all Google Cloud connectors (BigQuery, GCS, Spanner, Bigtable, Pub/Sub, etc.) as the vulnerability may be present in multiple plugin configurations.

}
}

Expand Down
67 changes: 67 additions & 0 deletions src/test/java/io/cdap/plugin/gcp/common/GCPUtilsTest.java
Original file line number Diff line number Diff line change
@@ -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" +
"}";
Comment thread
akkaur marked this conversation as resolved.

GoogleCredentials creds = GCPUtils.loadServiceAccountCredentials(validServiceAccountJson, false);
Assert.assertNotNull(creds);
Comment thread
akkaur marked this conversation as resolved.
Assert.assertTrue("Credentials should be an instance of ServiceAccountCredentials",
creds instanceof com.google.auth.oauth2.ServiceAccountCredentials);
}
}
Loading