Here is the solution that we arrived at. It was in part based on recommendations by our Microsoft representative, so you might consider this an unofficial, "official" approach.
First, we need keep SharePoint from advertising the disco file to the world (i.e. Google). Simply remove the following line in your master pages:
<SharePoint:SoapDiscoveryLink runat="server"/>
This will suppress the <link href="/_vti_bin/spsdisco.aspx" rel="alternate" type="text/xml">
reference in the HEAD of your pages.
Next, we want to make sure that unauthorized users don't have access to the web services described by the disco file, or anything in _vti_bin
for that matter. If your site only runs internal to your firewall (an intranet, for example), then this isn't as important. But if you've got anonymous endpoints that can be accessed externally, you want them locked down.
This is an excellent application for an HttpModule. We'll build one that intercepts any request containing _vti_bin
in the path, and if the current user is unauthorized will return a 404 NOT FOUND
status code. I chose to return a 404 rather than a 401 UNAUTHORIZED
because I don't just want to lock those paths down, I want to hide the fact that anything even exists at those paths.
Our HttpModule looks like this:
using System;
using System.Web;
namespace Custom.SharePoint.HttpModule.SpSecureVtiBin {
public class SpSecureVtiBinModule : IHttpModule {
#region IHttpModule Members
public void Dispose() { }
public void Init( HttpApplication context ) {
context.AuthorizeRequest += new EventHandler( context_AuthorizeRequest );
}
protected virtual void context_AuthorizeRequest( object sender, EventArgs e ) {
HttpApplication app = (HttpApplication)sender;
string requestedPath = app.Request.Path;
if ( requestedPath.ToLowerInvariant().Contains( "_vti_bin" ) ) {
if ( !app.Request.IsAuthenticated ) {
app.Response.StatusCode = 404;
app.Response.StatusDescription = "Not Found";
app.Response.Write( "404 NOT FOUND" );
app.Response.End();
}
}
}
#endregion
}
}
Simple enough. To use the HttpModule, it needs to be registered in the site's web.config file with an entry under \configuration\system.webServer\modules
:
<add name="SpSecureVtiBinModule" type="Custom.SharePoint.HttpModule.SpSecureVtiBin.SpSecureVtiBinModule, Custom.SharePoint.HttpModule.SpSecureVtiBin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=[your_public_key_token]" />
Of course, we don't want to modify a SharePoint application's web.config file manually. We'll create an SPFeatureReceiver to do the job:
using System.Collections.ObjectModel;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
namespace Custom.SharePoint.HttpModule.SpSecureVtiBin {
public class ModuleFeatureReceiver : SPFeatureReceiver {
private static string _owner = "SpSecureVtiBinModule";
public override void FeatureActivated( SPFeatureReceiverProperties properties ) {
SPWebApplication app = (SPWebApplication)properties.Feature.Parent;
app.WebConfigModifications.Add( GetModificationForSystemWebServer() );
app.WebService.ApplyWebConfigModifications();
app.Update();
}
public override void FeatureDeactivating( SPFeatureReceiverProperties properties ) {
SPWebApplication app = (SPWebApplication)properties.Feature.Parent;
Collection<SPWebConfigModification> mods = app.WebConfigModifications;
int modCount = mods.Count;
bool modRemoved = false;
for ( int i = modCount - 1; i >= 0; i-- ) {
SPWebConfigModification mod = mods[i];
if ( mod.Owner.Equals( _owner ) || mod.Owner.Equals( "CHK.SharePoint.HttpModule.SpSecureVtiBin.SpSecureVtiBinModule" ) ) {
app.WebConfigModifications.Remove( mod );
modRemoved = true;
}
}
if ( modRemoved ) {
app.WebService.ApplyWebConfigModifications();
app.Update();
}
}
private SPWebConfigModification GetModificationForSystemWebServer() {
return new SPWebConfigModification {
Name = "add[@name='SpSecureVtiBinModule']",
Owner = _owner,
Path = "configuration/system.webServer/modules",
Value = @"<add name=""SpSecureVtiBinModule"" type=""Custom.SharePoint.HttpModule.SpSecureVtiBin.SpSecureVtiBinModule, Custom.SharePoint.HttpModule.SpSecureVtiBin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=[your_public_key_token]"" />",
Sequence = 0
};
}
}
}
Now all that's left is to package up the HttpModule. You'll need to define a Feature in the package and reference the SPFeatureReceiver class. This will cause the web.config entry to be added when the Feature is activated, and the entry to be removed when the Feature is deactivated. Target the Feature for a WebApplication
and the assembly deployment target to GlobalAssemblyCache
.