Skip to content

Commit

Permalink
fix: validate token_url and service_account_impersonation_url (#1229)
Browse files Browse the repository at this point in the history
  • Loading branch information
xil222 committed Aug 17, 2021
1 parent 0dfb429 commit 0360bb7
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 0 deletions.
73 changes: 73 additions & 0 deletions src/auth/baseexternalclient.ts
Expand Up @@ -37,6 +37,11 @@ const STS_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange';
const STS_REQUEST_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
/** The default OAuth scope to request when none is provided. */
const DEFAULT_OAUTH_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
/** The google apis domain pattern. */
const GOOGLE_APIS_DOMAIN_PATTERN = '\\.googleapis\\.com$';
/** The variable portion pattern in a Google APIs domain. */
const VARIABLE_PORTION_PATTERN = '[^\\.\\s\\/\\\\]+';

/**
* Offset to take into account network delays and server clock skews.
*/
Expand Down Expand Up @@ -154,13 +159,28 @@ export abstract class BaseExternalAccountClient extends AuthClient {
clientSecret: options.client_secret,
} as ClientAuthentication)
: undefined;
if (!this.validateGoogleAPIsUrl('sts', options.token_url)) {
throw new Error(`"${options.token_url}" is not a valid token url.`);
}
this.stsCredential = new sts.StsCredentials(options.token_url, clientAuth);
// Default OAuth scope. This could be overridden via public property.
this.scopes = [DEFAULT_OAUTH_SCOPE];
this.cachedAccessToken = null;
this.audience = options.audience;
this.subjectTokenType = options.subject_token_type;
this.quotaProjectId = options.quota_project_id;
if (
typeof options.service_account_impersonation_url !== 'undefined' &&
!this.validateGoogleAPIsUrl(
'iamcredentials',
options.service_account_impersonation_url
)
) {
throw new Error(
`"${options.service_account_impersonation_url}" is ` +
'not a valid service account impersonation url.'
);
}
this.serviceAccountImpersonationUrl =
options.service_account_impersonation_url;
// As threshold could be zero,
Expand Down Expand Up @@ -501,4 +521,57 @@ export abstract class BaseExternalAccountClient extends AuthClient {
return this.scopes;
}
}

/**
* Checks whether Google APIs URL is valid.
* @param apiName The apiName of url.
* @param url The Google API URL to validate.
* @return Whether the URL is valid or not.
*/
private validateGoogleAPIsUrl(apiName: string, url: string): boolean {
let parsedUrl;
// Return false if error is thrown during parsing URL.
try {
parsedUrl = new URL(url);
} catch (e) {
return false;
}

const urlDomain = parsedUrl.hostname;
// Check the protocol is https.
if (parsedUrl.protocol !== 'https:') {
return false;
}

const googleAPIsDomainPatterns: RegExp[] = [
new RegExp(
'^' +
VARIABLE_PORTION_PATTERN +
'\\.' +
apiName +
GOOGLE_APIS_DOMAIN_PATTERN
),
new RegExp('^' + apiName + GOOGLE_APIS_DOMAIN_PATTERN),
new RegExp(
'^' +
apiName +
'\\.' +
VARIABLE_PORTION_PATTERN +
GOOGLE_APIS_DOMAIN_PATTERN
),
new RegExp(
'^' +
VARIABLE_PORTION_PATTERN +
'\\-' +
apiName +
GOOGLE_APIS_DOMAIN_PATTERN
),
];
for (const googleAPIsDomainPattern of googleAPIsDomainPatterns) {
if (urlDomain.match(googleAPIsDomainPattern)) {
return true;
}
}
return false;
}
}
109 changes: 109 additions & 0 deletions test/test.baseexternalclient.ts
Expand Up @@ -136,6 +136,115 @@ describe('BaseExternalAccountClient', () => {
}, expectedError);
});

const invalidTokenUrls = [
'http://sts.googleapis.com',
'https://',
'https://sts.google.com',
'https://sts.googleapis.net',
'https://sts.googleapis.comevil.com',
'https://sts.googleapis.com.evil.com',
'https://sts.googleapis.com.evil.com/path/to/example',
'https://sts..googleapis.com',
'https://-sts.googleapis.com',
'https://evilsts.googleapis.com',
'https://us.east.1.sts.googleapis.com',
'https://us east 1.sts.googleapis.com',
'https://us-east- 1.sts.googleapis.com',
'https://us/.east/.1.sts.googleapis.com',
'https://us.ea\\st.1.sts.googleapis.com',
];
invalidTokenUrls.forEach(invalidTokenUrl => {
it(`should throw on invalid token url: ${invalidTokenUrl}`, () => {
const invalidOptions = Object.assign({}, externalAccountOptions);
invalidOptions.token_url = invalidTokenUrl;
const expectedError = new Error(
`"${invalidTokenUrl}" is not a valid token url.`
);
assert.throws(() => {
return new TestExternalAccountClient(invalidOptions);
}, expectedError);
});
});

it('should not throw on valid token urls', () => {
const validTokenUrls = [
'https://sts.googleapis.com',
'https://sts.us-west-1.googleapis.com',
'https://sts.google.googleapis.com',
'https://sts.googleapis.com/path/to/example',
'https://us-west-1.sts.googleapis.com',
'https://us-west-1-sts.googleapis.com',
'https://exmaple.sts.googleapis.com',
'https://example-sts.googleapis.com',
];
const validOptions = Object.assign({}, externalAccountOptions);
for (const validTokenUrl of validTokenUrls) {
validOptions.token_url = validTokenUrl;
assert.doesNotThrow(() => {
return new TestExternalAccountClient(validOptions);
});
}
});

const invalidServiceAccountImpersonationUrls = [
'http://iamcredentials.googleapis.com',
'https://',
'https://iamcredentials.google.com',
'https://iamcredentials.googleapis.net',
'https://iamcredentials.googleapis.comevil.com',
'https://iamcredentials.googleapis.com.evil.com',
'https://iamcredentials.googleapis.com.evil.com/path/to/example',
'https://iamcredentials..googleapis.com',
'https://-iamcredentials.googleapis.com',
'https://eviliamcredentials.googleapis.com',
'https://evil.eviliamcredentials.googleapis.com',
'https://us.east.1.iamcredentials.googleapis.com',
'https://us east 1.iamcredentials.googleapis.com',
'https://us-east- 1.iamcredentials.googleapis.com',
'https://us/.east/.1.iamcredentials.googleapis.com',
'https://us.ea\\st.1.iamcredentials.googleapis.com',
];
invalidServiceAccountImpersonationUrls.forEach(
invalidServiceAccountImpersonationUrl => {
it(`should throw on invalid service account impersonation url: ${invalidServiceAccountImpersonationUrl}`, () => {
const invalidOptions = Object.assign(
{},
externalAccountOptionsWithSA
);
invalidOptions.service_account_impersonation_url =
invalidServiceAccountImpersonationUrl;
const expectedError = new Error(
`"${invalidServiceAccountImpersonationUrl}" is ` +
'not a valid service account impersonation url.'
);
assert.throws(() => {
return new TestExternalAccountClient(invalidOptions);
}, expectedError);
});
}
);

it('should not throw on valid service account impersonation url', () => {
const validServiceAccountImpersonationUrls = [
'https://iamcredentials.googleapis.com',
'https://iamcredentials.us-west-1.googleapis.com',
'https://iamcredentials.google.googleapis.com',
'https://iamcredentials.googleapis.com/path/to/example',
'https://us-west-1.iamcredentials.googleapis.com',
'https://us-west-1-iamcredentials.googleapis.com',
'https://example.iamcredentials.googleapis.com',
'https://example-iamcredentials.googleapis.com',
];
const validOptions = Object.assign({}, externalAccountOptionsWithSA);
for (const validServiceAccountImpersonationUrl of validServiceAccountImpersonationUrls) {
validOptions.service_account_impersonation_url =
validServiceAccountImpersonationUrl;
assert.doesNotThrow(() => {
return new TestExternalAccountClient(validOptions);
});
}
});

it('should not throw on valid options', () => {
assert.doesNotThrow(() => {
return new TestExternalAccountClient(externalAccountOptions);
Expand Down

0 comments on commit 0360bb7

Please sign in to comment.