diff --git a/contrib/win32/openssh/OpenSSHTestHelper.psm1 b/contrib/win32/openssh/OpenSSHTestHelper.psm1 index 84f3ce07f6d3..2fc90313bd3c 100644 --- a/contrib/win32/openssh/OpenSSHTestHelper.psm1 +++ b/contrib/win32/openssh/OpenSSHTestHelper.psm1 @@ -15,8 +15,9 @@ $PubKeyUser = "sshtest_pubkeyuser" $PasswdUser = "sshtest_passwduser" $AdminUser = "sshtest_adminuser" $NonAdminUser = "sshtest_nonadminuser" +$SshdUser = "sshd_user" $OpenSSHTestAccountsPassword = "Bulldog_123456" -$OpenSSHTestAccounts = $Script:SSOUser, $Script:PubKeyUser, $Script:PasswdUser, $Script:AdminUser, $Script:NonAdminUser +$OpenSSHTestAccounts = $Script:SSOUser, $Script:PubKeyUser, $Script:PasswdUser, $Script:AdminUser, $Script:NonAdminUser, $Script:SshdUser $SSHDTestSvcName = "sshdTestSvc" $Script:TestDataPath = "$env:SystemDrive\OpenSSHTests" @@ -69,6 +70,7 @@ function Set-OpenSSHTestEnvironment $Global:OpenSSHTestInfo.Add("PasswdUser", $PasswdUser) # test user to be used for password auth $Global:OpenSSHTestInfo.Add("AdminUser", $AdminUser) # test user to be used for admin logging tests $Global:OpenSSHTestInfo.Add("NonAdminUser", $NonAdminUser) # test user to be used for non-admin logging tests + $Global:OpenSSHTestInfo.Add("SshdUser", $SshdUser) # non-admin local user used by UserEnvironment tests $Global:OpenSSHTestInfo.Add("TestAccountPW", $OpenSSHTestAccountsPassword) # common password for all test accounts $Global:OpenSSHTestInfo.Add("DebugMode", $DebugMode.IsPresent) # run openssh E2E in debug mode $Global:OpenSSHTestInfo.Add("DelayTime", 3) # delay between stoppig sshd service and trying to access log files @@ -225,6 +227,9 @@ WARNING: Following changes will be made to OpenSSH configuration $NonAdminUserProfile = Get-LocalUserProfile -User $NonAdminUser $Global:OpenSSHTestInfo.Add("NonAdminUserProfile", $NonAdminUserProfile) + $SshdUserProfile = Get-LocalUserProfile -User $SshdUser + $Global:OpenSSHTestInfo.Add("SshdUserProfile", $SshdUserProfile) + #make $AdminUser admin net localgroup Administrators $AdminUser /add diff --git a/contrib/win32/win32compat/spawn-ext.c b/contrib/win32/win32compat/spawn-ext.c index 19b569ff892d..341f80752a6e 100644 --- a/contrib/win32/win32compat/spawn-ext.c +++ b/contrib/win32/win32compat/spawn-ext.c @@ -3,7 +3,7 @@ #include "inc\unistd.h" #include "debug.h" -int posix_spawn_internal(pid_t *pidp, const char *path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv[], char *const envp[], HANDLE user_token, BOOLEAN prepend_module_path); +int posix_spawn_internal(pid_t *pidp, const char *path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv[], char *const envp[], HANDLE user_token, BOOLEAN prepend_module_path, BOOLEAN load_user_env); int __posix_spawn_asuser(pid_t *pidp, const char *path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv[], char *const envp[], char* user) @@ -25,10 +25,15 @@ __posix_spawn_asuser(pid_t *pidp, const char *path, const posix_spawn_file_actio errno = EOTHER; return -1; } - if (strcmp(user, "sshd")) + /* + * This identity check decides whether to load both the user's profile and + * environment block. + */ + BOOLEAN is_privsep = (strcmp(user, "sshd") == 0); + if (!is_privsep) load_user_profile(user_token, user); - - r = posix_spawn_internal(pidp, path, file_actions, attrp, argv, envp, user_token, TRUE); + + r = posix_spawn_internal(pidp, path, file_actions, attrp, argv, envp, user_token, TRUE, !is_privsep); CloseHandle(user_token); return r; } \ No newline at end of file diff --git a/contrib/win32/win32compat/w32fd.c b/contrib/win32/win32compat/w32fd.c index b4c436864dc4..b7aa00ee4383 100644 --- a/contrib/win32/win32compat/w32fd.c +++ b/contrib/win32/win32compat/w32fd.c @@ -1055,45 +1055,6 @@ int fork() } char * build_commandline_string(const char* cmd, char *const argv[], BOOLEAN prepend_module_path); -wchar_t* -get_username_from_token(HANDLE as_user) -{ - wchar_t* user_name = NULL; - SID_NAME_USE usage; - DWORD count = 0; - GetTokenInformation(as_user, TokenUser, NULL, 0, &count); - if (count) { - void* buffer = malloc(count); - if (buffer) { - if (GetTokenInformation(as_user, TokenUser, buffer, count, &count)) { - TOKEN_USER* owner = (TOKEN_USER*)buffer; - DWORD name_length = 0; - DWORD domain_length = 0; - LookupAccountSidW(NULL, owner->User.Sid, NULL, &name_length, NULL, &domain_length, &usage); /* Figure out the length of the name. */ - if (name_length) { - wchar_t* domain_name = malloc(domain_length * sizeof(wchar_t)); - if (domain_name) { - user_name = malloc(name_length * sizeof(wchar_t)); - if (user_name) { - memset(user_name, 0, name_length * sizeof(wchar_t)); - memset(domain_name, 0, domain_length * sizeof(wchar_t)); - BOOL success = LookupAccountSidW(NULL, owner->User.Sid, user_name, &name_length, domain_name, &domain_length, &usage); - if (!success) /* Silently return an empty string if unsuccessful. */ - { - free(user_name); - user_name = NULL; - } - } - free(domain_name); - } - } - } - free(buffer); - } - } - return user_name; -} - /* * spawn a child process * - specified by cmd with agruments argv @@ -1102,14 +1063,15 @@ get_username_from_token(HANDLE as_user) * spawned child will run as as_user if its not NULL */ static int -spawn_child_internal(const char* cmd, char *const argv[], HANDLE in, HANDLE out, HANDLE err, unsigned long flags, HANDLE as_user, BOOLEAN prepend_module_path) +spawn_child_internal(const char* cmd, char *const argv[], HANDLE in, HANDLE out, HANDLE err, unsigned long flags, HANDLE as_user, BOOLEAN prepend_module_path, BOOLEAN load_user_env) { PROCESS_INFORMATION pi; STARTUPINFOW si; - BOOL b; + BOOL b = FALSE; char *cmdline; wchar_t * cmdline_utf16 = NULL; int ret = -1; + if ((cmdline = build_commandline_string(cmd, argv, prepend_module_path)) == NULL) { errno = ENOMEM; goto cleanup; @@ -1125,7 +1087,7 @@ spawn_child_internal(const char* cmd, char *const argv[], HANDLE in, HANDLE out, si.hStdOutput = out; si.hStdError = err; si.dwFlags = STARTF_USESTDHANDLES; - + if (strstr(cmd, "sshd-session.exe") || strstr(cmd, "sshd-auth.exe")) { flags |= DETACHED_PROCESS; } @@ -1146,12 +1108,8 @@ spawn_child_internal(const char* cmd, char *const argv[], HANDLE in, HANDLE out, if (as_user) { debug3("spawning %ls as user", t); LPVOID lpEnvironment = NULL; - wchar_t* as_user_name = get_username_from_token(as_user); - if (as_user_name) { - if (wcsncmp(L"sshd", as_user_name, wcslen(L"sshd")) != 0) { /* Ignore any names that begin with the service name `sshd`. */ - b = CreateEnvironmentBlock(&lpEnvironment, as_user, TRUE); /* Load a user environment block inheriting the current context, thereby passing session state. */ - } - free(as_user_name); + if (load_user_env) { + CreateEnvironmentBlock(&lpEnvironment, as_user, TRUE); } if (lpEnvironment) { /* Pass the user environment block to the new process. */ b = CreateProcessAsUserW(as_user, NULL, t, NULL, NULL, TRUE, flags | CREATE_UNICODE_ENVIRONMENT, lpEnvironment, NULL, &si, &pi); @@ -1311,7 +1269,7 @@ fd_decode_state(char* enc_buf) } int -posix_spawn_internal(pid_t *pidp, const char *path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv[], char *const envp[], HANDLE user_token, BOOLEAN prepend_module_path) +posix_spawn_internal(pid_t *pidp, const char *path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv[], char *const envp[], HANDLE user_token, BOOLEAN prepend_module_path, BOOLEAN load_user_env) { int i, ret = -1; int sc_flags = 0; @@ -1347,7 +1305,7 @@ posix_spawn_internal(pid_t *pidp, const char *path, const posix_spawn_file_actio if (_putenv_s(POSIX_FD_STATE, fd_info) != 0) goto cleanup; - i = spawn_child_internal(path, argv + 1, stdio_handles[STDIN_FILENO], stdio_handles[STDOUT_FILENO], stdio_handles[STDERR_FILENO], sc_flags, user_token, prepend_module_path); + i = spawn_child_internal(path, argv + 1, stdio_handles[STDIN_FILENO], stdio_handles[STDOUT_FILENO], stdio_handles[STDERR_FILENO], sc_flags, user_token, prepend_module_path, load_user_env); if (i == -1) goto cleanup; if (pidp) @@ -1380,23 +1338,23 @@ posix_spawn_internal(pid_t *pidp, const char *path, const posix_spawn_file_actio int posix_spawn(pid_t *pidp, const char *path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv[], char *const envp[]) { - return posix_spawn_internal(pidp, path, file_actions, attrp, argv, envp, NULL, TRUE); + return posix_spawn_internal(pidp, path, file_actions, attrp, argv, envp, NULL, TRUE, TRUE); } int posix_spawnp(pid_t *pidp, const char *file, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv[], char *const envp[]) { - return posix_spawn_internal(pidp, file, file_actions, attrp, argv, envp, NULL, FALSE); + return posix_spawn_internal(pidp, file, file_actions, attrp, argv, envp, NULL, FALSE, TRUE); } int posix_spawn_as_user(pid_t *pidp, const char *file, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv[], char *const envp[], HANDLE user_token) { - return posix_spawn_internal(pidp, file, file_actions, attrp, argv, envp, user_token, TRUE); + return posix_spawn_internal(pidp, file, file_actions, attrp, argv, envp, user_token, TRUE, TRUE); } int posix_spawnp_as_user(pid_t *pidp, const char *file, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const argv[], char *const envp[], HANDLE user_token) { - return posix_spawn_internal(pidp, file, file_actions, attrp, argv, envp, user_token, FALSE); + return posix_spawn_internal(pidp, file, file_actions, attrp, argv, envp, user_token, FALSE, TRUE); } diff --git a/regress/pesterTests/UserEnvironment.Tests.ps1 b/regress/pesterTests/UserEnvironment.Tests.ps1 new file mode 100644 index 000000000000..1e301da0db21 --- /dev/null +++ b/regress/pesterTests/UserEnvironment.Tests.ps1 @@ -0,0 +1,131 @@ +If ($PSVersiontable.PSVersion.Major -le 2) {$PSScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path} +Import-Module $PSScriptRoot\CommonUtils.psm1 -Force +Import-Module OpenSSHUtils -Force + +$tC = 1 +$tI = 0 +$suite = "userenvironment" + +Describe "E2E scenarios for user environment block" -Tags "CI" { + BeforeAll { + if ($OpenSSHTestInfo -eq $null) { + throw "`$OpenSSHTestInfo is null. Please run Set-OpenSSHTestEnvironment to set test environments." + } + + $server = $OpenSSHTestInfo["Target"] + $port = $OpenSSHTestInfo["Port"] + + $testDir = Join-Path $OpenSSHTestInfo["TestDataPath"] $suite + if (-not (Test-Path $testDir)) { + $null = New-Item $testDir -ItemType directory -Force -ErrorAction SilentlyContinue + } + + $script:envTestUser = $OpenSSHTestInfo["SshdUser"] + $script:envTestProfile = $OpenSSHTestInfo["SshdUserProfile"] + + $keypassphrase = "testpassword" + $script:envTestKey = Join-Path $testDir "sshd_user_envtest_ed25519" + Remove-Item -Path "$($script:envTestKey)*" -Force -ErrorAction SilentlyContinue + ssh-keygen.exe -q -t ed25519 -f $script:envTestKey -N $keypassphrase + $envTestSshDir = Join-Path $script:envTestProfile .ssh + if (-not (Test-Path $envTestSshDir -PathType Container)) { + New-Item $envTestSshDir -ItemType Directory -Force -ErrorAction Stop | Out-Null + } + $script:envTestAuthKeys = Join-Path $envTestSshDir authorized_keys + Copy-Item "$($script:envTestKey).pub" $script:envTestAuthKeys -Force + Repair-AuthorizedKeyPermission -FilePath $script:envTestAuthKeys -confirm:$false + Add-PasswordSetting -Pass $keypassphrase + } + + AfterAll { + Remove-PasswordSetting + if ($script:envTestKey) { + Remove-Item -Path "$($script:envTestKey)*" -Force -ErrorAction SilentlyContinue + } + if ($script:envTestAuthKeys -and (Test-Path $script:envTestAuthKeys)) { + Remove-Item $script:envTestAuthKeys -Force -ErrorAction SilentlyContinue + } + } + + AfterEach { $tI++ } + + Context "$tC - User environment variables" { + BeforeAll { $tI = 1 } + AfterAll { $tC++ } + + It "$tC.$tI - USERNAME matches the connecting user" { + $o = ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %USERNAME% + "$o".Trim() | Should Be $script:envTestUser + } + + It "$tC.$tI - USERPROFILE points to the connecting user's profile" { + $o = ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %USERPROFILE% + $remote = "$o".Trim() + $remote | Should Not BeNullOrEmpty + $remote | Should Not Match 'system32\\config\\systemprofile' + # Profile leaf is normally the user name, but Windows can suffix + # it (e.g. sshd_user.DESKTOP-XYZ.001) when the profile dir was + # recreated, so just check it starts with the user name. + ($remote -split '\\')[-1] | Should Match ("^" + [regex]::Escape($script:envTestUser)) + } + + It "$tC.$tI - HOMEDRIVE and HOMEPATH resolve to the user's profile" { + $hd = "$(ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %HOMEDRIVE%)".Trim() + $hp = "$(ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %HOMEPATH%)".Trim() + $up = "$(ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %USERPROFILE%)".Trim() + $hp | Should Not Match 'system32\\config\\systemprofile' + $hp | Should Not Match '^\\Windows' + $hp | Should Match ("^\\Users\\" + [regex]::Escape($script:envTestUser)) + $hd | Should Be $env:SystemDrive + ($hd + $hp) | Should Be $up + } + + It "$tC.$tI - APPDATA is populated for user" { + $ad = "$(ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %APPDATA%)".Trim() + $ad | Should Not Match 'system32\\config\\systemprofile' + $ad | Should Match 'AppData\\Roaming$' + $ad | Should Match ([regex]::Escape($script:envTestUser)) + } + + It "$tC.$tI - LOCALAPPDATA is populated for user" { + $la = "$(ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %LOCALAPPDATA%)".Trim() + $la | Should Not Match 'system32\\config\\systemprofile' + $la | Should Match 'AppData\\Local$' + $la | Should Match ([regex]::Escape($script:envTestUser)) + } + + It "$tC.$tI - USERDOMAIN equals the local computer name" { + $o = ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %USERDOMAIN% + $remote = "$o".Trim() + $remote | Should Not Be 'WORKGROUP' + $remote | Should Be $env:COMPUTERNAME + } + } + + Context "$tC - PATH variable" { + BeforeAll { + $tI = 1 + $script:pathMarker = "C:\sshtestmarker_$([guid]::NewGuid().ToString('N'))" + ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" ` + reg add HKCU\Environment /v Path /t REG_EXPAND_SZ /d $($script:pathMarker) /f | Out-Null + } + AfterAll { + ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" ` + reg delete HKCU\Environment /v Path /f | Out-Null + $tC++ + } + + It "$tC.$tI - contains both system and user entries" { + $o = ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %PATH% + $remote = "$o".Trim() + $segments = $remote -split ';' + $sysMatches = @($segments | Where-Object { $_ -match '[Ss]ystem32' }) + $markerMatches = @($segments | Where-Object { $_ -eq $script:pathMarker }) + $sysMatches.Count | Should BeGreaterThan 0 + $markerMatches.Count | Should BeGreaterThan 0 + $sysIndex = [array]::IndexOf($segments, $sysMatches[0]) + $markerIndex = [array]::IndexOf($segments, $script:pathMarker) + $sysIndex | Should BeLessThan $markerIndex + } + } +}