Skip to content

Commit 0d408ce

Browse files
Merge branch 'master' into dependabot/npm_and_yarn/multi-2b9612f831
2 parents 74a4f07 + 833e823 commit 0d408ce

6 files changed

Lines changed: 254 additions & 12 deletions

File tree

‎.github/CODEOWNERS‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
@YanaXu
1+
@Azure/act-identity-squad

‎.gitignore‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ typings/
9494
.DS_Store
9595
Thumbs.db
9696

97+
# IDE / editor
98+
.vs/
99+
.vscode/
100+
.idea/
101+
*.swp
102+
97103
# Ignore built ts files
98104
__tests__/runner/*
99105
lib/**/*

‎README.md‎

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# Azure Login Action
22

33
- [Azure Login Action](#azure-login-action)
4+
- [Supported Versions](#supported-versions)
5+
- [Version Selection](#version-selection)
6+
- [Major-version tag](#major-version-tag)
7+
- [Exact version tag](#exact-version-tag)
8+
- [Branch reference](#branch-reference)
9+
- [Commit SHA](#commit-sha)
10+
- [Security Updates](#security-updates)
411
- [Input Parameters](#input-parameters)
512
- [`client-id`](#client-id)
613
- [`subscription-id`](#subscription-id)
@@ -48,6 +55,70 @@ Azure Login Action supports different ways of authentication with Azure.
4855
> [!WARNING]
4956
> Avoid using managed identity login on self-hosted runners in public repositories. Managed identities enable secure authentication with Azure resources and obtain Microsoft Entra ID tokens without the need for explicit credential management. Any user can open pull requests against your repository and access your self-hosted runners without credentials. See more details in [self-hosted runner security](https://docs.github.com/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#self-hosted-runner-security).
5057
58+
## Supported Versions
59+
60+
Azure Login follows a major-version support model.
61+
62+
| Version | Status |
63+
| --- | --- |
64+
| v3 | Supported |
65+
| v2 | Maintenance mode (security fixes only) |
66+
| v1 | End of Life (EOL) |
67+
68+
New features are released only to supported versions. Security fixes are released to supported versions and versions in maintenance mode.
69+
70+
Customers are strongly encouraged to use the latest v3 release.
71+
72+
## Version Selection
73+
74+
GitHub Actions users can reference Azure Login using several forms.
75+
76+
### Major-version tag
77+
78+
```yaml
79+
uses: azure/login@v3
80+
```
81+
82+
Receives compatible updates, including security fixes, released to the referenced major version.
83+
84+
### Exact version tag
85+
86+
```yaml
87+
uses: azure/login@v2.4.0
88+
```
89+
90+
Remains pinned to that specific release and does not automatically receive future fixes or updates.
91+
92+
### Branch reference
93+
94+
```yaml
95+
uses: azure/login@master
96+
```
97+
98+
Receives updates from the referenced branch. For stable workflows, use a supported major-version tag or pin to a full-length commit SHA.
99+
100+
### Commit SHA
101+
102+
```yaml
103+
uses: azure/login@<full-length-commit-sha>
104+
```
105+
106+
Remains pinned to that commit and does not automatically receive future fixes or updates.
107+
108+
## Security Updates
109+
110+
Security fixes are released to supported versions and versions in maintenance mode. Customers using exact version tags or commit SHA references must explicitly upgrade to a patched release to receive security fixes.
111+
112+
```yaml
113+
# Automatically receives future v3 security updates
114+
uses: azure/login@v3
115+
116+
# Does not automatically receive future updates
117+
uses: azure/login@v2.4.0
118+
```
119+
120+
Customers using v1 should migrate to v3. End-of-life releases no longer receive updates or security fixes.
121+
51122
## Input Parameters
52123
53124
|Parameter Name|Required?|Type|Default Value|Description|
@@ -705,4 +776,4 @@ provided by the bot. You will only need to do this once across all repos using o
705776

706777
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
707778
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
708-
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
779+
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

‎__tests__/PowerShell/AzPSScriptBuilder.test.ts‎

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,157 @@ describe("Getting AzLogin PS script", () => {
150150
});
151151
});
152152

153+
const INJECT_RAW = "abc' ; Start-Process calc ; $x='";
154+
const INJECT_ESCAPED = "abc'' ; Start-Process calc ; $x=''";
155+
156+
test('SECURITY: tenant-id single quote is escaped (SP+secret path)', () => {
157+
setEnv('environment', 'azurecloud');
158+
setEnv('enable-AzPSSession', 'true');
159+
setEnv('allow-no-subscriptions', 'true');
160+
setEnv('auth-type', 'SERVICE_PRINCIPAL');
161+
let creds = {
162+
'clientId': 'client-id',
163+
'clientSecret': 'client-secret',
164+
'tenantId': INJECT_RAW,
165+
'subscriptionId': 'subscription-id'
166+
}
167+
setEnv('creds', JSON.stringify(creds));
168+
169+
let loginConfig = new LoginConfig();
170+
loginConfig.initialize();
171+
return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([_, loginScript]) => {
172+
expect(loginScript).toContain(`-Tenant '${INJECT_ESCAPED}'`);
173+
expect(loginScript).not.toContain(`-Tenant '${INJECT_RAW}'`);
174+
});
175+
});
176+
177+
test('SECURITY: subscription-id single quote is escaped (SP+secret path)', () => {
178+
setEnv('environment', 'azurecloud');
179+
setEnv('enable-AzPSSession', 'true');
180+
setEnv('allow-no-subscriptions', 'true');
181+
setEnv('auth-type', 'SERVICE_PRINCIPAL');
182+
let creds = {
183+
'clientId': 'client-id',
184+
'clientSecret': 'client-secret',
185+
'tenantId': 'tenant-id',
186+
'subscriptionId': INJECT_RAW
187+
}
188+
setEnv('creds', JSON.stringify(creds));
189+
190+
let loginConfig = new LoginConfig();
191+
loginConfig.initialize();
192+
return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([_, loginScript]) => {
193+
expect(loginScript).toContain(`-Subscription '${INJECT_ESCAPED}'`);
194+
expect(loginScript).not.toContain(`-Subscription '${INJECT_RAW}'`);
195+
});
196+
});
197+
198+
test('SECURITY: client-id single quote is escaped (SP+secret path, PSCredential)', () => {
199+
setEnv('environment', 'azurecloud');
200+
setEnv('enable-AzPSSession', 'true');
201+
setEnv('allow-no-subscriptions', 'true');
202+
setEnv('auth-type', 'SERVICE_PRINCIPAL');
203+
let creds = {
204+
'clientId': INJECT_RAW,
205+
'clientSecret': 'client-secret',
206+
'tenantId': 'tenant-id',
207+
'subscriptionId': 'subscription-id'
208+
}
209+
setEnv('creds', JSON.stringify(creds));
210+
211+
let loginConfig = new LoginConfig();
212+
loginConfig.initialize();
213+
return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([_, loginScript]) => {
214+
expect(loginScript).toContain(`New-Object System.Management.Automation.PSCredential('${INJECT_ESCAPED}',`);
215+
expect(loginScript).not.toContain(`New-Object System.Management.Automation.PSCredential('${INJECT_RAW}',`);
216+
});
217+
});
218+
219+
test('SECURITY: client-id single quote is escaped (OIDC path)', () => {
220+
setEnv('environment', 'azurecloud');
221+
setEnv('enable-AzPSSession', 'true');
222+
setEnv('allow-no-subscriptions', 'false');
223+
setEnv('tenant-id', 'tenant-id');
224+
setEnv('subscription-id', 'subscription-id');
225+
setEnv('client-id', INJECT_RAW);
226+
setEnv('auth-type', 'SERVICE_PRINCIPAL');
227+
228+
let loginConfig = new LoginConfig();
229+
loginConfig.initialize();
230+
jest.spyOn(loginConfig, 'getFederatedToken').mockImplementation(async () => { loginConfig.federatedToken = "fake-token"; });
231+
return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([_, loginScript]) => {
232+
expect(loginScript).toContain(`-ApplicationId '${INJECT_ESCAPED}'`);
233+
expect(loginScript).not.toContain(`-ApplicationId '${INJECT_RAW}'`);
234+
});
235+
});
236+
237+
test('SECURITY: client-id single quote is escaped (user-assigned MI path)', () => {
238+
setEnv('environment', 'azurecloud');
239+
setEnv('enable-AzPSSession', 'true');
240+
setEnv('allow-no-subscriptions', 'true');
241+
setEnv('auth-type', 'IDENTITY');
242+
setEnv('client-id', INJECT_RAW);
243+
244+
let loginConfig = new LoginConfig();
245+
loginConfig.initialize();
246+
return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([_, loginScript]) => {
247+
expect(loginScript).toContain(`-AccountId '${INJECT_ESCAPED}'`);
248+
expect(loginScript).not.toContain(`-AccountId '${INJECT_RAW}'`);
249+
});
250+
});
251+
252+
test('SECURITY: tenant-id and subscription-id single quotes are escaped (system-assigned MI path)', () => {
253+
setEnv('environment', 'azurecloud');
254+
setEnv('enable-AzPSSession', 'true');
255+
setEnv('allow-no-subscriptions', 'false');
256+
setEnv('tenant-id', INJECT_RAW);
257+
setEnv('subscription-id', INJECT_RAW);
258+
setEnv('auth-type', 'IDENTITY');
259+
260+
let loginConfig = new LoginConfig();
261+
loginConfig.initialize();
262+
return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([_, loginScript]) => {
263+
expect(loginScript).toContain(`-Tenant '${INJECT_ESCAPED}'`);
264+
expect(loginScript).toContain(`-Subscription '${INJECT_ESCAPED}'`);
265+
expect(loginScript).not.toContain(`-Tenant '${INJECT_RAW}'`);
266+
expect(loginScript).not.toContain(`-Subscription '${INJECT_RAW}'`);
267+
});
268+
});
269+
270+
test('SECURITY: resourceManagerEndpointUrl single quote is escaped (AzureStack path)', () => {
271+
setEnv('environment', 'azurestack');
272+
setEnv('enable-AzPSSession', 'true');
273+
setEnv('allow-no-subscriptions', 'true');
274+
setEnv('auth-type', 'SERVICE_PRINCIPAL');
275+
let creds = {
276+
'clientId': 'client-id',
277+
'clientSecret': 'client-secret',
278+
'tenantId': 'tenant-id',
279+
'subscriptionId': 'subscription-id',
280+
'resourceManagerEndpointUrl': INJECT_RAW
281+
}
282+
setEnv('creds', JSON.stringify(creds));
283+
284+
let loginConfig = new LoginConfig();
285+
loginConfig.initialize();
286+
return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([_, loginScript]) => {
287+
expect(loginScript).toContain(`-ARMEndpoint '${INJECT_ESCAPED}'`);
288+
expect(loginScript).not.toContain(`-ARMEndpoint '${INJECT_RAW}'`);
289+
});
290+
});
291+
292+
test('SECURITY: escapePSSingleQuoted handles null/undefined without throwing', () => {
293+
setEnv('environment', 'azurecloud');
294+
setEnv('enable-AzPSSession', 'true');
295+
setEnv('allow-no-subscriptions', 'false');
296+
setEnv('auth-type', 'IDENTITY');
297+
298+
let loginConfig = new LoginConfig();
299+
loginConfig.initialize();
300+
return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => {
301+
expect(loginScript).toContain("Connect-AzAccount -Identity -Environment 'azurecloud'");
302+
expect(loginMethod).toBe('system-assigned managed identity');
303+
});
304+
});
305+
153306
});

‎src/PowerShell/AzPSScriptBuilder.ts‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,20 @@ export default class AzPSScriptBuilder {
2121
return script;
2222
}
2323

24+
// Doubles single quotes for safe interpolation into a PowerShell '...' literal.
25+
private static escapePSSingleQuoted(value: string): string {
26+
if (value === null || value === undefined) {
27+
return "";
28+
}
29+
return String(value).split("'").join("''");
30+
}
31+
2432
static async getAzPSLoginScript(loginConfig: LoginConfig) {
2533
let loginMethodName = "";
2634
let commands = "";
2735

2836
if (loginConfig.environment.toLowerCase() == "azurestack") {
29-
commands += `Add-AzEnvironment -Name '${loginConfig.environment}' -ARMEndpoint '${loginConfig.resourceManagerEndpointUrl}' | out-null;`;
37+
commands += `Add-AzEnvironment -Name '${loginConfig.environment}' -ARMEndpoint '${AzPSScriptBuilder.escapePSSingleQuoted(loginConfig.resourceManagerEndpointUrl)}' | out-null;`;
3038
}
3139
if (loginConfig.authType === LoginConfig.AUTH_TYPE_SERVICE_PRINCIPAL) {
3240
if (loginConfig.servicePrincipalSecret) {
@@ -64,10 +72,11 @@ export default class AzPSScriptBuilder {
6472
}
6573

6674
private static loginWithSecret(loginConfig: LoginConfig): string {
67-
let servicePrincipalSecret: string = loginConfig.servicePrincipalSecret.split("'").join("''");
75+
let servicePrincipalSecret: string = AzPSScriptBuilder.escapePSSingleQuoted(loginConfig.servicePrincipalSecret);
76+
let servicePrincipalId: string = AzPSScriptBuilder.escapePSSingleQuoted(loginConfig.servicePrincipalId);
6877
let loginCmdlet = `$psLoginSecrets = ConvertTo-SecureString '${servicePrincipalSecret}' -AsPlainText -Force; `;
69-
loginCmdlet += `$psLoginCredential = New-Object System.Management.Automation.PSCredential('${loginConfig.servicePrincipalId}', $psLoginSecrets); `;
70-
78+
loginCmdlet += `$psLoginCredential = New-Object System.Management.Automation.PSCredential('${servicePrincipalId}', $psLoginSecrets); `;
79+
7180
let cmdletSuffix = "-Credential $psLoginCredential";
7281
loginCmdlet += AzPSScriptBuilder.psLoginCmdlet(loginConfig.authType, loginConfig.environment, loginConfig.tenantId, loginConfig.subscriptionId, cmdletSuffix);
7382

@@ -76,7 +85,9 @@ export default class AzPSScriptBuilder {
7685

7786
private static async loginWithOIDC(loginConfig: LoginConfig) {
7887
await loginConfig.getFederatedToken();
79-
let cmdletSuffix = `-ApplicationId '${loginConfig.servicePrincipalId}' -FederatedToken '${loginConfig.federatedToken}'`;
88+
let servicePrincipalId: string = AzPSScriptBuilder.escapePSSingleQuoted(loginConfig.servicePrincipalId);
89+
let federatedToken: string = AzPSScriptBuilder.escapePSSingleQuoted(loginConfig.federatedToken);
90+
let cmdletSuffix = `-ApplicationId '${servicePrincipalId}' -FederatedToken '${federatedToken}'`;
8091
return AzPSScriptBuilder.psLoginCmdlet(loginConfig.authType, loginConfig.environment, loginConfig.tenantId, loginConfig.subscriptionId, cmdletSuffix);
8192
}
8293

@@ -86,7 +97,8 @@ export default class AzPSScriptBuilder {
8697
}
8798

8899
static loginWithUserAssignedIdentity(loginConfig: LoginConfig): string {
89-
let cmdletSuffix = `-AccountId '${loginConfig.servicePrincipalId}'`;
100+
let servicePrincipalId: string = AzPSScriptBuilder.escapePSSingleQuoted(loginConfig.servicePrincipalId);
101+
let cmdletSuffix = `-AccountId '${servicePrincipalId}'`;
90102
return AzPSScriptBuilder.psLoginCmdlet(loginConfig.authType, loginConfig.environment, loginConfig.tenantId, loginConfig.subscriptionId, cmdletSuffix);
91103
}
92104

@@ -99,10 +111,10 @@ export default class AzPSScriptBuilder {
99111
}
100112
loginCmdlet += `-Environment '${environment}' `;
101113
if(tenantId){
102-
loginCmdlet += `-Tenant '${tenantId}' `;
114+
loginCmdlet += `-Tenant '${AzPSScriptBuilder.escapePSSingleQuoted(tenantId)}' `;
103115
}
104116
if(subscriptionId){
105-
loginCmdlet += `-Subscription '${subscriptionId}' `;
117+
loginCmdlet += `-Subscription '${AzPSScriptBuilder.escapePSSingleQuoted(subscriptionId)}' `;
106118
}
107119
loginCmdlet += `${cmdletSuffix} -InformationAction Ignore | out-null;`;
108120
return loginCmdlet;

‎src/common/Utils.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { AzPSConstants, AzPSUtils } from '../PowerShell/AzPSUtils';
77
export function setUserAgent(): void {
88
let usrAgentRepo = crypto.createHash('sha256').update(`${process.env.GITHUB_REPOSITORY}`).digest('hex');
99
let actionName = 'AzureLogin';
10-
process.env.AZURE_HTTP_USER_AGENT = (!!process.env.AZURE_HTTP_USER_AGENT ? `${process.env.AZURE_HTTP_USER_AGENT} ` : '') + `GITHUBACTIONS/${actionName}@v2_${usrAgentRepo}_${process.env.RUNNER_ENVIRONMENT}_${process.env.GITHUB_RUN_ID}`;
11-
process.env.AZUREPS_HOST_ENVIRONMENT = (!!process.env.AZUREPS_HOST_ENVIRONMENT ? `${process.env.AZUREPS_HOST_ENVIRONMENT} ` : '') + `GITHUBACTIONS/${actionName}@v2_${usrAgentRepo}_${process.env.RUNNER_ENVIRONMENT}_${process.env.GITHUB_RUN_ID}`;
10+
process.env.AZURE_HTTP_USER_AGENT = (!!process.env.AZURE_HTTP_USER_AGENT ? `${process.env.AZURE_HTTP_USER_AGENT} ` : '') + `GITHUBACTIONS/${actionName}@v3_${usrAgentRepo}_${process.env.RUNNER_ENVIRONMENT}_${process.env.GITHUB_RUN_ID}`;
11+
process.env.AZUREPS_HOST_ENVIRONMENT = (!!process.env.AZUREPS_HOST_ENVIRONMENT ? `${process.env.AZUREPS_HOST_ENVIRONMENT} ` : '') + `GITHUBACTIONS/${actionName}@v3_${usrAgentRepo}_${process.env.RUNNER_ENVIRONMENT}_${process.env.GITHUB_RUN_ID}`;
1212
}
1313

1414
export async function cleanupAzCLIAccounts(): Promise<void> {

0 commit comments

Comments
 (0)