From 6dc4e583dfd3aa3030dfbf959ee1c68a259abe2f Mon Sep 17 00:00:00 2001 From: sai-sunder-s <4540365+sai-sunder-s@users.noreply.github.com> Date: Thu, 3 Nov 2022 18:30:37 +0000 Subject: [PATCH] fix: Validate url domain for aws metadata urls (#1484) * fix: Validate url domain for aws metadata urls * lint * refactor and add test for ipv6 * update undefined check --- src/auth/awsclient.ts | 23 +++++++++++ test/test.awsclient.ts | 90 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/src/auth/awsclient.ts b/src/auth/awsclient.ts index 7a61fd17..fdbdc411 100644 --- a/src/auth/awsclient.ts +++ b/src/auth/awsclient.ts @@ -95,6 +95,7 @@ export class AwsClient extends BaseExternalAccountClient { options.credential_source.regional_cred_verification_url; this.imdsV2SessionTokenUrl = options.credential_source.imdsv2_session_token_url; + this.validateMetadataServerUrls(); const match = this.environmentId?.match(/^(aws)(\d+)$/); if (!match || !this.regionalCredVerificationUrl) { throw new Error('No valid AWS "credential_source" provided'); @@ -107,6 +108,28 @@ export class AwsClient extends BaseExternalAccountClient { this.region = ''; } + private validateMetadataServerUrls() { + this.validateMetadataServerUrlIfAny(this.regionUrl, 'region_url'); + this.validateMetadataServerUrlIfAny(this.securityCredentialsUrl, 'url'); + this.validateMetadataServerUrlIfAny( + this.imdsV2SessionTokenUrl, + 'imdsv2_session_token_url' + ); + } + + private validateMetadataServerUrlIfAny( + urlString: string | undefined, + nameOfData: string + ) { + if (urlString !== undefined) { + const url = new URL(urlString); + + if (url.host !== '169.254.169.254' && url.host !== '[fd00:ec2::254]') { + throw new Error(`Invalid host "${url.host}" for "${nameOfData}"`); + } + } + } + /** * Triggered when an external subject token is needed to be exchanged for a * GCP access token via GCP STS endpoint. diff --git a/test/test.awsclient.ts b/test/test.awsclient.ts index 496d6599..f753930a 100644 --- a/test/test.awsclient.ts +++ b/test/test.awsclient.ts @@ -203,6 +203,63 @@ describe('AwsClient', () => { }); }); + it('should throw when an unsupported url is provided', () => { + const expectedError = new Error('Invalid host "baddomain.com" for "url"'); + const invalidCredentialSource = Object.assign({}, awsCredentialSource); + invalidCredentialSource.url = 'http://baddomain.com/fake'; + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: invalidCredentialSource, + }; + + assert.throws(() => { + return new AwsClient(invalidOptions); + }, expectedError); + }); + + it('should throw when an unsupported imdsv2_session_token_url is provided', () => { + const expectedError = new Error( + 'Invalid host "baddomain.com" for "imdsv2_session_token_url"' + ); + const invalidCredentialSource = Object.assign( + {imdsv2_session_token_url: 'http://baddomain.com/fake'}, + awsCredentialSource + ); + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: invalidCredentialSource, + }; + + assert.throws(() => { + return new AwsClient(invalidOptions); + }, expectedError); + }); + + it('should throw when an unsupported region_url is provided', () => { + const expectedError = new Error( + 'Invalid host "baddomain.com" for "region_url"' + ); + const invalidCredentialSource = Object.assign({}, awsCredentialSource); + invalidCredentialSource.region_url = 'http://baddomain.com/fake'; + const invalidOptions = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: invalidCredentialSource, + }; + + assert.throws(() => { + return new AwsClient(invalidOptions); + }, expectedError); + }); + it('should throw when an unsupported environment ID is provided', () => { const expectedError = new Error( 'No valid AWS "credential_source" provided' @@ -266,6 +323,39 @@ describe('AwsClient', () => { scope.done(); }); + it('should resolve on success with ipv6', async () => { + const ipv6baseUrl = 'http://[fd00:ec2::254]'; + const ipv6CredentialSource = { + environment_id: 'aws1', + region_url: `${ipv6baseUrl}/latest/meta-data/placement/availability-zone`, + url: `${ipv6baseUrl}/latest/meta-data/iam/security-credentials`, + regional_cred_verification_url: + 'https://sts.{region}.amazonaws.com?' + + 'Action=GetCallerIdentity&Version=2011-06-15', + }; + const ipv6Options = { + type: 'external_account', + audience, + subject_token_type: 'urn:ietf:params:aws:token-type:aws4_request', + token_url: getTokenUrl(), + credential_source: ipv6CredentialSource, + }; + + const scope = nock(ipv6baseUrl) + .get('/latest/meta-data/placement/availability-zone') + .reply(200, `${awsRegion}b`) + .get('/latest/meta-data/iam/security-credentials') + .reply(200, awsRole) + .get(`/latest/meta-data/iam/security-credentials/${awsRole}`) + .reply(200, awsSecurityCredentials); + + const client = new AwsClient(ipv6Options); + const subjectToken = await client.retrieveSubjectToken(); + + assert.deepEqual(subjectToken, expectedSubjectToken); + scope.done(); + }); + it('should resolve on success with imdsv2 session token', async () => { const scopes: nock.Scope[] = []; scopes.push(