diff --git a/docs/configuration.md b/docs/configuration.md index 27939e701..f843b839a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -255,6 +255,24 @@ Defaults to false (use hardware acceleration where available). --- +### credential.allowUnsafeRemotes + +Allow transmitting credentials to unsafe remote URLs such as unencrypted HTTP +URLs. This setting is not recommended for general use and should only be used +when necessary. + +Defaults false (disallow unsafe remote URLs). + +#### Example + +```shell +git config --global credential.allowUnsafeRemotes true +``` + +**Also see: [GCM_ALLOW_UNSAFE_REMOTES][gcm-allow-unsafe-remotes]** + +--- + ### credential.autoDetectTimeout Set the maximum length of time, in milliseconds, that GCM should wait for a @@ -1024,6 +1042,7 @@ Defaults to disabled. [envars]: environment.md [freedesktop-ss]: https://specifications.freedesktop.org/secret-service-spec/ [gcm-allow-windowsauth]: environment.md#GCM_ALLOW_WINDOWSAUTH +[gcm-allow-unsafe-remotes]: environment.md#GCM_ALLOW_UNSAFE_REMOTES [gcm-authority]: environment.md#GCM_AUTHORITY-deprecated [gcm-autodetect-timeout]: environment.md#GCM_AUTODETECT_TIMEOUT [gcm-azrepos-credentialtype]: environment.md#GCM_AZREPOS_CREDENTIALTYPE diff --git a/docs/environment.md b/docs/environment.md index 05c0ea8fd..1973132ea 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -302,6 +302,32 @@ Defaults to false (use hardware acceleration where available). --- +### GCM_ALLOW_UNSAFE_REMOTES + +Allow transmitting credentials to unsafe remote URLs such as unencrypted HTTP +URLs. This setting is not recommended for general use and should only be used +when necessary. + +Defaults false (disallow unsafe remote URLs). + +#### Example + +##### Windows + +```batch +SET GCM_ALLOW_UNSAFE_REMOTES=true +``` + +##### macOS/Linux + +```bash +export GCM_ALLOW_UNSAFE_REMOTES=true +``` + +**Also see: [credential.allowUnsafeRemotes][credential-allowunsaferemotes]** + +--- + ### GCM_AUTODETECT_TIMEOUT Set the maximum length of time, in milliseconds, that GCM should wait for a @@ -1153,7 +1179,8 @@ Defaults to disabled. [autodetect]: autodetect.md [azure-access-tokens]: azrepos-users-and-tokens.md [configuration]: configuration.md -[credential-allowwindowsauth]: environment.md#credentialallowWindowsAuth +[credential-allowwindowsauth]: configuration.md#credentialallowwindowsauth +[credential-allowunsaferemotes]: configuration.md#credentialallowunsaferemotes [credential-authority]: configuration.md#credentialauthority-deprecated [credential-autodetecttimeout]: configuration.md#credentialautodetecttimeout [credential-azrepos-credential-type]: configuration.md#credentialazreposcredentialtype diff --git a/docs/netconfig.md b/docs/netconfig.md index cf312336f..920344f15 100644 --- a/docs/netconfig.md +++ b/docs/netconfig.md @@ -191,6 +191,22 @@ network traffic inspection tool such as [Telerik Fiddler][telerik-fiddler]. If you are using such tools please consult their documentation for trusting the proxy root certificates. +--- + +## Unsafe Remote URLs + +If you are using a remote URL that is not considered safe, such as unencrypted +HTTP (remote URLs that start with `http://`), host providers may prevent you +from authenticating with your credentials. + +In this case, you should consider using a HTTPS (starting with `https://`) +remote URL to ensure your credentials are transmitted securely. + +If you accept the risks associated with using an unsafe remote URL, you can +configure GCM to allow the use of unsafe remote URLS by setting the environment +variable [`GCM_ALLOW_UNSAFE_REMOTES`][unsafe-envar], or by using the Git +configuration option [`credential.allowUnsafeRemotes`][unsafe-config] to `true`. + [environment]: environment.md [configuration]: configuration.md [git-http-proxy]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy @@ -212,3 +228,5 @@ proxy root certificates. [git-ssl-no-verify]: https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_networking [git-http-ssl-verify]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslVerify [telerik-fiddler]: https://www.telerik.com/fiddler +[unsafe-envar]: environment.md#gcm_allow_unsafe_remotes +[unsafe-config]: configuration.md#credentialallowunsaferemotes diff --git a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs index 35472682c..286398de9 100644 --- a/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs +++ b/src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs @@ -55,8 +55,8 @@ public bool IsSupported(InputArguments input) return false; } - // We do not support unencrypted HTTP communications to Bitbucket, - // but we report `true` here for HTTP so that we can show a helpful + // We do not recommend unencrypted HTTP communications to Bitbucket, but it is possible. + // Therefore, we report `true` here for HTTP so that we can show a helpful // error message for the user in `GetCredentialAsync`. return (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http") || StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "https")) && @@ -81,11 +81,14 @@ public bool IsSupported(HttpResponseMessage response) public async Task GetCredentialAsync(InputArguments input) { // We should not allow unencrypted communication and should inform the user - if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http") - && BitbucketHelper.IsBitbucketOrg(input)) + if (!_context.Settings.AllowUnsafeRemotes && + StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http") && + BitbucketHelper.IsBitbucketOrg(input)) { throw new Trace2Exception(_context.Trace2, - "Unencrypted HTTP is not supported for Bitbucket.org. Ensure the repository remote URL is using HTTPS."); + "Unencrypted HTTP is not recommended for Bitbucket.org. " + + "Ensure the repository remote URL is using HTTPS " + + $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes."); } var authModes = await GetSupportedAuthenticationModesAsync(input); diff --git a/src/shared/Core/Constants.cs b/src/shared/Core/Constants.cs index 210c991bc..41ccb990c 100644 --- a/src/shared/Core/Constants.cs +++ b/src/shared/Core/Constants.cs @@ -119,6 +119,7 @@ public static class EnvironmentVariables public const string OAuthDefaultUserName = "GCM_OAUTH_DEFAULT_USERNAME"; public const string GcmDevUseLegacyUiHelpers = "GCM_DEV_USELEGACYUIHELPERS"; public const string GcmGuiSoftwareRendering = "GCM_GUI_SOFTWARE_RENDERING"; + public const string GcmAllowUnsafeRemotes = "GCM_ALLOW_UNSAFE_REMOTES"; } public static class Http @@ -163,6 +164,7 @@ public static class Credential public const string MsAuthUseDefaultAccount = "msauthUseDefaultAccount"; public const string GuiSoftwareRendering = "guiSoftwareRendering"; public const string GpgPassStorePath = "gpgPassStorePath"; + public const string AllowUnsafeRemotes = "allowUnsafeRemotes"; public const string OAuthAuthenticationModes = "oauthAuthModes"; public const string OAuthClientId = "oauthClientId"; @@ -226,6 +228,7 @@ public static class HelpUrls public const string GcmAutoDetect = "https://aka.ms/gcm/autodetect"; public const string GcmDefaultAccount = "https://aka.ms/gcm/defaultaccount"; public const string GcmMultipleUsers = "https://aka.ms/gcm/multipleusers"; + public const string GcmUnsafeRemotes = "https://aka.ms/gcm/unsaferemotes"; } private static Version _gcmVersion; diff --git a/src/shared/Core/GenericHostProvider.cs b/src/shared/Core/GenericHostProvider.cs index 447e465d5..9f087ca5b 100644 --- a/src/shared/Core/GenericHostProvider.cs +++ b/src/shared/Core/GenericHostProvider.cs @@ -54,6 +54,17 @@ public override async Task GenerateCredentialAsync(InputArguments i { ThrowIfDisposed(); + // We only want to *warn* about HTTP remotes for the generic provider because it supports all protocols + // and, historically, we never blocked HTTP remotes in this provider. + // The user can always set the 'GCM_ALLOW_UNSAFE' setting to silence the warning. + if (!Context.Settings.AllowUnsafeRemotes && + StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) + { + Context.Streams.Error.WriteLine( + "warning: use of unencrypted HTTP remote URLs is not recommended; " + + $"see {Constants.HelpUrls.GcmUnsafeRemotes} for more information."); + } + Uri uri = input.GetRemoteUri(); // Determine the if the host supports Windows Integration Authentication (WIA) or OAuth diff --git a/src/shared/Core/Settings.cs b/src/shared/Core/Settings.cs index 2aa71edf4..0e24ce9a3 100644 --- a/src/shared/Core/Settings.cs +++ b/src/shared/Core/Settings.cs @@ -189,6 +189,11 @@ public interface ISettings : IDisposable /// bool UseSoftwareRendering { get; } + /// + /// Permit the use of unsafe remotes URLs such as regular HTTP. + /// + bool AllowUnsafeRemotes { get; } + /// /// Get TRACE2 settings. /// @@ -580,6 +585,12 @@ public bool UseSoftwareRendering } } + public bool AllowUnsafeRemotes => + TryGetSetting(KnownEnvars.GcmAllowUnsafeRemotes, + KnownGitCfg.Credential.SectionName, + KnownGitCfg.Credential.AllowUnsafeRemotes, + out string str) && str.ToBooleanyOrDefault(false); + public Trace2Settings GetTrace2Settings() { var settings = new Trace2Settings(); diff --git a/src/shared/GitHub/GitHubHostProvider.cs b/src/shared/GitHub/GitHubHostProvider.cs index 918e859a0..21d29f651 100644 --- a/src/shared/GitHub/GitHubHostProvider.cs +++ b/src/shared/GitHub/GitHubHostProvider.cs @@ -285,10 +285,13 @@ public virtual Task EraseCredentialAsync(InputArguments input) ThrowIfDisposed(); // We should not allow unencrypted communication and should inform the user - if (StringComparer.OrdinalIgnoreCase.Equals(remoteUri.Scheme, "http")) + if (!_context.Settings.AllowUnsafeRemotes && + StringComparer.OrdinalIgnoreCase.Equals(remoteUri.Scheme, "http")) { throw new Trace2Exception(_context.Trace2, - "Unencrypted HTTP is not supported for GitHub. Ensure the repository remote URL is using HTTPS."); + "Unencrypted HTTP is not recommended for GitHub. " + + "Ensure the repository remote URL is using HTTPS " + + $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes."); } string service = GetServiceName(remoteUri); diff --git a/src/shared/GitLab/GitLabHostProvider.cs b/src/shared/GitLab/GitLabHostProvider.cs index eda6e2f0f..6cda3c0e1 100644 --- a/src/shared/GitLab/GitLabHostProvider.cs +++ b/src/shared/GitLab/GitLabHostProvider.cs @@ -95,10 +95,13 @@ public override async Task GenerateCredentialAsync(InputArguments i ThrowIfDisposed(); // We should not allow unencrypted communication and should inform the user - if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) + if (!Context.Settings.AllowUnsafeRemotes && + StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) { throw new Trace2Exception(Context.Trace2, - "Unencrypted HTTP is not supported for GitHub. Ensure the repository remote URL is using HTTPS."); + "Unencrypted HTTP is not recommended for GitLab. " + + "Ensure the repository remote URL is using HTTPS " + + $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes."); } Uri remoteUri = input.GetRemoteUri(); diff --git a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs index 1d5c649d0..525704886 100644 --- a/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs +++ b/src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs @@ -59,7 +59,7 @@ public bool IsSupported(InputArguments input) return false; } - // We do not support unencrypted HTTP communications to Azure Repos, + // We do not recommend unencrypted HTTP communications to Azure Repos, // but we report `true` here for HTTP so that we can show a helpful // error message for the user in `CreateCredentialAsync`. return input.TryGetHostAndPort(out string hostName, out _) @@ -208,16 +208,22 @@ protected override void ReleaseManagedResources() base.ReleaseManagedResources(); } - private async Task GeneratePersonalAccessTokenAsync(InputArguments input) + private void ThrowIfUnsafeRemote(InputArguments input) { - ThrowIfDisposed(); - - // We should not allow unencrypted communication and should inform the user - if (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) + if (!_context.Settings.AllowUnsafeRemotes && + StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http")) { throw new Trace2Exception(_context.Trace2, - "Unencrypted HTTP is not supported for Azure Repos. Ensure the repository remote URL is using HTTPS."); + "Unencrypted HTTP is not recommended for Azure Repos. " + + "Ensure the repository remote URL is using HTTPS " + + $"or see {Constants.HelpUrls.GcmUnsafeRemotes} about how to allow unsafe remotes."); } + } + + private async Task GeneratePersonalAccessTokenAsync(InputArguments input) + { + ThrowIfDisposed(); + ThrowIfUnsafeRemote(input); Uri remoteUserUri = input.GetRemoteUri(includeUser: true); Uri orgUri = UriHelpers.CreateOrganizationUri(remoteUserUri, out _); @@ -257,16 +263,11 @@ private async Task GeneratePersonalAccessTokenAsync(InputArguments private async Task GetAzureAccessTokenAsync(InputArguments input) { + ThrowIfUnsafeRemote(input); + Uri remoteWithUserUri = input.GetRemoteUri(includeUser: true); string userName = input.UserName; - // We should not allow unencrypted communication and should inform the user - if (StringComparer.OrdinalIgnoreCase.Equals(remoteWithUserUri.Scheme, "http")) - { - throw new Trace2Exception(_context.Trace2, - "Unencrypted HTTP is not supported for Azure Repos. Ensure the repository remote URL is using HTTPS."); - } - Uri orgUri = UriHelpers.CreateOrganizationUri(remoteWithUserUri, out string orgName); _context.Trace.WriteLine($"Determining Microsoft Authentication authority for Azure DevOps organization '{orgName}'..."); diff --git a/src/shared/TestInfrastructure/Objects/TestSettings.cs b/src/shared/TestInfrastructure/Objects/TestSettings.cs index f14bf6cc9..3e67e39b0 100644 --- a/src/shared/TestInfrastructure/Objects/TestSettings.cs +++ b/src/shared/TestInfrastructure/Objects/TestSettings.cs @@ -53,6 +53,8 @@ public class TestSettings : ISettings public bool UseMsAuthDefaultAccount { get; set; } + public bool AllowUnsafeRemotes { get; set; } = false; + public Trace2Settings GetTrace2Settings() { return new Trace2Settings() @@ -189,6 +191,8 @@ ProxyConfiguration ISettings.GetProxyConfiguration() bool ISettings.UseSoftwareRendering => false; + bool ISettings.AllowUnsafeRemotes => AllowUnsafeRemotes; + #endregion #region IDisposable