mirror of
https://github.com/aws-actions/configure-aws-credentials.git
synced 2026-09-03 06:05:04 +09:00
fix: account-ids handling, mask proxy as secret in logs (#1943)
* fix: enforce allowed-account-ids when the list contains empty entries An empty first element previously short-circuited the allowed account check. Empty entries are now filtered out and validation applies whenever any non-empty entry exists. * fix: enforce allowed-account-ids on the use-existing-credentials path The early return for valid pre-existing credentials skipped the allowed-account-ids check, now included. * fix: reject newlines in names and values when writing profile files If the profile file writing was enabled, we emitted newlines into the file verbatim, permitting injecting arbitrary profiles into the file. Writing now fails instead. * fix: honor configured STS endpoint for "ambient" credentials Ambient credential resolution built a bare STS client, so a web-identity token found by the SDK default chain (e.g. AWS_WEB_IDENTITY_TOKEN_FILE on a self-hosted runner) was exchanged with public STS instead of any operator-configured sts-endpoint. Resolution now passes the configured region, endpoint, and proxy handler to the default provider chain. * fix: mask proxy URL credentials in job logs Basic-auth userinfo in the http-proxy input or HTTP(S)_PROXY environment variables was never registered as a secret, so error messages carrying the proxy URL printed the credentials unmasked in the job log. * fix: omit account IDs from the allowed-account-ids failure message The mismatch error is thrown before exportAccountId registers the account-id mask, so setFailed wrote the raw account ID (and the configured allow-list) into a public annotation. (C4) * chore: remove outdated examples All of the examples were out of date and we do not have a mechanism for keeping them up to date. Removed the examples.
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('@aws-sdk/credential-provider-node', () => ({
|
||||
defaultProvider: vi.fn(() => async () => ({ accessKeyId: 'AKIA', secretAccessKey: 'secret' })),
|
||||
}));
|
||||
|
||||
import { defaultProvider } from '@aws-sdk/credential-provider-node';
|
||||
import { CredentialsClient } from '../src/CredentialsClient';
|
||||
|
||||
describe('CredentialsClient', {}, () => {
|
||||
it('pins ambient credential resolution to the configured region and STS endpoint', {}, async () => {
|
||||
const client = new CredentialsClient({
|
||||
region: 'eu-west-1',
|
||||
stsEndpoint: 'https://sts.example.com',
|
||||
roleChaining: false,
|
||||
});
|
||||
// biome-ignore lint/suspicious/noExplicitAny: any required to call private method
|
||||
await (client as any).loadCredentials();
|
||||
expect(defaultProvider).toHaveBeenCalledWith({
|
||||
clientConfig: expect.objectContaining({ region: 'eu-west-1', endpoint: 'https://sts.example.com' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('omits unset client config values from ambient credential resolution', {}, async () => {
|
||||
const client = new CredentialsClient({ region: 'eu-west-1', roleChaining: false });
|
||||
// biome-ignore lint/suspicious/noExplicitAny: any required to call private method
|
||||
await (client as any).loadCredentials();
|
||||
expect(defaultProvider).toHaveBeenLastCalledWith({ clientConfig: { region: 'eu-west-1' } });
|
||||
});
|
||||
});
|
||||
@@ -126,6 +126,45 @@ 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/);
|
||||
});
|
||||
|
||||
it('passes an allowed account despite empty entries in the list', {}, () => {
|
||||
expect(() => helpers.validateAccountId(['', '111111111111'], '111111111111')).not.toThrow();
|
||||
});
|
||||
|
||||
it('skips validation only when no non-empty entries exist', {}, () => {
|
||||
expect(() => helpers.validateAccountId(undefined, '111111111111')).not.toThrow();
|
||||
expect(() => helpers.validateAccountId([], '111111111111')).not.toThrow();
|
||||
expect(() => helpers.validateAccountId([''], '111111111111')).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('filesystem helpers', {}, () => {
|
||||
describe('isSymlink', {}, () => {
|
||||
it('returns true for a symlink', {}, () => {
|
||||
|
||||
+47
-5
@@ -841,7 +841,7 @@ describe('Configure AWS Credentials', {}, () => {
|
||||
|
||||
await run();
|
||||
expect(core.setFailed).toHaveBeenCalledWith(
|
||||
'The account ID of the provided credentials (111111111111) does not match any of the expected account IDs: 999999999999',
|
||||
'The account ID of the provided credentials does not match any of the allowed account IDs',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -861,7 +861,7 @@ describe('Configure AWS Credentials', {}, () => {
|
||||
|
||||
await run();
|
||||
expect(core.setFailed).toHaveBeenCalledWith(
|
||||
'The account ID of the provided credentials (111111111111) does not match any of the expected account IDs: 999999999999, 888888888888',
|
||||
'The account ID of the provided credentials does not match any of the allowed account IDs',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -917,7 +917,7 @@ describe('Configure AWS Credentials', {}, () => {
|
||||
|
||||
await run();
|
||||
expect(core.setFailed).toHaveBeenCalledWith(
|
||||
'The account ID of the provided credentials (111111111111) does not match any of the expected account IDs: 999999999999',
|
||||
'The account ID of the provided credentials does not match any of the allowed account IDs',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -936,7 +936,7 @@ describe('Configure AWS Credentials', {}, () => {
|
||||
|
||||
await run();
|
||||
expect(core.setFailed).toHaveBeenCalledWith(
|
||||
'The account ID of the provided credentials (111111111111) does not match any of the expected account IDs: 999999999999',
|
||||
'The account ID of the provided credentials does not match any of the allowed account IDs',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -956,7 +956,7 @@ describe('Configure AWS Credentials', {}, () => {
|
||||
|
||||
await run();
|
||||
expect(core.setFailed).toHaveBeenCalledWith(
|
||||
'The account ID of the provided credentials (111111111111) does not match any of the expected account IDs: 999999999999',
|
||||
'The account ID of the provided credentials does not match any of the allowed account IDs',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1015,6 +1015,33 @@ describe('Configure AWS Credentials', {}, () => {
|
||||
await run();
|
||||
expect(core.setFailed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fails on the use-existing-credentials path when the account is not allowed', async () => {
|
||||
vi.mocked(core.getInput).mockImplementation(
|
||||
mocks.getInput({
|
||||
...mocks.USE_EXISTING_CREDENTIALS_INPUTS,
|
||||
'allowed-account-ids': '999999999999',
|
||||
}),
|
||||
);
|
||||
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
|
||||
|
||||
await run();
|
||||
expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('does not match'));
|
||||
});
|
||||
|
||||
it('reuses existing credentials when their account is allowed', async () => {
|
||||
vi.mocked(core.getInput).mockImplementation(
|
||||
mocks.getInput({
|
||||
...mocks.USE_EXISTING_CREDENTIALS_INPUTS,
|
||||
'allowed-account-ids': '111111111111',
|
||||
}),
|
||||
);
|
||||
mockedSTSClient.on(GetCallerIdentityCommand).resolves({ ...mocks.outputs.GET_CALLER_IDENTITY });
|
||||
|
||||
await run();
|
||||
expect(core.notice).toHaveBeenCalledWith('Pre-existing credentials are valid. No need to generate new ones.');
|
||||
expect(core.setFailed).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Global Timeout Configuration', {}, () => {
|
||||
@@ -1240,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', {}, () => {
|
||||
|
||||
@@ -114,6 +114,22 @@ describe('Profile Manager', {}, () => {
|
||||
const result = stringifyIni({ dev: {} });
|
||||
expect(result).toBe('[dev]\n');
|
||||
});
|
||||
|
||||
it('rejects values containing newlines', {}, () => {
|
||||
expect(() =>
|
||||
stringifyIni({ dev: { aws_session_token: 'token\n[injected]\ncredential_process = evil' } }),
|
||||
).toThrow('must not contain newline characters');
|
||||
});
|
||||
|
||||
it('rejects keys containing newlines', {}, () => {
|
||||
expect(() => stringifyIni({ dev: { 'key\ninjected': 'val' } })).toThrow('must not contain newline characters');
|
||||
});
|
||||
|
||||
it('rejects section names containing newlines', {}, () => {
|
||||
expect(() => stringifyIni({ 'dev\r\n[injected]': { key: 'val' } })).toThrow(
|
||||
'must not contain newline characters',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateProfileName', {}, () => {
|
||||
@@ -423,6 +439,24 @@ describe('Profile Manager', {}, () => {
|
||||
expect(configParsed['profile dev'].region).toBe('us-east-1');
|
||||
});
|
||||
|
||||
it('refuses to write credentials containing newlines instead of injecting profiles', {}, () => {
|
||||
expect(() =>
|
||||
writeProfileFiles(
|
||||
'dev',
|
||||
{
|
||||
AccessKeyId: 'AKIAIOSFODNN7EXAMPLE',
|
||||
SecretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
||||
SessionToken: 'token\n[injected]\ncredential_process = evil-command',
|
||||
},
|
||||
'us-east-1',
|
||||
false,
|
||||
),
|
||||
).toThrow('must not contain newline characters');
|
||||
|
||||
const credsPath = getProfileFilePaths().credentials;
|
||||
expect(fs.existsSync(credsPath)).toBe(false);
|
||||
});
|
||||
|
||||
it('uses correct section naming for default profile', {}, () => {
|
||||
writeProfileFiles(
|
||||
'default',
|
||||
|
||||
Reference in New Issue
Block a user