Not too long ago Microsoft introduced a security fix which addresses a possible attack vector in which malicious software tries to impersonate a local request, thereby bypassing certain constraints. The problem with this fix is that it introduces some issues for SharePoint servers, effectively resulting in 401.1 Access Denied errors. Spence Harbar does a great write-up of the fix and the options available to get your SharePoint environment working again so I won’t re-hash all that here: http://www.harbar.net/archive/2009/07/02/disableloopbackcheck-amp-sharepoint-what-every-admin-and-developer-should-know.aspx.

As Spence points out, the preferred way to fix this is to add the host names to the BackConnectionHostNames registry key and to not set the DisableLoopbackCheck registry key. You can of course do this using Group Policy but for those not managing their servers using GPO I decided to implement a custom STSADM command that would make setting the BackConnectionHostNames registry key really simple. I called this new command, oddly enough, gl-setbackconnectionhostnames.

The command has two ways to run it, you can run it without any parameters which will cause it to update only the server in which the command is executed on, or you can pass in an -updatefarm parameter along with a username and password which will cause it to update every server in the farm. There’s no need to pass in the host header names as the code will dynamically determine them by inspecting each web application and their alternate access mappings (alternate URLs) and perform some logic to determine whether the host header is pointing to a local IP address or to a specific SharePoint server (I do this to exclude Central Admin which is usually accessed using a server name and non-standard port).

I accomplish the farm update by using a custom Timer Job which executes on each server. Unfortunately the timer service account does not have access to write to the registry (unless you’ve given it rights, which you shouldn’t) so it was necessary to pass in a username and password and then use impersonation to update the registry. The custom timer job code is shown below, notice that all the core work is being done via the SetBackConnectionHostNames class which is shown below the timer job code:

 1using System;
 2using System.Collections.Generic;
 3using System.Runtime.InteropServices;
 4using System.Security.Principal;
 5using System.Text;
 6using Microsoft.SharePoint;
 7using Microsoft.SharePoint.Administration;
 8 
 9namespace Lapointe.SharePoint.STSADM.Commands.WebApplications
10{
11    public class SetBackConnectionHostNamesTimerJob : SPJobDefinition
12    {
13        public const int LOGON32_LOGON_INTERACTIVE = 2;
14        public const int LOGON32_LOGON_SERVICE = 3;
15        public const int LOGON32_PROVIDER_DEFAULT = 0;
16 
17        [DllImport("advapi32.dll", CharSet = CharSet.Auto)]
18        public static extern bool LogonUser(
19          String lpszUserName,
20          String lpszDomain,
21          String lpszPassword,
22          int dwLogonType,
23          int dwLogonProvider,
24          ref IntPtr phToken
25        );
26 
27        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
28        public extern static bool CloseHandle(IntPtr handle);
29 
30        private const string JOB_NAME = "job-set-back-connection-host-names-";
31        private const string KEY_USER = "userName";
32        private const string KEY_PWD = "password";
33 
34        private static readonly string jobId = Guid.NewGuid().ToString();
35 
36        public SetBackConnectionHostNamesTimerJob() : base() { }
37 
38        /// <summary>
39        /// Initializes a new instance of the <see cref="SetBackConnectionHostNamesTimerJob"/> class.
40        /// </summary>
41        public SetBackConnectionHostNamesTimerJob(SPService service)
42            : base(JOB_NAME + jobId, service, null, SPJobLockType.None)
43        {
44            Title = "Set BackConnectionHostNames Registry Key";
45        }
46 
47        /// <summary>
48        /// Executes the job definition.
49        /// </summary>
50        /// <param name="targetInstanceId">For target types of <see cref="T:Microsoft.SharePoint.Administration.SPContentDatabase"></see> this is the database ID of the content database being processed by the running job. This value is Guid.Empty for all other target types.</param>
51        public override void Execute(Guid targetInstanceId)
52        {
53            string user = Properties[KEY_USER] as string;
54            string password = Properties[KEY_PWD] as string;
55 
56            if (string.IsNullOrEmpty(user) || password == null)
57                throw new ArgumentNullException("Username and password is required.");
58 
59            if (user.IndexOf('\\') < 0)
60                throw new ArgumentException("Username must be in the form \"DOMAIN\\USER\"");
61 
62            IntPtr userHandle = new IntPtr(0);
63            WindowsImpersonationContext impersonatedUser = null;
64 
65            bool returnValue = LogonUser(
66              user.Split('\\')[1],
67              user.Split('\\')[0],
68              password,
69              LOGON32_LOGON_INTERACTIVE,
70              LOGON32_PROVIDER_DEFAULT,
71              ref userHandle
72              );
73 
74            if (!returnValue)
75            {
76                throw new Exception("Invalid Username");
77            }
78            WindowsIdentity newId = new WindowsIdentity(userHandle);
79            impersonatedUser = newId.Impersonate();
80 
81            SetBackConnectionHostNames.SetBackConnectionRegKey(SetBackConnectionHostNames.GetUrls());
82 
83            impersonatedUser.Undo();
84            CloseHandle(userHandle);
85        }
86 
87        /// <summary>
88        /// Submits the job.
89        /// </summary>
90        public void SubmitJob(string user, string password)
91        {
92            Properties[KEY_USER] = user;
93            Properties[KEY_PWD] = password;
94            Schedule = new SPOneTimeSchedule(DateTime.Now);
95            Update();
96        }
97    }
98}

The following is the code for the SetBackConnectionHostNames class:

  1using System;
  2using System.Collections;
  3using System.Collections.Generic;
  4using System.Collections.Specialized;
  5using System.Net;
  6using System.Text;
  7using Lapointe.SharePoint.STSADM.Commands.SPValidators;
  8using Microsoft.SharePoint;
  9using Microsoft.SharePoint.Administration;
 10using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
 11using Microsoft.Win32;
 12 
 13namespace Lapointe.SharePoint.STSADM.Commands.WebApplications
 14{
 15    public class SetBackConnectionHostNames : SPOperation
 16    {
 17        /// <summary>
 18        /// Initializes a new instance of the <see cref="SetBackConnectionHostNames"/> class.
 19        /// </summary>
 20        public SetBackConnectionHostNames()
 21        {
 22            
 23            SPParamCollection parameters = new SPParamCollection();
 24            parameters.Add(new SPParam("updatefarm", "uf"));
 25            parameters.Add(new SPParam("username", "user", false, null, new SPNonEmptyValidator()));
 26            parameters.Add(new SPParam("password", "pwd", false, null, new SPNullOrNonEmptyValidator()));
 27 
 28            StringBuilder sb = new StringBuilder();
 29            sb.Append("\r\n\r\nSets the BackConnectionHostNames registry key with the URLs associated with each web application.\r\n\r\nParameters:");
 30            sb.Append("\r\n\t[-updatefarm (update all servers in the farm)]");
 31            sb.Append("\r\n\t[-username <DOMAIN\\user (must have rights to update the registry on each server)>]");
 32            sb.Append("\r\n\t[-password <password>]");
 33 
 34            Init(parameters, sb.ToString());
 35        }
 36 
 37        #region ISPStsadmCommand Members
 38 
 39        /// <summary>
 40        /// Gets the help message.
 41        /// </summary>
 42        /// <param name="command">The command.</param>
 43        /// <returns></returns>
 44        public override string GetHelpMessage(string command)
 45        {
 46            return HelpMessage;
 47        }
 48 
 49        /// <summary>
 50        /// Runs the specified command.
 51        /// </summary>
 52        /// <param name="command">The command.</param>
 53        /// <param name="keyValues">The key values.</param>
 54        /// <param name="output">The output.</param>
 55        /// <returns></returns>
 56        public override int Execute(string command, StringDictionary keyValues, out string output)
 57        {
 58            output = string.Empty;
 59 
 60            if (!Params["updatefarm"].UserTypedIn)
 61                SetBackConnectionRegKey(GetUrls());
 62            else
 63            {
 64                SPTimerService timerService = SPFarm.Local.TimerService;
 65                if (null == timerService)
 66                {
 67                    throw new SPException("The Farms timer service cannot be found.");
 68                }
 69                SetBackConnectionHostNamesTimerJob job = new SetBackConnectionHostNamesTimerJob(timerService);
 70 
 71                string user = Params["username"].Value;
 72                if (user.IndexOf('\\') < 0)
 73                    user = Environment.UserDomainName + "\\" + user;
 74                job.SubmitJob(user, Params["password"].Value + "");
 75 
 76                output += "Timer job successfully created.";
 77            }
 78 
 79            return OUTPUT_SUCCESS;
 80        }
 81 
 82        public override void Validate(StringDictionary keyValues)
 83        {
 84            base.Validate(keyValues);
 85 
 86            if (Params["updatefarm"].UserTypedIn)
 87            {
 88                if (!Params["username"].UserTypedIn)
 89                    throw new SPSyntaxException("A valid username with rights to edit the registry is required.");
 90            }
 91        }
 92 
 93        #endregion
 94 
 95        public static List<string> GetUrls()
 96        {
 97            List<string> urls = new List<string>();
 98            foreach (SPService svc in SPFarm.Local.Services)
 99            {
100                if (!(svc is SPWebService))
101                    continue;
102 
103                foreach (SPWebApplication webApp in ((SPWebService)svc).WebApplications)
104                {
105                    
106                    foreach (SPAlternateUrl url in webApp.AlternateUrls)
107                    {
108                        string host = url.Uri.Host.ToLower();
109                        if (!urls.Contains(host) && // Don't add if we already have it
110                            !url.Uri.IsLoopback && // Quick check to short circuit the more elaborate checks
111                            host != Environment.MachineName.ToLower() && // Quick check to short circuit the more elaborate checks
112                            IsLocalIpAddress(host) && // If the host name points locally then we need to add it
113                            !IsSharePointServer(host)) // Don't add if it matches an SP server name (handles central admin)
114                        {
115                            urls.Add(host);
116                        }
117                    }
118                }
119            }
120            return urls;
121        }
122 
123        private static bool IsSharePointServer(string host)
124        {
125            foreach (SPServer server in SPFarm.Local.Servers)
126            {
127                if (server.Address.ToLower() == host)
128                    return true;
129            }
130            return false;
131        }
132 
133        private static bool IsLocalIpAddress(string host)
134        {
135            try
136            { 
137                IPAddress[] hostIPs = Dns.GetHostAddresses(host);
138                IPAddress[] localIPs = Dns.GetHostAddresses(Dns.GetHostName());
139 
140                // test if any host IP equals to any local IP or to localhost
141                foreach (IPAddress hostIP in hostIPs)
142                {
143                    // is localhost
144                    if (IPAddress.IsLoopback(hostIP)) return true;
145                    // is local address
146                    foreach (IPAddress localIP in localIPs)
147                    {
148                        if (hostIP.Equals(localIP)) return true;
149                    }
150                }
151            }
152            catch { }
153            return false;
154        }
155 
156        public static void SetBackConnectionRegKey(List<string> urls)
157        {
158            const string KEY_NAME = "SYSTEM\\CurrentControlSet\\Control\\Lsa\\MSV1_0";
159            const string KEY_VAL_NAME = "BackConnectionHostNames";
160 
161            RegistryKey reg = Registry.LocalMachine.OpenSubKey(KEY_NAME, true);
162            if (reg != null)
163            {
164                string[] existing = (string[])reg.GetValue(KEY_VAL_NAME);
165                if (existing != null)
166                {
167                    foreach (string val in existing)
168                    {
169                        if (!urls.Contains(val.ToLower()))
170                            urls.Add(val.ToLower());
171                    }
172                }
173                string[] multiVal = new string[urls.Count];
174                urls.CopyTo(multiVal);
175                
176                reg.SetValue(KEY_VAL_NAME, multiVal, RegistryValueKind.MultiString);
177            }
178            else
179            {
180                throw new SPException("Unable to open registry key.");
181            }
182        }
183 
184    }
185}

There’s two core methods, GetUrls and SetBackConnectionRegKey. The SetBackConnectionRegKey method started out from a bit of sample code that my friend Ben Robb sent me – there’s no much of his original code but it saved me some time in trying to remember how to manipulate the registry using C#. Essentially all this method does is get the current list of host names, add any missing items to the passed in list, and then reset the list (thus avoiding duplicate entries). The GetUrls method is the more interesting piece – I’m looping through all the Farm’s Web Applications and their corresponding Alternate URLs and then building a list of URLs that meet some basic inclusion criteria:

  • Don’t add duplicates – you can get duplicates when both HTTP and HTTPS are used so we make sure that we exclude them
  • Don’t add loopback URLs – this shouldn’t come up but if the URL is localhost or 127.0.0.1 it will be flagged as a loopback URL so we exclude them
  • Don’t add URLs that match the server name – if the host name matches the server name then exclude it (this is essentially just a short circuit for the next check which is a bit more thorough)
  • Exclude host names that map to the local IP address – this is the most crucial bit (the previous steps were just short circuits for this step to avoid the additional querying necessary); I use the System.Net.Dns class’s static GetHostAddresses method to check the local addresses against those associated with the provided host name
  • Exclude host names that map to SharePoint servers – this step is necessary to address host names such as those belonging to Central Administration

The help for the command is shown below:

C:\>stsadm -help gl-setbackconnectionhostnames

stsadm -o gl-setbackconnectionhostnames


Sets the BackConnectionHostNames registry key with the URLs associated with each web application.
Parameters:
        [-updatefarm (update all servers in the farm)]
        [-username <DOMAIN\user (must have rights to update the registry on each server)>]
        [-password <password>]

The following table summarizes the command and its various parameters:

Command NameAvailabilityBuild Date
gl-setbackconnectionhostnamesWSS v3, MOSS 2007Released: 9/20/2009
Parameter NameShort FormRequiredDescriptionExample Usage
updatefarmufNoIf provided then update the BackConnectionHostNames registry key on all servers in the farm.-updatefarm, -uf
usernameuserYes if updatefarm is providedThe username with sufficient rights to update the registry. If no domain part is specified then the current users domain is used.-username domain\spadmin, -user spadmin
passwordpwdNoIf the users password is blank then this parameter is not required (please change your password if this is the case!); otherwise, this parameter is required if the updatefarm parameter is provided.-password pa$$w0rd, -pwd pa$$w0rd

The following is an example of how to update the BackConnectionHostNames registry key on the current server only:

stsadm -o gl-setbackconnectionhostnames

The following is an example of how to update the BackConnectionHostNames registry key on all servers in the farm:

stsadm -o gl-setbackconnectionhostnames -updatefarm -username domain\spadmin -password pa$$w0rd