fix: enforce allowed-account-ids on all auth paths (#1847)

* fix: enforce allowed-account-ids on all auth paths

The allowed-account-ids list was only enforced in some auth flows. This
was due to the check being included in validateCredentials, which was
skipped if (GITHUB_ACTIONS && AccessKeyId && output-env-credentials) ->
false.

This unifies credential validation into a single path.

- validateCredentials(credentials?, ...) resolves credentials, proves
  liveness via one GetCallerIdentity call, and returns the identity.
- validateAccountId(expectedAccountIds, account) is now a pure comparison
  against the resolved account, enforced against the final (assumed)
  account independent of auth method, GITHUB_ACTIONS, or
  output-env-credentials.
- exportAccountId(identity, ...) consumes the resolved identity instead of
  making its own GetCallerIdentity call, so credential resolution happens
  exactly once per credential set.

Pre-assume account checks remain gated on !roleToAssume so cross-account
assume-role (source account differs from the role's target) is preserved.

Adds regression tests for the OIDC wrong-account case (the previously
missing negative test), OIDC with output-env-credentials: false, and the
assume-role wrong-account case.

* chore: move validateAccountId into helpers
This commit is contained in:
Tom Keller
2026-06-26 13:33:19 -07:00
committed by GitHub
parent e004cdcd28
commit 4d281fbc56
5 changed files with 184 additions and 86 deletions
+32 -3
View File
@@ -3,6 +3,7 @@ import * as path from 'node:path';
import * as core from '@actions/core';
import type { Credentials, STSClient } from '@aws-sdk/client-sts';
import { GetCallerIdentityCommand } from '@aws-sdk/client-sts';
import type { AwsCredentialIdentity } from '@aws-sdk/types';
import type { UserAgent } from '@smithy/types';
import type { CredentialsClient } from './CredentialsClient';
@@ -150,9 +151,8 @@ export async function getCallerIdentity(client: STSClient): Promise<{ Account: s
return result;
}
// Obtains account ID from STS Client and sets it as output
export async function exportAccountId(credentialsClient: CredentialsClient, maskAccountId?: boolean) {
const identity = await getCallerIdentity(credentialsClient.stsClient);
// Emits the account ID and ARN of an already-resolved caller identity as action outputs.
export function exportAccountId(identity: { Account: string; Arn: string }, maskAccountId?: boolean) {
const accountId = identity.Account;
const arn = identity.Arn;
if (maskAccountId) {
@@ -164,6 +164,35 @@ export async function exportAccountId(credentialsClient: CredentialsClient, mask
return accountId;
}
// Validates that the account of the already-resolved caller identity is in the allow-list provided via the
// `allowed-account-ids` input.
export function validateAccountId(expectedAccountIds: string[] | undefined, account: string | undefined): void {
if (!expectedAccountIds || expectedAccountIds.length === 0 || expectedAccountIds[0] === '') {
return;
}
if (!account || !expectedAccountIds.includes(account)) {
throw new Error(
`The account ID of the provided credentials (${
account ?? 'unknown'
}) does not match any of the expected account IDs: ${expectedAccountIds.join(', ')}`,
);
}
}
// Converts the STS Credentials shape (returned by AssumeRole and provided as action inputs) into
// the AwsCredentialIdentity shape the SDK expects when credentials are supplied explicitly to a
// client. Returns undefined if the access key ID or secret access key is missing.
export function toCredentialIdentity(creds?: Partial<Credentials>): AwsCredentialIdentity | undefined {
if (!creds?.AccessKeyId || !creds.SecretAccessKey) {
return undefined;
}
return {
accessKeyId: creds.AccessKeyId,
secretAccessKey: creds.SecretAccessKey,
...(creds.SessionToken && { sessionToken: creds.SessionToken }),
};
}
// 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.