Skip to content

Conversation

@Kinin-Code-Offical
Copy link
Owner

@Kinin-Code-Offical Kinin-Code-Offical commented Dec 21, 2025

Potential fix for https://github.com/Kinin-Code-Offical/cloudsqlctl/security/code-scanning/3

General approach: Ensure that any file path used as an executable for execa (or passed into PowerShell) is strictly controlled and not directly derived from tainted data in a way that could lead to arbitrary command execution. In practice here, we should (1) avoid passing a tainted installerPath as the process to launch and instead rely on a known runtime (like powershell or cmd) with a fixed command, or (2) validate and restrict installerPath so it cannot be abused, or (3) ensure that in all code paths the tainted string is only ever used as a data argument, not as an executable.

Best fix with minimal behavior change: In the non-elevated branch of applyUpdateInstaller, replace execa(installerPath, args) with a PowerShell invocation similar to the elevated path, where installerPath and arguments are passed as data. This keeps the functional behavior (we still run the installer with the same arguments), but instead of treating the tainted installerPath as the executable directly, we always execute a fixed, trusted program (powershell) and let it run the installer path that we control (downloaded to our chosen directory and checksum-verified). To maintain the behavior of waiting for the installer to finish and propagating errors, we can pass -Wait and -PassThru in PowerShell and keep using await execa(...) to observe exit codes. The elevated and non-elevated branches will both use the same pattern: execute powershell with a small inline script and environment variables holding the path and arguments.

Concretely, in src/core/selfUpdate.ts:

  • In applyUpdateInstaller, keep the current elevated branch but extend the non-elevated branch to use PowerShell instead of directly calling execa(installerPath, args).
  • Build an environment-variable-based approach (as in the elevated case) for the non-elevated branch (e.g., PS_INSTALLER_PATH, PS_INSTALLER_ARGS), and use a simple PowerShell command that reads those variables and calls Start-Process -FilePath $p -ArgumentList $a -Wait (without -Verb RunAs for non-elevated).
  • This does not change external behavior (we still launch the installer with optional silent flags; the user already must be on Windows for this branch to be relevant) but satisfies CodeQL by making the only executable invoked (powershell) a hard-coded, trusted string and treating installerPath as data.

No changes are needed in src/commands/upgrade.ts because downloadPath remains a data argument passed through to applyUpdateInstaller / applyUpdatePortableExe; hardening the sink in applyUpdateInstaller is sufficient for this alert.


Suggested fixes powered by Copilot Autofix. Review carefully before merging.

Summary by Sourcery

Enhancements:

  • Unify elevated and non-elevated installer invocation to use environment-variable-based PowerShell Start-Process commands, reducing command injection risk.

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@sourcery-ai
Copy link

sourcery-ai bot commented Dec 21, 2025

Reviewer's Guide

Refactors the Windows self-update installer launch path to always invoke a fixed PowerShell executable and pass the (potentially tainted) installer path and arguments via environment variables, eliminating uncontrolled command execution while preserving behavior for elevated and non-elevated runs.

Sequence diagram for updated applyUpdateInstaller PowerShell invocation

sequenceDiagram
    actor User
    participant CLI as cloudsqlctl
    participant SelfUpdate as selfUpdate_applyUpdateInstaller
    participant Execa as execa
    participant PS as powershell
    participant Installer

    User->>CLI: run upgrade command
    CLI->>SelfUpdate: applyUpdateInstaller(installerPath, silent, elevate)
    SelfUpdate->>SelfUpdate: build args array (silent flags)
    SelfUpdate->>SelfUpdate: set envVars PS_INSTALLER_PATH, PS_INSTALLER_ARGS
    SelfUpdate->>SelfUpdate: build basePsCommand
    alt elevate is true
        SelfUpdate->>Execa: execa(powershell, [-NoProfile, -NonInteractive, -Command, psCommand], env)
        Execa->>PS: start PowerShell process
        PS->>PS: $p = GetEnvironmentVariable(PS_INSTALLER_PATH)
        PS->>PS: $a = GetEnvironmentVariable(PS_INSTALLER_ARGS)
        PS->>Installer: Start-Process -FilePath $p -ArgumentList $a -Verb RunAs -Wait
        Installer-->>PS: exit code
        PS-->>Execa: process exit
        Execa-->>SelfUpdate: resolved or rejected promise
    else elevate is false
        SelfUpdate->>Execa: execa(powershell, [-NoProfile, -NonInteractive, -Command, psCommand], env)
        Execa->>PS: start PowerShell process
        PS->>PS: $p = GetEnvironmentVariable(PS_INSTALLER_PATH)
        PS->>PS: $a = GetEnvironmentVariable(PS_INSTALLER_ARGS)
        PS->>Installer: Start-Process -FilePath $p -ArgumentList $a -Wait
        Installer-->>PS: exit code
        PS-->>Execa: process exit
        Execa-->>SelfUpdate: resolved or rejected promise
    end
    SelfUpdate-->>CLI: completion or error
    CLI-->>User: report update result
Loading

File-Level Changes

Change Details Files
Harden self-update installer execution by routing both elevated and non-elevated runs through a fixed PowerShell command with environment-variable–based arguments instead of executing the downloaded installer path directly.
  • Initialize installer argument list with an explicit string[] type and conditionally append silent install flags.
  • Create shared environment variables (PS_INSTALLER_PATH, PS_INSTALLER_ARGS) populated from the installer path and joined argument string.
  • Introduce a shared PowerShell prelude script (basePsCommand) that reads installer path and arguments from environment variables.
  • Retain the elevated branch but update it to build its PowerShell command by prefixing basePsCommand and then calling Start-Process with -Verb RunAs and -Wait.
  • Replace the non-elevated execa(installerPath, args) call with an execa invocation of powershell -NoProfile -NonInteractive -Command <script> that uses basePsCommand and Start-Process -Wait, passing through the environment variables.
src/core/selfUpdate.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@Kinin-Code-Offical Kinin-Code-Offical marked this pull request as ready for review December 21, 2025 18:26
Copilot AI review requested due to automatic review settings December 21, 2025 18:26
@Kinin-Code-Offical Kinin-Code-Offical merged commit 5be6be3 into main Dec 21, 2025
6 of 7 checks passed
@Kinin-Code-Offical Kinin-Code-Offical deleted the alert-autofix-3 branch December 21, 2025 18:26
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `src/core/selfUpdate.ts:181-183` </location>
<code_context>
-    if (elevate) {
-        // Use PowerShell Start-Process -Verb RunAs
-        // To prevent command injection, we pass arguments via environment variables.
-        const envVars: Record<string, string> = {
-            'PS_INSTALLER_PATH': installerPath,
-            'PS_INSTALLER_ARGS': args.join(' ')
-        };
+    // Use PowerShell Start-Process with environment variables for both elevated and non-elevated runs
</code_context>

<issue_to_address>
**issue (bug_risk):** Combining arguments into a single space-separated string risks incorrect parsing when arguments contain spaces or special characters.

Previously `execa(installerPath, args)` kept each argument separate, but now all args are collapsed into one string and passed via `PS_INSTALLER_ARGS` to `Start-Process -ArgumentList`. Any arg with spaces or special chars may be split or misparsed by PowerShell. To avoid this, either preserve the array structure (e.g., one env var per arg and rebuild an array in PowerShell) or encode the args in a format that can be safely reconstructed (e.g., JSON + `ConvertFrom-Json`).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +181 to +183
'PS_INSTALLER_ARGS': args.join(' ')
};

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Combining arguments into a single space-separated string risks incorrect parsing when arguments contain spaces or special characters.

Previously execa(installerPath, args) kept each argument separate, but now all args are collapsed into one string and passed via PS_INSTALLER_ARGS to Start-Process -ArgumentList. Any arg with spaces or special chars may be split or misparsed by PowerShell. To avoid this, either preserve the array structure (e.g., one env var per arg and rebuild an array in PowerShell) or encode the args in a format that can be safely reconstructed (e.g., JSON + ConvertFrom-Json).

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a security vulnerability (code scanning alert #3) related to uncontrolled command line execution in the self-update functionality. The fix prevents potential command injection by ensuring that the installer path from potentially tainted data is never directly executed as a process. Instead, both elevated and non-elevated code paths now invoke a fixed, trusted executable (PowerShell) and pass the installer path as data via environment variables.

Key Changes:

  • Modified applyUpdateInstaller to use PowerShell Start-Process for both elevated and non-elevated installer execution
  • Replaced direct execa(installerPath, args) call with environment-variable-based PowerShell invocation
  • Unified the security approach across both execution paths
Comments suppressed due to low confidence (1)

src/core/selfUpdate.ts:109

  • Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped.
            response.data.pipe(writer);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +181 to +186
'PS_INSTALLER_ARGS': args.join(' ')
};

const basePsCommand = `
$p = [System.Environment]::GetEnvironmentVariable('PS_INSTALLER_PATH')
$a = [System.Environment]::GetEnvironmentVariable('PS_INSTALLER_ARGS')
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PowerShell Start-Process -ArgumentList parameter expects an array, but a single joined string is being passed. When arguments are joined with spaces into a single string and passed to PowerShell's ArgumentList parameter, PowerShell will treat the entire string as a single argument rather than parsing it into multiple arguments.

For example, if args = ['/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART'], joining them with spaces produces '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART', which PowerShell will pass as one argument to the installer instead of three separate arguments.

Consider passing the arguments as an array to PowerShell. One approach is to use PowerShell's comma-separated syntax or pass each argument individually through environment variables (e.g., PS_INSTALLER_ARG_0, PS_INSTALLER_ARG_1, etc.) and reconstruct the array in PowerShell, or use a JSON-encoded array that PowerShell can parse.

Suggested change
'PS_INSTALLER_ARGS': args.join(' ')
};
const basePsCommand = `
$p = [System.Environment]::GetEnvironmentVariable('PS_INSTALLER_PATH')
$a = [System.Environment]::GetEnvironmentVariable('PS_INSTALLER_ARGS')
'PS_INSTALLER_ARGS_JSON': JSON.stringify(args)
};
const basePsCommand = `
$p = [System.Environment]::GetEnvironmentVariable('PS_INSTALLER_PATH')
$aJson = [System.Environment]::GetEnvironmentVariable('PS_INSTALLER_ARGS_JSON')
if ([string]::IsNullOrWhiteSpace($aJson)) {
$a = @()
} else {
$a = $aJson | ConvertFrom-Json
}

Copilot uses AI. Check for mistakes.
Comment on lines +189 to +208
if (elevate) {
// Elevated: use -Verb RunAs
const psCommand = `
$p = [System.Environment]::GetEnvironmentVariable('PS_INSTALLER_PATH')
$a = [System.Environment]::GetEnvironmentVariable('PS_INSTALLER_ARGS')
${basePsCommand}
Start-Process -FilePath $p -ArgumentList $a -Verb RunAs -Wait
`.trim();

await execa('powershell', ['-NoProfile', '-NonInteractive', '-Command', psCommand], {
env: { ...process.env, ...envVars }
});
} else {
await execa(installerPath, args);
// Non-elevated: run without -Verb RunAs
const psCommand = `
${basePsCommand}
Start-Process -FilePath $p -ArgumentList $a -Wait
`.trim();

await execa('powershell', ['-NoProfile', '-NonInteractive', '-Command', psCommand], {
env: { ...process.env, ...envVars }
});
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is code duplication between the elevated and non-elevated branches. Both branches construct nearly identical PowerShell commands and invoke execa with the same parameters, differing only by the presence of '-Verb RunAs' in the Start-Process call.

Consider extracting the common logic to reduce duplication. For example, construct the Start-Process line conditionally and use a single execa invocation.

Copilot uses AI. Check for mistakes.
Comment on lines 170 to 210
export async function applyUpdateInstaller(installerPath: string, silent: boolean, elevate: boolean) {
logger.info('Launching installer...');

const args = [];
const args: string[] = [];
if (silent) {
args.push('/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART');
}

if (elevate) {
// Use PowerShell Start-Process -Verb RunAs
// To prevent command injection, we pass arguments via environment variables.
const envVars: Record<string, string> = {
'PS_INSTALLER_PATH': installerPath,
'PS_INSTALLER_ARGS': args.join(' ')
};
// Use PowerShell Start-Process with environment variables for both elevated and non-elevated runs
const envVars: Record<string, string> = {
'PS_INSTALLER_PATH': installerPath,
'PS_INSTALLER_ARGS': args.join(' ')
};

const basePsCommand = `
$p = [System.Environment]::GetEnvironmentVariable('PS_INSTALLER_PATH')
$a = [System.Environment]::GetEnvironmentVariable('PS_INSTALLER_ARGS')
`.trim();

if (elevate) {
// Elevated: use -Verb RunAs
const psCommand = `
$p = [System.Environment]::GetEnvironmentVariable('PS_INSTALLER_PATH')
$a = [System.Environment]::GetEnvironmentVariable('PS_INSTALLER_ARGS')
${basePsCommand}
Start-Process -FilePath $p -ArgumentList $a -Verb RunAs -Wait
`.trim();

await execa('powershell', ['-NoProfile', '-NonInteractive', '-Command', psCommand], {
env: { ...process.env, ...envVars }
});
} else {
await execa(installerPath, args);
// Non-elevated: run without -Verb RunAs
const psCommand = `
${basePsCommand}
Start-Process -FilePath $p -ArgumentList $a -Wait
`.trim();

await execa('powershell', ['-NoProfile', '-NonInteractive', '-Command', psCommand], {
env: { ...process.env, ...envVars }
});
}
}
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The applyUpdateInstaller function lacks test coverage. The existing test file tests/selfUpdate.test.ts tests checkForUpdates and pickAsset, but doesn't include tests for applyUpdateInstaller.

Given the security-sensitive nature of this function (handling installer execution) and that comprehensive automated testing exists for this module, consider adding tests to verify that the PowerShell command is constructed correctly for both elevated and non-elevated cases, and that arguments are properly passed.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants