Skip to content

Commit 89ff2e1

Browse files
quartzmocodyoss
authored andcommitted
google: add safer credentials JSON loading options.
Add safer credentials JSON loading options in `google` package. Adds `CredentialsFromJSONWithType` and `CredentialsFromJSONWithTypeAndParams` to mitigate a security vulnerability where credential configurations from untrusted sources could be used without validation. These new functions require the credential type to be explicitly specified. Deprecates the less safe `CredentialsFromJSON` and `CredentialsFromJSONWithParams` functions. Change-Id: I27848b5ebd2dff76d0397cdc08908d680c0ccd69 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/732440 Reviewed-by: Seth Hollyman <shollyman@google.com> Reviewed-by: Cody Oss <codyoss@google.com> Reviewed-by: Sai Sunder Srinivasan <saisunder@google.com> TryBot-Bypass: Cody Oss <codyoss@google.com>
1 parent acc3815 commit 89ff2e1

File tree

6 files changed

+218
-11
lines changed

6 files changed

+218
-11
lines changed

google/default.go

Lines changed: 121 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,43 @@ func (params CredentialsParams) deepCopy() CredentialsParams {
153153
return paramsCopy
154154
}
155155

156+
// CredentialsType specifies the type of JSON credentials being provided
157+
// to a loading function.
158+
type CredentialsType string
159+
160+
const (
161+
// ServiceAccount represents a service account file type.
162+
ServiceAccount CredentialsType = "service_account"
163+
// AuthorizedUser represents a user credentials file type.
164+
AuthorizedUser CredentialsType = "authorized_user"
165+
// ExternalAccount represents an external account file type.
166+
//
167+
// IMPORTANT:
168+
// This credential type does not validate the credential configuration. A security
169+
// risk occurs when a credential configuration configured with malicious urls
170+
// is used.
171+
// You should validate credential configurations provided by untrusted sources.
172+
// See [Security requirements when using credential configurations from an external
173+
// source] https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
174+
// for more details.
175+
ExternalAccount CredentialsType = "external_account"
176+
// ExternalAccountAuthorizedUser represents an external account authorized user file type.
177+
ExternalAccountAuthorizedUser CredentialsType = "external_account_authorized_user"
178+
// ImpersonatedServiceAccount represents an impersonated service account file type.
179+
//
180+
// IMPORTANT:
181+
// This credential type does not validate the credential configuration. A security
182+
// risk occurs when a credential configuration configured with malicious urls
183+
// is used.
184+
// You should validate credential configurations provided by untrusted sources.
185+
// See [Security requirements when using credential configurations from an external
186+
// source] https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
187+
// for more details.
188+
ImpersonatedServiceAccount CredentialsType = "impersonated_service_account"
189+
// GDCHServiceAccount represents a GDCH service account credentials.
190+
GDCHServiceAccount CredentialsType = "gdch_service_account"
191+
)
192+
156193
// DefaultClient returns an HTTP Client that uses the
157194
// DefaultTokenSource to obtain authentication credentials.
158195
func DefaultClient(ctx context.Context, scope ...string) (*http.Client, error) {
@@ -246,17 +283,71 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials
246283
return FindDefaultCredentialsWithParams(ctx, params)
247284
}
248285

249-
// CredentialsFromJSONWithParams obtains Google credentials from a JSON value. The JSON can
250-
// represent either a Google Developers Console client_credentials.json file (as in ConfigFromJSON),
251-
// a Google Developers service account key file, a gcloud user credentials file (a.k.a. refresh
252-
// token JSON), or the JSON configuration file for workload identity federation in non-Google cloud
253-
// platforms (see https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation).
286+
// CredentialsFromJSONWithType invokes CredentialsFromJSONWithTypeAndParams with the specified scopes.
254287
//
255288
// Important: If you accept a credential configuration (credential JSON/File/Stream) from an
256289
// external source for authentication to Google Cloud Platform, you must validate it before
257290
// providing it to any Google API or library. Providing an unvalidated credential configuration to
258291
// Google APIs can compromise the security of your systems and data. For more information, refer to
259292
// [Validate credential configurations from external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
293+
func CredentialsFromJSONWithType(ctx context.Context, jsonData []byte, credType CredentialsType, scopes ...string) (*Credentials, error) {
294+
var params CredentialsParams
295+
params.Scopes = scopes
296+
return CredentialsFromJSONWithTypeAndParams(ctx, jsonData, credType, params)
297+
}
298+
299+
// CredentialsFromJSONWithTypeAndParams obtains Google credentials from a JSON value and
300+
// validates that the credentials match the specified type.
301+
//
302+
// Important: If you accept a credential configuration (credential JSON/File/Stream) from an
303+
// external source for authentication to Google Cloud Platform, you must validate it before
304+
// providing it to any Google API or library. Providing an unvalidated credential configuration to
305+
// Google APIs can compromise the security of your systems and data. For more information, refer to
306+
// [Validate credential configurations from external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
307+
func CredentialsFromJSONWithTypeAndParams(ctx context.Context, jsonData []byte, credType CredentialsType, params CredentialsParams) (*Credentials, error) {
308+
var f struct {
309+
Type string `json:"type"`
310+
}
311+
if err := json.Unmarshal(jsonData, &f); err != nil {
312+
return nil, err
313+
}
314+
if CredentialsType(f.Type) != credType {
315+
return nil, fmt.Errorf("google: expected credential type %q, found %q", credType, f.Type)
316+
}
317+
return CredentialsFromJSONWithParams(ctx, jsonData, params)
318+
}
319+
320+
// CredentialsFromJSONWithParams obtains Google credentials from a JSON value. The JSON can
321+
// represent either a Google Developers Console client_credentials.json file (as in ConfigFromJSON),
322+
// a Google Developers service account key file, a gcloud user credentials file (a.k.a. refresh
323+
// token JSON), or the JSON configuration file for workload identity federation in non-Google cloud
324+
// platforms (see https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation).
325+
//
326+
// Deprecated: This function is deprecated because of a potential security risk.
327+
// It does not validate the credential configuration. The security risk occurs
328+
// when a credential configuration is accepted from a source that is not
329+
// under your control and used without validation on your side.
330+
//
331+
// If you know that you will be loading credential configurations of a
332+
// specific type, it is recommended to use a credential-type-specific
333+
// CredentialsFromJSONWithTypeAndParams method. This will ensure that an unexpected
334+
// credential type with potential for malicious intent is not loaded
335+
// unintentionally. You might still have to do validation for certain
336+
// credential types. Please follow the recommendation for that method. For
337+
// example, if you want to load only service accounts, you can use
338+
//
339+
// creds, err := google.CredentialsFromJSONWithTypeAndParams(ctx, jsonData, google.ServiceAccount, params)
340+
//
341+
// If you are loading your credential configuration from an untrusted source
342+
// and have not mitigated the risks (e.g. by validating the configuration
343+
// yourself), make these changes as soon as possible to prevent security
344+
// risks to your environment.
345+
//
346+
// Regardless of the method used, it is always your responsibility to
347+
// validate configurations received from external sources.
348+
//
349+
// For more details see:
350+
// https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
260351
func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params CredentialsParams) (*Credentials, error) {
261352
// Make defensive copy of the slices in params.
262353
params = params.deepCopy()
@@ -301,11 +392,31 @@ func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params
301392

302393
// CredentialsFromJSON invokes CredentialsFromJSONWithParams with the specified scopes.
303394
//
304-
// Important: If you accept a credential configuration (credential JSON/File/Stream) from an
305-
// external source for authentication to Google Cloud Platform, you must validate it before
306-
// providing it to any Google API or library. Providing an unvalidated credential configuration to
307-
// Google APIs can compromise the security of your systems and data. For more information, refer to
308-
// [Validate credential configurations from external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
395+
// Deprecated: This function is deprecated because of a potential security risk.
396+
// It does not validate the credential configuration. The security risk occurs
397+
// when a credential configuration is accepted from a source that is not
398+
// under your control and used without validation on your side.
399+
//
400+
// If you know that you will be loading credential configurations of a
401+
// specific type, it is recommended to use a credential-type-specific
402+
// CredentialsFromJSONWithType method. This will ensure that an unexpected
403+
// credential type with potential for malicious intent is not loaded
404+
// unintentionally. You might still have to do validation for certain
405+
// credential types. Please follow the recommendation for that method. For
406+
// example, if you want to load only service accounts, you can use
407+
//
408+
// creds, err := google.CredentialsFromJSONWithType(ctx, jsonData, google.ServiceAccount, scopes...)
409+
//
410+
// If you are loading your credential configuration from an untrusted source
411+
// and have not mitigated the risks (e.g. by validating the configuration
412+
// yourself), make these changes as soon as possible to prevent security
413+
// risks to your environment.
414+
//
415+
// Regardless of the method used, it is always your responsibility to
416+
// validate configurations received from external sources.
417+
//
418+
// For more details see:
419+
// https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
309420
func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) (*Credentials, error) {
310421
var params CredentialsParams
311422
params.Scopes = scopes

google/default_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"net/http"
1010
"net/http/httptest"
11+
"os"
1112
"strings"
1213
"testing"
1314

@@ -310,3 +311,88 @@ func TestComputeUniverseDomain(t *testing.T) {
310311
}
311312

312313
}
314+
315+
func TestCredentialsFromJSONWithType(t *testing.T) {
316+
ctx := context.Background()
317+
sa, err := os.ReadFile("testdata/sa.json")
318+
if err != nil {
319+
t.Fatal(err)
320+
}
321+
user, err := os.ReadFile("testdata/user.json")
322+
if err != nil {
323+
t.Fatal(err)
324+
}
325+
gdch, err := os.ReadFile("testdata/gdch.json")
326+
if err != nil {
327+
t.Fatal(err)
328+
}
329+
tests := []struct {
330+
name string
331+
credType CredentialsType
332+
json []byte
333+
wantErr bool
334+
wantErrMsg string
335+
}{
336+
{
337+
name: "ServiceAccount Success",
338+
credType: ServiceAccount,
339+
json: sa,
340+
wantErr: false,
341+
},
342+
{
343+
name: "User Success",
344+
credType: AuthorizedUser,
345+
json: user,
346+
wantErr: false,
347+
},
348+
{
349+
name: "GDCH Success",
350+
credType: GDCHServiceAccount,
351+
json: gdch,
352+
wantErr: false,
353+
},
354+
{
355+
name: "ServiceAccount Mismatch",
356+
credType: ServiceAccount,
357+
json: user,
358+
wantErr: true,
359+
wantErrMsg: `google: expected credential type "service_account", found "authorized_user"`,
360+
},
361+
{
362+
name: "User Mismatch",
363+
credType: AuthorizedUser,
364+
json: sa,
365+
wantErr: true,
366+
wantErrMsg: `google: expected credential type "authorized_user", found "service_account"`,
367+
},
368+
{
369+
name: "Malformed JSON",
370+
credType: ServiceAccount,
371+
json: []byte(`{"type": "service_account",}`),
372+
wantErr: true,
373+
wantErrMsg: "invalid character",
374+
},
375+
{
376+
name: "Missing Type Field",
377+
credType: ServiceAccount,
378+
json: []byte(`{"project_id": "my-proj"}`),
379+
wantErr: true,
380+
wantErrMsg: `google: expected credential type "service_account", found ""`,
381+
},
382+
}
383+
384+
for _, tt := range tests {
385+
t.Run(tt.name, func(t *testing.T) {
386+
_, err := CredentialsFromJSONWithType(ctx, tt.json, tt.credType)
387+
if (err != nil) != tt.wantErr {
388+
t.Fatalf("CredentialsFromJSONWithType() error = %v, wantErr %v", err, tt.wantErr)
389+
}
390+
if tt.wantErr {
391+
if !strings.Contains(err.Error(), tt.wantErrMsg) {
392+
t.Errorf("CredentialsFromJSONWithType() error = %q, want error containing %q", err.Error(), tt.wantErrMsg)
393+
}
394+
return
395+
}
396+
})
397+
}
398+
}

google/google.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ const (
103103
externalAccountKey = "external_account"
104104
externalAccountAuthorizedUserKey = "external_account_authorized_user"
105105
impersonatedServiceAccount = "impersonated_service_account"
106+
gdchServiceAccountKey = "gdch_service_account"
106107
)
107108

108109
// credentialsFile is the unmarshalled representation of a credentials file.
@@ -165,7 +166,7 @@ func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config
165166

166167
func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsParams) (oauth2.TokenSource, error) {
167168
switch f.Type {
168-
case serviceAccountKey:
169+
case serviceAccountKey, gdchServiceAccountKey:
169170
cfg := f.jwtConfig(params.Scopes, params.Subject)
170171
return cfg.TokenSource(ctx), nil
171172
case userCredentialsKey:

google/testdata/gdch.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"type": "gdch_service_account"
3+
}

google/testdata/sa.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"type": "service_account"
3+
}

google/testdata/user.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"type": "authorized_user"
3+
}

0 commit comments

Comments
 (0)