Unified Security Hardening Script for Sitecore

I wanted to share here some good work we shared on the Rackspace developer blog last week about Sitecore security hardening.

If one is pursuing security hardening for Sitecore, the whole piece is worth reading, but it’s particularly targeting those looking for repeatable, scripted processes to apply the website configs and IIS changes that secure Sitecore to a general standard.

We get a ton of use out of this unified PowerShell script that applyies the standards from the Sitecore Security Hardening Guide: unified PowerShell script for Sitecore security hardening.

We’ve also posted to GitHub the necessary .config patches for the security hardening:

The “unified” PowerShell script follows a pattern of downloading the .config and copies it to proper directory in the Sitecore installation:

# this .config is what we're applying: https://gist.github.com/grant-killian/b64aa6cabd18e9b0097257ee4a2dc614
$downLoadURI = "https://gist.githubusercontent.com/grant-killian/b64aa6cabd18e9b0097257ee4a2dc614/raw"
$downLoadPath = "C:\localStaging\Rackspace.SecurityHardening.Step5.IncreaseLoginSecurity.config"
Invoke-WebRequest -Uri $downLoadURI -OutFile $downLoadPath
Copy-Item -Path $downLoadPath -Destination $rackspaceInclude #we use a "Z.Rackspace" directory under /app_config/include

This PowerShell isn’t the end of the process of securing a Sitecore implementation, but more just a start.  Sitecore publishes these recommendations as part of their best practices, and what we’ve done at Rackspace it taken those recommendations and scripted them for ease of deployment across a spectrum of environments.  We’ve made slight adjustments here and there to suit our perspective and experience, as there is rarely a one size fits all solution to this kind of work.

Customizing the Sitecore MediaCreator Logic

Many customers use a CDN to improve the performance of their site (static assets will be cached at the CDN and load more quickly).  There are various ways of implementing a CDN with Sitecore, and one customer I work with takes advantage of Sitecore’s flexibility around media item storage to facilitate the synchronization of the file system with their CDN.  Without going into all the details, they had enabled file system storage for media (by altering the Sitecore setting to true: <setting name=Media.UploadAsFiles value=true/>) and were using the physical file system on the Sitecore server to populate their CDN.

I should interject here that Sitecore generally recommends keeping the default UploadAsFiles setting (to false) and implement a CDN by extending Sitecore’s save or publish process to synchronize media — there are other advantages to keeping media stored in the Sitecore databases.  This post isn’t exploring CDN setups, though, so I’ll leave that as a topic for a later day.

For this particular customer, storing Sitecore media as files on disk made the most sense given all their various constraints.  The problem I helped them to solve was that Sitecore’s default naming system for media was creating items on disk that looked like this:

  • /App_Data/MediaFiles/3/F/7/{3F784C5A-68CA-4B93-8CE7-9CC32F8DC5FD}Red.jpg
  • /App_Data/MediaFiles/2/C/A/{2CA14D5F-FC67-9A71-C911-5D26F817D241}Green.jpg

Those ugly GUIDs were displaying in the customer requests, which can apparently hurt SEO.  Usually the media item names wouldn’t be a concern since using the Sitecore FieldRenderer object one can use Sitecore’s <setting name=Media.UseItemPaths value=true/> setting to control how the names are exposed.  For this customer, however, the FieldRenderer wasn’t an option due to their CDN approach.

The challenge was: how can one have full control over the media item names generated by Sitecore?  The final approach we settled on was to extend the Sitecore.Resources.Media.MediaCreator class with our own logic for the GetMediaStorageFolder method. This is a virtual method in Sitecore’s MediaCreator class, which means it is readily extensible.

I created a new VS.Net class library project and added a reference to the Sitecore.Kernel for this customer’s implementation. Then, using Reflector to inspect the existing GetMediaStorageFolder implementation, I added the custom logic to remove the GUID (in truth, any logic could be applied here).

Here’s the default GetMediaStorageFolder method Siteocre uses OOTB:

public virtual string GetMediaStorageFolder(ID itemID, string fullPath)
{
    Assert.IsNotNull(itemID, "itemID is null");
    Assert.IsNotNullOrEmpty(fullPath, "fullPath is empty");
    string fileName = FileUtil.GetFileName(fullPath);
    string str2 = itemID.ToString();
    return string.Format("/{0}/{1}/{2}/{3}{4}", new object[] { str2[1], str2[2], str2[3], str2, fileName });
}

And here’s our customized version that eliminates the GUID from the name of the item, I’m including the full class so you can see in inherits the Sitecore.Resources.Media.MediaCreator class:

 public class CustomMediaCreator : Sitecore.Resources.Media.MediaCreator 
    {
        public override string GetMediaStorageFolder(Sitecore.Data.ID itemID, string fullPath)
        {
            Sitecore.Diagnostics.Assert.IsNotNull(itemID, "itemID is null");
            Sitecore.Diagnostics.Assert.IsNotNullOrEmpty(fullPath, "fullPath is empty");
            string fileName = Sitecore.IO.FileUtil.GetFileName(fullPath);
            string str2 = itemID.ToString();
            return string.Format("/{0}/{1}/{2}/{3}", new object[] { str2[1], str2[2], str2[3],fileName });        
        }    
    }

The real trick comes in how one introduces this custom logic into Sitecore . . . we need to take advantage of the Hooks extensibility point in Sitecore and use our CustomMediaCreator in place of the default.

This requires writing a Hook class that implements the Sitecore.Events.Hooks.IHook interface. IHook requires an Initiliaze method to connect up the custom functionality. In our case, we’re setting the following:

Sitecore.Resources.Media.MediaManager.Provider.Creator = new CustomMediaCreator();

For convenience for this blog, I folded it all into a single file and namespace, but a better design would be to separate this out of course. The finished product, both the Hook and the CustomMediaCreator, are now here:

namespace CustomMediaStorage
{
    public class CustomHook: Sitecore.Events.Hooks.IHook
    {
        public void Initialize()
        {
            Sitecore.Resources.Media.MediaManager.Provider.Creator = new CustomMediaCreator();
        }
    }

    public class CustomMediaCreator : Sitecore.Resources.Media.MediaCreator 
    {
        public override string GetMediaStorageFolder(Sitecore.Data.ID itemID, string fullPath)
        {
            Sitecore.Diagnostics.Assert.IsNotNull(itemID, "itemID is null");
            Sitecore.Diagnostics.Assert.IsNotNullOrEmpty(fullPath, "fullPath is empty");
            string fileName = Sitecore.IO.FileUtil.GetFileName(fullPath);
            string str2 = itemID.ToString();
            return string.Format("/{0}/{1}/{2}/{3}", new object[] { str2[1], str2[2], str2[3],fileName });        
        }    
    }
}

To wire up our custom Hook, we go to web.config and go to definition of our Hooks. HealthMonitorHook and MemoryMonitorHook are the two default hooks defined in Sitecore; I put our configuration just above those two definitions:

<!– HOOKS –>

<hooks>

<hook type=CustomMediaStorage.CustomHook, CustomMediaStorage />

Now, when we add new media items to Sitecore, they’re stored in a path like the following:
/App_Data/MediaFiles/9/C/7/CustomIcon.jpg.  Hooray, no GUID!

This isn’t quite case-closed, however, as the customer was using MVC and initializing the application using a dependency injection registration engine which overwrote our Hook. So it turns out the Hook wasn’t necessary for them, but they needed to include the Sitecore.Resources.Media.MediaManager.Provider.Creator = new CustomMediaCreator(); into that app startup logic.