diff --git a/src/CredentialsClient.ts b/src/CredentialsClient.ts index ef8beed..43c16b1 100644 --- a/src/CredentialsClient.ts +++ b/src/CredentialsClient.ts @@ -4,7 +4,7 @@ import { defaultProvider } from '@aws-sdk/credential-provider-node'; import type { AwsCredentialIdentity } from '@aws-sdk/types'; import { NodeHttpHandler } from '@smithy/node-http-handler'; import { ProxyAgent } from 'proxy-agent'; -import { buildCustomUserAgent, errorMessage, getCallerIdentity } from './helpers'; +import { buildCustomUserAgent, errorMessage, getCallerIdentity, maskProxyCredentials } from './helpers'; import { ProxyResolver } from './ProxyResolver'; if (!process.env.AWS_EXECUTION_ENV) { @@ -32,6 +32,7 @@ export class CredentialsClient { } if (props.proxyServer) { info('Configuring proxy handler for STS client'); + maskProxyCredentials(props.proxyServer); const proxyOptions: { httpProxy: string; httpsProxy: string; noProxy?: string } = { httpProxy: props.proxyServer, httpsProxy: props.proxyServer, diff --git a/src/helpers.ts b/src/helpers.ts index 27524ec..c65f297 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -193,6 +193,29 @@ export function toCredentialIdentity(creds?: Partial): AwsCredentia }; } +// Registers any userinfo embedded in a proxy URL as secrets so it is masked in job logs. +// First the literal proxy string, then any username/password components if parseable. +// If the username/password is percent-encoded, the decoded form is also masked. +export function maskProxyCredentials(proxyServer: string): void { + core.setSecret(proxyServer); + let url: URL; + try { + url = new URL(proxyServer); + } catch (_) { + return; + } + for (const part of [url.username, url.password]) { + if (!part) continue; + core.setSecret(part); + try { + const decoded = decodeURIComponent(part); + if (decoded !== part) core.setSecret(decoded); + } catch (_) { + // malformed percent-encoding; the raw form is already masked + } + } +} + // Tags have a more restrictive set of acceptable characters than GitHub environment variables can. // This replaces anything not conforming to the tag restrictions by inverting the regular expression. // See the AWS documentation for constraint specifics https://docs.aws.amazon.com/STS/latest/APIReference/API_Tag.html. diff --git a/test/helpers.test.ts b/test/helpers.test.ts index eef1eb9..71a7c8e 100644 --- a/test/helpers.test.ts +++ b/test/helpers.test.ts @@ -126,6 +126,29 @@ describe('Configure AWS Credentials helpers', {}, () => { expect(core.exportVariable).toHaveBeenCalledWith('AWS_SESSION_TOKEN', ''); }); + describe('maskProxyCredentials', {}, () => { + it('masks username and password embedded in a proxy URL', {}, () => { + helpers.maskProxyCredentials('http://user:secretpass@proxy.example.com:8080'); + expect(core.setSecret).toHaveBeenCalledWith('user'); + expect(core.setSecret).toHaveBeenCalledWith('secretpass'); + }); + + it('masks both encoded and decoded forms of the credentials', {}, () => { + helpers.maskProxyCredentials('http://user:p%40ss@proxy.example.com:8080'); + expect(core.setSecret).toHaveBeenCalledWith('p%40ss'); + expect(core.setSecret).toHaveBeenCalledWith('p@ss'); + }); + + it('masks the whole value even without embedded credentials or when unparseable', {}, () => { + helpers.maskProxyCredentials('http://proxy.example.com:8080'); + expect(core.setSecret).toHaveBeenCalledWith('http://proxy.example.com:8080'); + helpers.maskProxyCredentials('not a url'); + expect(core.setSecret).toHaveBeenCalledWith('not a url'); + // no username/password parts, so exactly one mask per call + expect(core.setSecret).toHaveBeenCalledTimes(2); + }); + }); + describe('validateAccountId', {}, () => { it('enforces the allow-list even when the first element is empty', {}, () => { expect(() => helpers.validateAccountId(['', '999999999999'], '111111111111')).toThrow(/does not match/); diff --git a/test/index.test.ts b/test/index.test.ts index bc7d4f4..7b9aafd 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1267,6 +1267,21 @@ describe('Configure AWS Credentials', {}, () => { expect(core.setFailed).not.toHaveBeenCalled(); }); + + it('masks credentials embedded in the proxy URL', async () => { + vi.mocked(core.getInput).mockImplementation( + mocks.getInput({ + ...mocks.GH_OIDC_INPUTS, + 'http-proxy': 'http://user:secretpass@proxy.example.com:8080', + }), + ); + + await run(); + + expect(core.setSecret).toHaveBeenCalledWith('user'); + expect(core.setSecret).toHaveBeenCalledWith('secretpass'); + expect(core.setFailed).not.toHaveBeenCalled(); + }); }); describe('AWS Profile Support', {}, () => {