Make Sitecore Forms more secure

In this blog post, I will talk about two approaches on how to make Sitecore Forms more secure. First I will cover the encryption of data at rest (data storage) by enabling Transparent Data Encryption (TDE) on SQL Server. Then I will walk you through on how to encrypt data stored by Forms in SQL databases.

Recently I wrote Is it time to migrate WFFM to Sitecore Forms . I mentioned that one of the missing features at Forms is that Sitecore does not encrypt the information it captures from the forms. It is an issue if an organization is considering Sitecore Forms for sensitive data such as credit card information.

I was talking to Michael Thyregod on the Slack Sitecore Community about this missing encryption when he said that encryption of data at rest is possible and suggested me filing a ticket for the Sitecore support .

I filed the ticket. Sitecore answered. It turns out Sitecore supports the encryption of data at rest (data storage) by enabling Transparent Data Encryption (TDE) on SQL Server.

Transparent Data Encryption (TDE)

Even if you take several precautions to secure the database, if the physical media (such as drives or backup tapes) are stolen, a malicious party can just restore or attach the database and browse the data. One solution is to encrypt the sensitive data in the database and protect the keys that are used to encrypt the data with a certificate. This prevents anyone without the keys from using the data. Here is where Transparent Data Encryption (TDE) comes into action.

You can learn more by reading through the Microsoft’s documentation on Transparent Data Encryption (TDE). It is recommended reading through this documentation before planning on using TDE in production environments.

Below I give a brief summary of what you need to do. Just reminding you that this feature is not available on SQL Server Express editions but on Standard and Developer editions.

USE master;
GO
CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'MyStrongPasswordInPowerfulWays';
go
CREATE CERTIFICATE MyServerCert WITH SUBJECT = 'Local Server DEK Certificate';

Now, enable the TDE on the Forms database.

USE Site_Sitecore_ExperienceForms;
GO
CREATE DATABASE ENCRYPTION KEY
WITH ALGORITHM = AES_128
ENCRYPTION BY SERVER CERTIFICATE MyServerCert;
GO
ALTER DATABASE Site_Sitecore_ExperienceForms
SET ENCRYPTION ON;

Also, don’t forget to do a backup of the certificate you have just created.

BACKUP CERTIFICATE MyServerCert
TO FILE = 'X:\Program Files\Microsoft SQL Server\MSSQL13.SQL2016EXPRESS\MSSQL\BackupCertificates\MyServerCert.bak'
WITH PRIVATE KEY ( FILE = 'X:\Program Files\Microsoft SQL Server\MSSQL13.SQL2016EXPRESS\MSSQL\BackupCertificates\MyServerCert.bak' ,
ENCRYPTION BY PASSWORD = 'MyStrongPasswordInPowerfulWays');

Done!

Here’s what happens if someone that doesn’t have the private key tries to restore/attach your encrypted database.

Encrypt data stored by Forms in SQL databases

Mike Reynolds and Jose Dominguez wrote nice blog posts about how to encrypt data stored in Web Form for Marketers (WFFM). Please see WFFM data encryption – Sitecore 8 by Jose Dominguez and Encrypt Web Forms For Marketers Fields in Sitecore by Mike Reynolds.

Based on these articles, I created a way to implement data encryption on Forms. You’ll create a custom Submit Action that encrypts data and a custom CSV Export Provider that decrypts the data when exporting to CSV.

You’ll need to add the following NuGet packages to your Visual Studio project.

  • Sitecore.ExperienceForms.Client.NoReferences
  • Sitecore.ExperienceForms.NoReferences
  • Sitecore.Kernel.NoReferences
  • Sitecore.Mvc.Analytics.NoReferences
  • Sitecore.Mvc.NoReferences

First, create the encryption helper class.

/*
* This code was copied from the article "WFFM data encryption - Sitecore 8"
* Author: José Domínguez
* Source: http://josedbaez.com/2016/09/wffm-encryption/
*/

using System;
using System.Text;
using System.Web;
using System.Web.Security;

namespace SandboxSC9u1.Forms.Helpers
{
    public class EncryptionHelper
    {
        private static readonly UTF8Encoding Encoder = new UTF8Encoding();

        public static string Encrypt(string unencrypted)
        {
            if (string.IsNullOrEmpty(unencrypted))
                return string.Empty;
            try
            {
                var encryptedBytes = MachineKey.Protect(Encoder.GetBytes(unencrypted));
                if (encryptedBytes != null && encryptedBytes.Length > 0)
                    return HttpServerUtility.UrlTokenEncode(encryptedBytes);
            }
            catch (Exception)
            {
                return string.Empty;
            }
            return string.Empty;
        }

        public static string Decrypt(string encrypted)
        {
            if (string.IsNullOrEmpty(encrypted))
                return string.Empty;
            try
            {
                var bytes = HttpServerUtility.UrlTokenDecode(encrypted);
                if (bytes != null && bytes.Length > 0)
                {
                    var decryptedBytes = MachineKey.Unprotect(bytes);
                    if (decryptedBytes != null && decryptedBytes.Length > 0)
                        return Encoder.GetString(decryptedBytes);
                }
            }
            catch (Exception)
            {
                return string.Empty;
            }
            return string.Empty;
        }
    }
}

Now, let’s create the custom Submit Action that encrypts data.

using SandboxSC9u1.Forms.Helpers;
using Sitecore.Diagnostics;
using Sitecore.ExperienceForms.Data.Entities;
using Sitecore.ExperienceForms.Models;
using System;
using System.Collections.Generic;
using System.Reflection;

namespace SandboxSC9u1.Forms.Processing.Actions
{
    public class EncryptedSaveData : Sitecore.ExperienceForms.Processing.Actions.SaveData
    {
        public EncryptedSaveData(ISubmitActionData submitActionData) : base(submitActionData)
        {
        }

        protected override bool SavePostedData(Guid formId, Guid sessionId, IList postedFields)
        {
            try
            {
                FormEntry formEntry = new FormEntry()
                {
                    Created = DateTime.UtcNow,
                    FormItemId = formId,
                    FormEntryId = sessionId,
                    Fields = (ICollection)new List()
                };

                if (postedFields != null)
                {
                    foreach (IViewModel postedField in (IEnumerable)postedFields)
                        EncryptedSaveData.AddFieldData(postedField, formEntry);
                }

                this.FormDataProvider.CreateEntry(formEntry);
                return true;
            }
            catch (Exception ex)
            {
                this.Logger.LogError(ex.Message, ex, (object)this);
                return false;
            }
        }

        private new static void AddFieldData(IViewModel postedField, FormEntry formEntry)
        {
            Assert.ArgumentNotNull((object)postedField, "postedField");
            Assert.ArgumentNotNull((object)formEntry, "formEntry");

            IValueField valueField = postedField as IValueField;
            if ((valueField != null ? (valueField.AllowSave ? 1 : 0) : 0) == 0)
                return;

            PropertyInfo property = postedField.GetType().GetProperty("Value");

            object postedValue;
            if ((object)property == null)
            {
                postedValue = (object)null;
            }
            else
            {
                IViewModel viewModel = postedField;
                postedValue = property.GetValue((object)viewModel);
            }

            if (postedValue == null)
                return;

            string fieldValueType = postedValue.GetType().FullName;
            string fieldValue = ParseFieldValue(postedValue);

            FieldData fieldData = new FieldData();
            fieldData.FieldDataId = Guid.NewGuid();
            fieldData.FieldItemId = Guid.Parse(postedField.ItemId);
            fieldData.FieldName = postedField.Name;
            fieldData.FormEntryId = formEntry.FormEntryId;

            // Encrypt data
            fieldData.Value = EncryptionHelper.Encrypt(fieldValue);

            fieldData.ValueType = fieldValueType;

            formEntry.Fields.Add(fieldData);
        }
    }
}

In Sitecore, create the custom Submit Action item. You can simply duplicate the item /sitecore/system/Settings/Forms/Submit Actions/Save Data, rename it to Encrypted Save Data, and update the Model Type field to be the fully qualified name of EncryptedSaveData class.

Now, simply create a new form and add the Encrypted Save Data to the Submit button.

If you submit data from your form, you will notice that the data is encrypted in SQL databases.

Finally, let’s create the custom CSV Export Provider that decrypts the data when exporting to CSV. Add a new class named EncryptedCsvExportProvider.

using System;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;

namespace SandboxSC9u1.Forms.Client.Data
{
    public class EncryptedCsvExportProvider : CsvExportProvider
    {
        public EncryptedCsvExportProvider(IFormDataProvider formDataProvider) : base(formDataProvider)
        {
        }

        protected override string GenerateFileContent(IEnumerable formEntries)
        {

            Assert.ArgumentNotNull((object)formEntries, "formEntries");

            IOrderedEnumerable orderedEnumerable = formEntries.OrderByDescending((Func)(item => item.Created));

            List fieldColumnsList = new List();

            StringBuilder stringBuilder = new StringBuilder();

            foreach (FormEntry formEntry in (IEnumerable)orderedEnumerable)
                fieldColumnsList.AddRange(formEntry.Fields.Where((Func)(x => fieldColumnsList.All((Func)(c => c.FieldItemId != x.FieldItemId)))));

            if (fieldColumnsList.Count == 0)
                return string.Empty;

            stringBuilder.AppendFormat((IFormatProvider)CultureInfo.InvariantCulture, "Created{0}", (object)";");
            stringBuilder.AppendLine(string.Join(";", fieldColumnsList.Select((Func)(f => f.FieldName)).ToArray()));

            int count = fieldColumnsList.Count;

            foreach (FormEntry formEntry in (IEnumerable)orderedEnumerable)
            {
                stringBuilder.AppendFormat((IFormatProvider)CultureInfo.InvariantCulture, "{0:yyyy-MM-dd HH:mm}{1}", (object)formEntry.Created, (object)";");
                string[] strArray = new string[count];
                for (int index = 0; index  f.FieldItemId == fieldItem.FieldItemId));

                string decryptedValue = EncryptionHelper.Decrypt(fieldData.Value);
                strArray[index] = EscapeCsvDelimiters(fieldData != null ? decryptedValue : (string)null);
            }
            stringBuilder.AppendLine(string.Join(";", strArray));
        }

        return stringBuilder.ToString();
    }

    private static string EscapeCsvDelimiters(string fieldValue)
    {
        if (!string.IsNullOrEmpty(fieldValue))
        {
            fieldValue = fieldValue.Replace("\"", "\"\"");
            if (fieldValue.IndexOf(Environment.NewLine, 0, StringComparison.OrdinalIgnoreCase) >= 0 || fieldValue.IndexOf(";", StringComparison.OrdinalIgnoreCase) >= 0 || fieldValue.IndexOf("\"", StringComparison.OrdinalIgnoreCase) >= 0)
                fieldValue = FormattableString.Invariant(FormattableStringFactory.Create("\"{0}\"", (object)fieldValue));
            fieldValue = fieldValue.Replace(Environment.NewLine, " ");
        }
        return fieldValue;
    }
}
}

Create a patch file named SITE_NAME.ExperienceForms.Pipelines.Client.config.

Now, when you export data from Forms, you get it decrypted.

You can find the complete solution in the SandboxSC9u1 repository on my Github.

comments powered by Disqus