Skip to content

Fix env var leak when running extension commands with -e flag#7035

Draft
Copilot wants to merge 4 commits intomainfrom
copilot/fix-env-variable-leak
Draft

Fix env var leak when running extension commands with -e flag#7035
Copilot wants to merge 4 commits intomainfrom
copilot/fix-env-variable-leak

Conversation

Copy link
Contributor

Copilot AI commented Mar 6, 2026

Extension commands use DisableFlagParsing: true, so cobra never parses -e/--environment. This caused lazyEnv to always resolve to the default environment, leaking its variables into the extension process even when a different environment was explicitly requested. AZD_ENVIRONMENT was also never set in these cases.

Root cause

Two call sites both relied on cmd.Flags().GetString("environment") which returns "" for any command with DisableFlagParsing: true:

  • The EnvFlag DI resolver in container.go — controls which environment lazyEnv loads
  • extensionAction.Run() in extensions.go — controls InvokeOptions.Environment (i.e., AZD_ENVIRONMENT propagation)

Changes

Follows the established GlobalCommandOptions pattern used by --cwd, --debug, and --no-prompt:

  • internal/global_command_options.go — Added EnvironmentName field to GlobalCommandOptions
  • cmd/auto_install.goParseGlobalFlags() now pre-parses -e/--environment from raw args before cobra runs, storing the value in GlobalCommandOptions.EnvironmentName. The flag is added only to the parsing flag set (not to CreateGlobalFlagSet()) so it doesn't appear in every command's help text.
  • cmd/container.goEnvFlag DI resolver now accepts globalOptions *internal.GlobalCommandOptions and falls back to globalOptions.EnvironmentName when cmd.Flags() doesn't have the value (e.g. extension commands with DisableFlagParsing: true)
  • cmd/extensions.goextensionAction.Run() now uses a.globalOptions directly for all InvokeOptions fields (Debug, Cwd, Environment, NoPrompt), removing the fragile cmd.Flags().GetString() calls that silently fail for extension commands
  • TestsTestParseGlobalFlags_EnvironmentName validates the early parsing with short flags, long flags, equals syntax, and mixed with unknown flags
Original prompt

This section details on the original issue you should resolve

<issue_title>azd -e leaks default environment variables into extension process</issue_title>
<issue_description>## Description

When running an extension command with -e <env> to specify a non-default environment, environment variables from the default environment leak into the extension process. Variables that exist only in the default environment (and are not overridden by the -e environment) are visible to the extension and its child processes.

Steps to Reproduce

  1. Create a project with two environments:

    azd env new dev
    azd env new staging
    azd env select staging   # make staging the default
    
  2. Set a variable ONLY in staging:

    azd env set MY_STAGING_VAR "staging-value" -e staging
    
  3. Set a different variable in dev:

    azd env set MY_DEV_VAR "dev-value" -e dev
    
  4. Run an extension with -e dev:

    azd app run -e dev
    
  5. In the child process, BOTH MY_STAGING_VAR and MY_DEV_VAR are present in the environment.

Expected Behavior

Only variables from the -e dev environment should be present. MY_STAGING_VAR (which exists only in the staging/default environment) should NOT leak into the extension process.

Actual Behavior

The extension process receives a merged set of environment variables: default environment values as the base, with -e environment values overlaid on top. Any variable that exists ONLY in the default environment leaks through because the -e environment doesn't define it to override.

Root Cause Analysis

Traced through the source code:

cli/azd/cmd/extensions.goextensionAction.Run():

allEnv := []string{}
allEnv = append(allEnv, os.Environ()...)          // system env

env, err := a.lazyEnv.GetValue()                   // resolves to... which env?
if err == nil && env != nil {
    allEnv = append(allEnv, env.Environ()...)      // injects env values
}

The problem is that lazyEnv resolves to the default environment rather than the -e environment. This is because extension commands are registered with DisableFlagParsing: true:

cmd := &cobra.Command{
    Use:                lastPart,
    DisableFlagParsing: true,  // prevents -e from being parsed by cobra
}

With DisableFlagParsing: true, cobra does not parse persistent flags (including -e) for extension commands. The flag value is passed through to the extension as args, but a.cmd.Flags().GetString("environment") returns "". This causes the DI-injected lazyEnv to fall back to the default environment.

Additionally, the envName extraction logic also hits this wall:

envName, _ := a.cmd.Flags().GetString("environment")  // returns "" due to DisableFlagParsing
options := &extensions.InvokeOptions{
    Environment: envName,  // "" → AZD_ENVIRONMENT not set
}

Since envName is "", AZD_ENVIRONMENT is not propagated to the extension process either (the runner.go guard checks if options.Environment != "").

Impact

This is a data isolation bug. Environment-specific secrets, API endpoints, feature flags, and configuration from the default environment leak into non-default environment runs. This can cause:

  • Wrong API endpoints being used
  • Feature flags from one environment activating in another
  • Staging-specific behavior appearing in dev (or vice versa)
  • Secrets from one environment being visible in another

Suggested Fix

Ensure that when -e <env> is specified, lazyEnv resolves to that environment, not the default. Options:

  1. Parse -e before dispatching to extensions: Extract the -e/--environment flag value from args before DisableFlagParsing takes effect, and use it for DI environment resolution.
  2. Use the middleware path: The middleware-based extension execution path (cli/azd/cmd/middleware/extensions.go) may handle flag parsing differently via m.options.Flags.GetString("environment"). Ensure parity between the two execution paths.
  3. Don't inject lazyEnv.Environ() for extension commands: Let extensions query the gRPC EnvironmentService for values explicitly, rather than inheriting them via process environment.

Workaround

Run azd env select <env> before running the extension command to make the desired environment the default:

azd env select dev
azd app run    # now uses dev as default, no -e needed

Environment

  • azd version: 1.23.7
  • OS: Windows 11
  • Extension: jongio/azd-app v0.13.2 (but affects all extensions that rely on inherited env vars)</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Co-authored-by: jongio <2163001+jongio@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix environment variable leak into extension process Fix env var leak when running extension commands with -e flag Mar 6, 2026
@jongio
Copy link
Member

jongio commented Mar 7, 2026

@copilot that looks like a lazy implementation, fix it with better design principles

Instead of scattered parseEnvFlagFromArgs + DisableFlagParsing checks,
follow the established pattern for --cwd/--debug/--no-prompt:

1. Add EnvironmentName to GlobalCommandOptions
2. Parse -e/--environment in ParseGlobalFlags() before cobra runs
3. Use globalOptions.EnvironmentName as fallback in EnvFlag DI resolver
4. Use a.globalOptions directly in extensionAction.Run() for InvokeOptions

Co-authored-by: jongio <2163001+jongio@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 7, 2026

@copilot that looks like a lazy implementation, fix it with better design principles

Redesigned in 79d1f16. Instead of scattered parseEnvFlagFromArgs + DisableFlagParsing checks, this now follows the established GlobalCommandOptions pattern used by --cwd, --debug, and --no-prompt:

  1. Added EnvironmentName to GlobalCommandOptions
  2. ParseGlobalFlags() pre-parses -e/--environment from raw args before cobra runs — single source of truth
  3. container.go EnvFlag resolver falls back to globalOptions.EnvironmentName
  4. extensions.go uses a.globalOptions directly for all InvokeOptions fields

No more DisableFlagParsing checks, no standalone helper functions, no extra DI parameters.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • aka.ms
    • Triggering command: /home/REDACTED/work/azure-dev/azure-dev/cli/azd/azd /home/REDACTED/work/azure-dev/azure-dev/cli/azd/azd extension source add -n local -t file -l /home/REDACTED/work/azure-dev/azure-dev/cli/azd/extensions/registry.json -o x_amd64/vet -p st/mocks/mockexe-errorsas olang.org/grpc@v-ifaceassert x_amd64/vet storage/items.go/opt/hostedtoolcache/go/1.26.1/x64/pkg/tool/linux_amd64/vet ce/internal/env -I x_amd64/vet (dns block)
    • Triggering command: /home/REDACTED/work/azure-dev/azure-dev/cli/azd/azd /home/REDACTED/work/azure-dev/azure-dev/cli/azd/azd extension list --source local --output json x_amd64/vet --gdwarf-5 --64 -o x_amd64/vet -o st/mocks/mockhtt-errorsas olang.org/protob-ifaceassert x_amd64/vet -p noop -lang=go1.24 x_amd64/vet (dns block)
    • Triggering command: /home/REDACTED/work/azure-dev/azure-dev/cli/azd/azd /home/REDACTED/work/azure-dev/azure-dev/cli/azd/azd extension install azure.ai.agents --source local --version 0.1.13-preview --gdwarf-5 --64 -o x_amd64/vet -o 3@v3.2.0/recorde-errorsas olang.org/grpc@v-ifaceassert x_amd64/vet base.go /v1.39.0/otelcon-atomic -lang=go1.26 x_amd64/vet (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Co-authored-by: jongio <2163001+jongio@users.noreply.github.com>
@microsoft-github-policy-service microsoft-github-policy-service bot added the no-recent-activity identity issues with no activity label Mar 14, 2026
@microsoft-github-policy-service
Copy link
Contributor

Hi @@copilot. Thank you for your interest in helping to improve the Azure Developer CLI experience and for your contribution. We've noticed that there hasn't been recent engagement on this pull request. If this is still an active work stream, please let us know by pushing some changes or leaving a comment. Otherwise, we'll close this out in 7 days.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-recent-activity identity issues with no activity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

azd <extension> -e <env> leaks default environment variables into extension process

2 participants