Bypassing WAF Blocking in Optimizely CMS 11 with Custom Base64 Properties
Introduction
As Optimizely developers, we frequently encounter requirements to allow editors to inject third-party scripts into the head or body of a site. Typical examples include Google Tag Manager (GTM), Facebook Messenger chatbots, or specialized analytics trackers. We usually implement this using a standard String or LongString property on a "Site Settings" page.
However, in high-security environments, there is a catch. Modern security architecture usually places a Security Gateway or Web Application Firewall (WAF) (like Cloudflare, Azure Front Door, or Imperva) in front of the application. These gateways inspect the POST body of incoming requests.
If an editor tries to publish a script, the gateway flags the raw <script> tags as a potential XSS (Cross-Site Scripting) attack and blocks the request. The editor receives a vague network error, and their changes are lost.
In this blog post, I will walk you through a solid, end-to-end solution that bypasses WAF blocking without lowering security standards, ensuring a seamless experience for your editors.
Requirement
The core requirement is to allow editors to store and manage raw script configurations in Site Settings, using either standard multiline text areas or single-line text boxes, while ensuring that the site remains behind a strict WAF that blocks requests containing raw HTML tags in the payload.
The Problem
Optimizely CMS Edit Mode is a single-page application that communicates with the server via REST APIs. When an editor clicks "Publish," the CMS sends a POST or PUT request containing the property data.
If a gateway inspects a request body and finds this...
{ "GtmScriptHeader": "<script>(function(w,d,s,l,i)..."}
...it blocks it.
We need a way to transport this data through the firewall without using HTML entities or special characters that might trigger the WAF rules, while also solving the common "Autosave Loop" issue, where the editor UI displays the encoded data instead of readable code during standard autosave events.
The Solution: Client-Side Base64 Encoding
The architecture we designed involves three main components:
-
The Client-Side (Dojo Mixin): Encodes raw script to Base64 before it leaves the browser on save. It also decodes returning Base64 immediately onautosave events so the editor always sees readable code.
-
The Server-Side (Custom Property): Acts as the transparent translation layer. It handles storage formatting in the database and ensures the frontend View always receives decoded raw HTML.
-
Visual Wrappers (Widgets & Descriptors): Allow developers to easily choose between a multiline Textarea or a single-line TextBox via standard
[UIHint].
We use a "Marker" protocol (SAFE:) so the system knows exactly when the data came from the UI editor vs. direct C# API calls.
Step 1: Client-Side Logic (The Base64 Mixin)
Instead of duplicating code, we use a Dojo Mixin to encapsulate the bidirectional encoding logic. This uses standard, modern TextEncoder and TextDecoder APIs to ensure all UTF-8 characters (like localized text or emojis) survive the Base64 transformation.
File: ~/[ProjectRoot]/ClientResources/Scripts/Editors/Base64Mixin.js
define([
"dojo/_base/declare"
], function (declare) {
// We return a declared class rather than a plain object to
// ensure Dojo can correctly mix it into other constructors.
return declare(null, {
// OUTBOUND: Raw Code -> Base64 (Intercepted before saving)
_getValueAttr: function () {
// Get the standard value from the parent TextBox/TextArea widget
var rawValue = this.inherited(arguments);
if (rawValue && typeof rawValue === "string") {
try {
// Standard, modern UTF-8 safe encoding
var utf8Bytes = new TextEncoder().encode(rawValue);
var binaryString = Array.from(utf8Bytes, function (b) {
return String.fromCharCode(b);
}).join("");
var base64 = window.btoa(binaryString);
// Add the 'SAFE:' prefix so the server knows it is encoded.
// This bypasses WAF inspection because it is alphanumeric.
return "SAFE:" + base64;
} catch (e) {
console.error("Base64 Encoding failed", e);
// Fallback to raw value if encoding fails
return rawValue;
}
}
return "";
},
// INBOUND: Base64 -> Raw Code (Intercepted during autosave echo/load)
// This solves the bug where editors see "Massey" data in the box.
_setValueAttr: function (val) {
// Check if the value is "poisoned" with the SAFE marker
if (val && typeof val === "string" && val.indexOf("SAFE:") === 0) {
try {
// Strip the "SAFE:" prefix
var base64 = val.substring(5);
// Decode Base64 -> Binary String
var binaryString = window.atob(base64);
// Decode Binary String -> UTF-8 Raw Script
var bytes = new Uint8Array(binaryString.length);
for (var i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
val = new TextDecoder().decode(bytes);
} catch (e) {
console.warn("Decoding failed in UI", e);
}
}
// Pass the decoded, readable script to the standard Textbox logic
this.inherited(arguments, [val]);
}
});
});
Step 2: Visual Wrappers (Textarea & TextBox)
Now we create two small Dojo wrappers that combine standard standard Optimizely widgets with your new Mixin.
File: ~/[ProjectRoot]/ClientResources/Scripts/Editors/Base64Textarea.js
define([
"dojo/_base/declare",
"dijit/form/Textarea",
"app/editors/Base64Mixin" // Logical path from module.config
], function (declare, Textarea, Base64Mixin) {
return declare("app.editors.Base64Textarea", [Textarea, Base64Mixin], {});
});
File: ~/[ProjectRoot]/ClientResources/Scripts/Editors/Base64TextBox.js
define([
"dojo/_base/declare",
"dijit/form/ValidationTextBox",
"app/editors/Base64Mixin"
], function (declare, TextBox, Base64Mixin) {
return declare("app.editors.Base64TextBox", [TextBox, Base64Mixin], {});
});
Step 3: Server-Side Property (The Custom Backing Type)
This C# class encapsulates the logic entirely. This adheres to SOLID principles because the Page Model remains clean, while this single class handles all data transformation.
We ensure Liskov Substitution: the getter always returns a string, meaning the view doesn't need to change.
File: ~/[ProjectRoot]/Business/Properties/PropertyBase64String.cs
using EPiServer.Core;
using EPiServer.Framework.DataAnnotations;
using EPiServer.PlugIn;
using System;
using System.Text;
namespace MyApp.Business.Properties
{
// Register this as a custom property definition type in Optimizely
[PropertyDefinitionTypePlugIn]
public class PropertyBase64String : PropertyLongString
{
private const string TransportPrefix = "SAFE:";
public override object Value
{
get
{
var storedValue = base.Value as string;
if (string.IsNullOrWhiteSpace(storedValue)) return null;
// DECODE: Transform DB (Base64) -> View (Raw HTML String)
try
{
// Fail-safe: Handle legacy data that might be raw HTML
if (storedValue.Trim().StartsWith("<")) return storedValue;
var bytes = Convert.FromBase64String(storedValue);
return Encoding.UTF8.GetString(bytes);
}
catch
{
return storedValue; // Fail-safe
}
}
set
{
if (value is string inputString)
{
// CASE A: Input from Dojo Editor (Has marker)
// The WAF let this through because it looked like "SAFE:PGh0..."
if (inputString.StartsWith(TransportPrefix))
{
// Strip marker, store only the pure Base64 in the DB
base.Value = inputString.Substring(TransportPrefix.Length);
}
// CASE B: Input from C# Code (Developer did: page.Script = "<script>...")
else
{
// We must manually encode it before saving so the format matches Storage
var bytes = Encoding.UTF8.GetBytes(inputString);
base.Value = Convert.ToBase64String(bytes);
}
}
else
{
base.Value = value;
}
}
}
}
}
Step 4: Editor Descriptors & Usage
Finally, we link the UIHints and implement it in a model.
File: ~/[ProjectRoot]/Business/EditorDescriptors/Base64Editors.cs
using EPiServer.Shell.ObjectEditing.EditorDescriptors;
namespace MyApp.Business.EditorDescriptors
{
[EditorDescriptorRegistration(TargetType = typeof(string), UIHint = "Base64TextArea")]
public class Base64TextAreaDescriptor : EditorDescriptor
{
public Base64TextAreaDescriptor()
{
ClientEditingClass = "app/editors/Base64Textarea";
}
}
[EditorDescriptorRegistration(TargetType = typeof(string), UIHint = "Base64TextBox")]
public class Base64TextBoxDescriptor : EditorDescriptor
{
public Base64TextBoxDescriptor()
{
ClientEditingClass = "app/editors/Base64TextBox";
}
}
}
Implementation in Page Model:
[Display(Name = "GTM Header Script")]
[UIHint("Base64TextArea")] // Multiline Visual
[BackingType(typeof(PropertyBase64String))] // Back-end Logic
public virtual string GtmScriptHeader { get; set; }
[Display(Name = "Messenger Chat Script")]
[UIHint("Base64TextBox")] // Single-line Visual
[BackingType(typeof(PropertyBase64String))] // Back-end Logic
public virtual string ChatScript { get; set; }
Implementation in Razor View:
Since the property logic always decodes the value back to raw script tags, usage is standard. Remember to use Html.Raw to prevent Razor from HTML encoding the script tags.
<head>
@Html.Raw(Model.CurrentPage.GtmScriptHeader)
</head>
A Note on Handling Existing Properties
A common pitfall is applying this logic to an existing property. Optimizely locks Property Definitions in the database. When you run this code, Optimizely might ignore the [BackingType] attribute and continue treating the existing property as a normal string.
This results in the SAFE:Base64... string being saved literally and rendered on the frontend.
Crucial Admin Mode Step
-
Go to Admin Mode -> Content Types -> Find your Page Type -> Select the Property.
-
If the Type still says
Long stringorStringinstead ofPropertyBase64String, you must click "Revert to Default". -
If that button is grayed out, your fastest solution is to rename the C# property in the model (e.g., from
ChatScripttoChatScript_Secure). Optimizely will see this as a brand new property and apply the custom logic perfectly.
Conclusion
This custom Base64 Property architecture provides a robust, expert-level solution for bypassing WAF restrictions in Optimizely CMS 11. It preserves the strict security gateway rules required by the organization while maintaining an exceptional experience for editors, who can continue working with the raw code they are familiar with.
By encapsulating the complexity inside Dojo Mixins and custom Property Types, the implementation adheres to SOLID principles, ensuring your Page Models remain clean and the solution is easy to maintain and scale.
Comments