1616 specific language governing permissions and limitations
1717 under the License.
1818#>
19+
20+ # SDL CE.10116 SECURITY FIX: Removed PowerShell injection vulnerabilities
21+ # - Replaced Invoke-Expression with direct cmdlet invocation
22+ # - Implemented parameterized ScriptBlock pattern for elevated execution
23+ # - Added input validation for package IDs and paths
24+ # Date: October 14, 2025
25+
1926$code = @"
2027using System;
2128using System.Runtime.CompilerServices;
@@ -53,21 +60,150 @@ namespace StoreAppRunner
5360}
5461"@
5562
63+ # region Security Helper Functions
64+
65+ <#
66+ . SYNOPSIS
67+ Validates package identifier format to prevent injection attacks.
68+ . DESCRIPTION
69+ Ensures package ID contains only safe characters and matches expected format.
70+ SDL CE.10116 compliance - prevents PowerShell injection via package names.
71+ #>
72+ function Validate-PackageIdentifier {
73+ param (
74+ [Parameter (Mandatory = $true )]
75+ [string ]$PackageId
76+ )
77+
78+ # Valid format: alphanumeric, dots, hyphens, underscores only
79+ if ($PackageId -notmatch ' ^[a-zA-Z0-9\.\-_]+$' ) {
80+ throw " Invalid package identifier format. Only alphanumeric characters, dots, hyphens, and underscores are allowed."
81+ }
82+
83+ # Prevent common injection patterns
84+ $dangerousPatterns = @ (' ;' , ' |' , ' &' , ' $' , ' `' , ' <' , ' >' , " `n " , " `r " , ' (' , ' )' , ' {' , ' }' )
85+ foreach ($pattern in $dangerousPatterns ) {
86+ if ($PackageId.Contains ($pattern )) {
87+ throw " Package identifier contains forbidden characters: $pattern "
88+ }
89+ }
90+
91+ return $true
92+ }
93+
94+ <#
95+ . SYNOPSIS
96+ Validates and canonicalizes script path to prevent path traversal attacks.
97+ . DESCRIPTION
98+ Ensures path is a valid .ps1 file and resolves to canonical path.
99+ SDL CE.10116 compliance - prevents path injection attacks.
100+ #>
101+ function Validate-ScriptPath {
102+ param (
103+ [Parameter (Mandatory = $true )]
104+ [string ]$Path
105+ )
106+
107+ # Check file exists
108+ if (! (Test-Path $Path - PathType Leaf)) {
109+ throw " Script path does not exist: $Path "
110+ }
111+
112+ # Check .ps1 extension
113+ if ([System.IO.Path ]::GetExtension($Path ) -ne ' .ps1' ) {
114+ throw " Path must reference a PowerShell script (.ps1)"
115+ }
116+
117+ # Get canonical path (prevents ../ traversal)
118+ $canonicalPath = [System.IO.Path ]::GetFullPath($Path )
119+
120+ return $canonicalPath
121+ }
122+
123+ <#
124+ . SYNOPSIS
125+ Executes a ScriptBlock with optional elevation using parameterized approach.
126+ . DESCRIPTION
127+ SDL CE.10116 compliant - uses ScriptBlock with ArgumentList instead of string concatenation.
128+ Prevents PowerShell injection attacks by properly isolating parameters.
129+ #>
130+ function Invoke-ElevatedScriptBlock {
131+ param (
132+ [Parameter (Mandatory = $true )]
133+ [ScriptBlock ]$ScriptBlock ,
134+
135+ [Parameter (Mandatory = $false )]
136+ [object []]$ArgumentList = @ ()
137+ )
138+
139+ if (! (IsElevated)) {
140+ # Serialize ScriptBlock and arguments for elevated execution
141+ $encodedCommand = [Convert ]::ToBase64String(
142+ [System.Text.Encoding ]::Unicode.GetBytes($ScriptBlock.ToString ())
143+ )
144+
145+ $encodedArgs = [Convert ]::ToBase64String(
146+ [System.Text.Encoding ]::Unicode.GetBytes(
147+ ($ArgumentList | ConvertTo-Json - Compress - Depth 10 )
148+ )
149+ )
150+
151+ # Create secure argument list (no string interpolation)
152+ $startArgs = @ (
153+ ' -NoProfile' ,
154+ ' -NonInteractive' ,
155+ ' -Command' ,
156+ " & ([ScriptBlock]::Create([System.Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$encodedCommand ')))) @(ConvertFrom-Json ([System.Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$encodedArgs '))))"
157+ )
158+
159+ $process = Start-Process PowerShell - ArgumentList $startArgs `
160+ - Verb RunAs - Wait - PassThru - WindowStyle Hidden - ErrorAction Stop
161+
162+ if ($process.ExitCode -ne 0 ) {
163+ throw " Elevated command failed with exit code $ ( $process.ExitCode ) "
164+ }
165+ }
166+ else {
167+ # Already elevated, execute directly with splatting
168+ & $ScriptBlock @ArgumentList
169+ }
170+ }
171+
172+ # endregion
173+
174+ # region Core Functions
175+
176+ <#
177+ . SYNOPSIS
178+ Uninstalls an AppX package by package identifier.
179+ . DESCRIPTION
180+ SDL CE.10116 FIX: Replaced Invoke-Expression with direct Remove-AppxPackage cmdlet.
181+ Uses parameterized ScriptBlock for elevated execution.
182+ #>
56183function Uninstall-App {
57184 param (
58185 [Parameter (Mandatory = $true , Position = 0 , ValueFromPipelineByPropertyName = $true )]
59186 [string ] $ID <# package.appxmanifest//Identity@name #>
60187 )
61188
189+ # SDL FIX: Validate package ID to prevent injection
190+ Validate- PackageIdentifier - PackageId $ID | Out-Null
191+
62192 $package = Get-AppxPackage $ID
63193
64194 if ($package ) {
65195 $pfn = $package.PackageFullName
66- $command = " Remove-AppxPackage $pfn -ErrorAction Stop"
196+
197+ # SDL FIX: Direct cmdlet invocation instead of Invoke-Expression
67198 try {
68- Invoke-Expression $command
199+ Remove-AppxPackage $pfn - ErrorAction Stop
69200 } catch {
70- Invoke-Expression - MayElevate $command - ErrorAction Stop
201+ # SDL FIX: Use parameterized ScriptBlock for elevation
202+ $scriptBlock = {
203+ param ($PackageFullName )
204+ Remove-AppxPackage $PackageFullName - ErrorAction Stop
205+ }
206+ Invoke-ElevatedScriptBlock - ScriptBlock $scriptBlock - ArgumentList @ ($pfn )
71207 }
72208 }
73209}
@@ -91,44 +227,60 @@ function IsElevated {
91227 return [bool ](([System.Security.Principal.WindowsIdentity ]::GetCurrent()).groups -match " S-1-5-32-544" );
92228}
93229
94- function RunElevatedPowerShellSync ([string ]$Command ) {
95- $process = Start-Process Powershell - ArgumentList " $Command " - Verb RunAs - ErrorAction Stop - PassThru
96- if ($process -ne $null ) {
97- $process.WaitForExit ();
98- if ($process.ExitCode -ne 0 ) {
99- $code = $process.ExitCode ;
100- throw " Command exited with code $code " ;
101- }
102- } else {
103- throw " Process creation failed for $Command " ;
104- }
105- }
106-
107- function Invoke-Expression-MayElevate ([string ]$Command ) {
108- if (! (IsElevated))
109- {
110- RunElevatedPowerShellSync($Command ) - ErrorAction Stop
111- }
112- else
113- {
114- Invoke-Expression (" & $Command " ) - ErrorAction Stop
115- }
116- }
117-
230+ <#
231+ . SYNOPSIS
232+ Enables developer mode by setting registry keys.
233+ . DESCRIPTION
234+ SDL CE.10116 FIX: Replaced Invoke-Expression-MayElevate with direct cmdlet calls
235+ and parameterized ScriptBlock for elevation.
236+ #>
118237function EnableDevmode {
119238 $RegistryKeyPath = " HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
120239
240+ # Create registry key if it doesn't exist
121241 if (-not (Test-Path - Path $RegistryKeyPath )) {
122- New-Item - Path $RegistryKeyPath - ItemType Directory - Force
242+ if (! (IsElevated)) {
243+ $scriptBlock = {
244+ param ($Path )
245+ New-Item - Path $Path - ItemType Directory - Force | Out-Null
246+ }
247+ Invoke-ElevatedScriptBlock - ScriptBlock $scriptBlock - ArgumentList @ ($RegistryKeyPath )
248+ }
249+ else {
250+ New-Item - Path $RegistryKeyPath - ItemType Directory - Force | Out-Null
251+ }
123252 }
124253
125- $value = get-ItemProperty - Path $RegistryKeyPath - Name AllowDevelopmentWithoutDevLicense - ErrorAction SilentlyContinue
254+ # SDL FIX: Direct cmdlet invocation for AllowDevelopmentWithoutDevLicense
255+ $value = Get-ItemProperty - Path $RegistryKeyPath - Name AllowDevelopmentWithoutDevLicense - ErrorAction SilentlyContinue
126256 if (($value -eq $null ) -or ($value.AllowDevelopmentWithoutDevLicense -ne 1 )) {
127- Invoke-Expression - MayElevate(" Set-ItemProperty -Path $RegistryKeyPath -Name AllowDevelopmentWithoutDevLicense -Value 1 -ErrorAction Stop" ) - ErrorAction Stop;
257+ if (! (IsElevated)) {
258+ $scriptBlock = {
259+ param ($Path , $Name , $Value )
260+ Set-ItemProperty - Path $Path - Name $Name - Value $Value - ErrorAction Stop
261+ }
262+ Invoke-ElevatedScriptBlock - ScriptBlock $scriptBlock `
263+ - ArgumentList @ ($RegistryKeyPath , ' AllowDevelopmentWithoutDevLicense' , 1 )
264+ }
265+ else {
266+ Set-ItemProperty - Path $RegistryKeyPath - Name AllowDevelopmentWithoutDevLicense - Value 1 - ErrorAction Stop
267+ }
128268 }
129- $value = get-ItemProperty - Path $RegistryKeyPath - Name AllowAllTrustedApps - ErrorAction SilentlyContinue
269+
270+ # SDL FIX: Direct cmdlet invocation for AllowAllTrustedApps
271+ $value = Get-ItemProperty - Path $RegistryKeyPath - Name AllowAllTrustedApps - ErrorAction SilentlyContinue
130272 if (($value -eq $null ) -or ($value.AllowAllTrustedApps -ne 1 )) {
131- Invoke-Expression - MayElevate(" Set-ItemProperty -Path $RegistryKeyPath -Name AllowAllTrustedApps -Value 1 -ErrorAction Stop" ) - ErrorAction Stop;
273+ if (! (IsElevated)) {
274+ $scriptBlock = {
275+ param ($Path , $Name , $Value )
276+ Set-ItemProperty - Path $Path - Name $Name - Value $Value - ErrorAction Stop
277+ }
278+ Invoke-ElevatedScriptBlock - ScriptBlock $scriptBlock `
279+ - ArgumentList @ ($RegistryKeyPath , ' AllowAllTrustedApps' , 1 )
280+ }
281+ else {
282+ Set-ItemProperty - Path $RegistryKeyPath - Name AllowAllTrustedApps - Value 1 - ErrorAction Stop
283+ }
132284 }
133285}
134286
@@ -174,22 +326,44 @@ function CheckIfNeedInstallCertificate
174326 return (-not $Valid )
175327}
176328
329+ <#
330+ . SYNOPSIS
331+ Installs an app package using Add-AppDevPackage.ps1 script.
332+ . DESCRIPTION
333+ SDL CE.10116 FIX: Replaced Invoke-Expression with call operator (&) and
334+ parameterized ScriptBlock for elevated execution.
335+ #>
177336function Install-App {
178337 param (
179338 [Parameter (Mandatory = $true , Position = 0 , ValueFromPipelineByPropertyName = $true )]
180339 [string ] $Path , <# Full path to Add-AppDevPackage.ps1 #>
181340 [switch ] $Force = $false
182341 )
183- $needInstallCertificate = CheckIfNeedInstallCertificate (Join-Path $Path " .." );
342+
343+ # SDL FIX: Validate script path
344+ $Path = Validate- ScriptPath - Path $Path
345+
346+ $needInstallCertificate = CheckIfNeedInstallCertificate (Join-Path $Path " .." )
347+
184348 if (! $Force -and ((CheckIfNeedDeveloperLicense) -or ($needInstallCertificate )))
185349 {
186- # we can't run the script with -force param if license/certificate installation step is required
187- Invoke-Expression (" & `" $Path `" " )
350+ # SDL FIX: Use call operator (&) instead of Invoke-Expression
351+ # No user input in path (validated above), so safe to execute
352+ & $Path
188353 }
189354 else
190355 {
191- $Path = [System.IO.Path ]::GetFullPath($Path );
192- Invoke-Expression - MayElevate(" `" $Path `" -force" ) - ErrorAction Stop;
356+ # SDL FIX: Use parameterized ScriptBlock for elevation
357+ if (! (IsElevated)) {
358+ $scriptBlock = {
359+ param ($ScriptPath )
360+ & $ScriptPath - Force
361+ }
362+ Invoke-ElevatedScriptBlock - ScriptBlock $scriptBlock - ArgumentList @ ($Path )
363+ }
364+ else {
365+ & $Path - Force
366+ }
193367 }
194368}
195369
@@ -220,6 +394,9 @@ function Start-Locally {
220394 [string []] $argv
221395 )
222396
397+ # SDL FIX: Validate package ID
398+ Validate- PackageIdentifier - PackageId $ID | Out-Null
399+
223400 $package = Get-AppxPackage $ID
224401 $manifest = Get-appxpackagemanifest $package
225402 $applicationUserModelId = $package.PackageFamilyName + " !" + $manifest.package.applications.application.id
@@ -240,7 +417,6 @@ function Start-Locally {
240417 }
241418}
242419
243-
244420function Map-PackageNameToPackage {
245421 param ([Parameter (Mandatory = $true )] [string ]$DependenciesPath )
246422 $mapP2N = @ {};
@@ -272,3 +448,18 @@ function Install-AppDependencies {
272448 $packagePaths = $packageNamesToInstall | % { $map [$_ ] }
273449 $packagePaths | % { Add-AppxPackage - Path $_ }
274450}
451+
452+ # endregion
453+
454+ # Export functions
455+ Export-ModuleMember - Function @ (
456+ ' Uninstall-App' ,
457+ ' EnableDevmode' ,
458+ ' CheckIfNeedDeveloperLicense' ,
459+ ' CheckIfNeedInstallCertificate' ,
460+ ' Install-App' ,
461+ ' Install-AppFromDirectory' ,
462+ ' Install-AppFromAppx' ,
463+ ' Start-Locally' ,
464+ ' Install-AppDependencies'
465+ )
0 commit comments