Making Sitecore XP Compliant with GDPR and Other Privacy Laws (Because It's Not)
As of April 2023, Sitecore XP (up to and including 10.3) is not GDPR-compliant out of the box (depending on which lawyers you ask). Given the steep fines of privacy compliance, this is probably not something you want to risk to a gray area technicality.
In Sitecore 10.0, the ConsentManager
API was introduced to allow developers to more easily manage a user’s consent and disable/enable tracking accordingly. According to Sitecore’s documentation, the GiveConsent()
method writes the consent choice to the SC_TRACKING_CONSENT
cookie and initializes the tracker for the user. Easy enough. The RevokeConsent()
method stops any tracking that’s already in progress, invalidates the SC_ANALYTICS_GLOBAL_COOKIE
, and updates the SC_TRACKING_CONSENT
cookie to reflect the consent choice. Okay, great. That’s exactly what we want.
The Problem
Unfortunately, Sitecore’s own code doesn’t (always) use this API. Whenever Sitecore does something that requires a Tracker
, it tries to create an instance of DefaultTracker
and initializes it, including running pipelines such as ensureSessionContext
. In the ensureSessionContext
pipeline, there is a processor Sitecore.Analytics.Pipelines.EnsureSessionContext.EnsureDevice
that executes the following code:
ContactKeyCookie contactKeyCookie = new ContactKeyCookie();
...
contactKeyCookie.Create(args.Session.Device.DeviceId);
That creates the SC_ANALYTICS_GLOBAL_COOKIE
to store the device ID. That’s okay, because there’s another processor Sitecore.Analytics.Pipelines.EnsureSessionContext.CheckConsent
that executes afterwards that invalidates this based on a user’s consent choice. Except, it doesn’t. Here’s the code, see if you can figure out which Sitecore-provided consent API you don’t see used here:
bool? consent = args.Session.Contact.IsTrackingConsentGivenFor(Context.Site.Name);
if (!consent.HasValue)
this._consentStorage.RemoveConsent(HttpContext.Current.ToHttpContextBase());
else
this._consentStorage.SetConsent(HttpContext.Current.ToHttpContextBase(), new TrackingConsent()
{
IsGiven = false
});
if (!args.Session.IsReadOnly)
this._sharedSessionStateManager.ReleaseContact(args.ContactId.Value);
args.Session.Contact = null;
public void RemoveConsent(HttpContextBase httpContext)
{
var cookie = httpContext.GetCookie("SC_TRACKING_CONSENT");
if (cookie == null)
return;
var siteConsentList = new List<CookiesConsentStorage.SiteConsent>();
if (!string.IsNullOrEmpty(cookie.Value))
siteConsentList = this.GetSiteConsentsFromCookie(cookie);
var siteName = this._currentSiteContext.SiteName;
var siteConsent = siteConsentList.FirstOrDefault<CookiesConsentStorage.SiteConsent>((Func<CookiesConsentStorage.SiteConsent, bool>) (s => s.SiteName == siteName));
if (siteConsent != null)
siteConsentList.Remove(siteConsent);
if (siteConsentList.Any<CookiesConsentStorage.SiteConsent>())
{
cookie.Value = this.ConvertSiteConsentsToCookieValue(siteConsentList);
}
else
{
cookie.Value = string.Empty;
cookie.Expires = DateTime.UtcNow.AddDays(-1.0);
}
httpContext.Response.Cookies["SC_TRACKING_CONSENT"].Value = cookie.Value;
httpContext.Response.Cookies["SC_TRACKING_CONSENT"].Expires = cookie.Expires;
}
Instead of using the API, the code only updates the SC_TRACKING_CONSENT
cookie but doesn’t invalidate the all-important SC_ANALYTICS_GLOBAL_COOKIE
which gets created when the tracker gets created. This means the SC_ANALYTICS_GLOBAL_COOKIE
gets issued regardless of a user’s consent choice, which is Not Good™. While the cookie doesn’t contain anything more than a user’s device ID, according to the EU ePrivacy Directive (and in turn, GDPR), this is considered personally identifiable information that a user actively rejected you from collecting, and since all cookies are sent with every request, the value of that cookie gets sent to the server and the legal argument of whether that data is “processed” or not begins.
The Solution
What we can do is to rewrite the CheckConsent
processor to use the ConsentManager
API and have it properly revoke the cookie.
Here’s the new and improved rewritten CheckConsent
processor:
using Sitecore;
using Sitecore.Abstractions;
using Sitecore.Analytics.Configuration;
using Sitecore.Analytics.Pipelines.InitializeTracker;
using Sitecore.Diagnostics;
using Sitecore.Analytics.Tracking.Consent;
namespace Foundation.Personalization.Pipelines.EnsureSessionContext
{
public class CheckConsent : InitializeTrackerProcessor
{
public ICurrentSiteContext CurrentSiteContext { get; }
public IConsentManager ConsentManager { get; }
public BaseLog Log { get; }
public CheckConsent(ICurrentSiteContext currentSiteContext, IConsentManager consentManager, BaseLog baseLog)
{
CurrentSiteContext = currentSiteContext;
ConsentManager = consentManager;
Log = baseLog;
}
public override void Process(InitializeTrackerArgs args)
{
Assert.ArgumentNotNull(args, nameof(args));
if (Context.Site == null || !CurrentSiteContext.ExplicitConsentForTrackingIsRequired)
{
Log.Debug("CheckConsent is skipped for site: " + (Context.Site?.Name ?? "<undefined>"));
}
else
{
var consentChoice = ConsentManager.GetConsent(null);
if (consentChoice == null || !consentChoice.IsGiven)
{
ConsentManager.RevokeConsent(null);
args.AbortPipeline();
Log.Debug("CheckConsent aborts pipeline");
}
}
}
}
}
and the patch file to replace the default Sitecore one with this one:
<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<ensureSessionContext>
<processor type="Foundation.Compliance.Pipelines.EnsureSessionContext.CheckConsent, Foundation.Compliance" resolve="true" patch:instead="processor[@type='Sitecore.Analytics.Pipelines.EnsureSessionContext.CheckConsent, Sitecore.Analytics']" />
</ensureSessionContext>
</pipelines>
</sitecore>
</configuration>
With this, for any requests that try to create a new tracker instance will have the SC_ANALYTICS_GLOBAL_COOKIE
cookie invalidated before sending the response back to the user rather than merely resetting the consent choice.
Sitecore is aware of this bug, however, they have no timeline or plans to resolve. If you want to follow along, Sitecore support has given the reference number 526015.
EDIT: I was mistaken, Sitecore is not aware of the bug. Intead, 526015 is for another bug where Sitecore Forms doesn’t respect the consent choice and enables the cookie for form field tracking. Big yikes.
** This blog post should not be taken as any sort of legal guidance. You should consult with a law professional for your specific use-case. It’s also code you found on the internet, so you’re on your own as far as support goes.