Amit Mittal
Mar 2, 2026
  97
(0 votes)

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:

  1. 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.

  2. 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.

  3. 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

  1. Go to Admin Mode -> Content Types -> Find your Page Type -> Select the Property.

  2. If the Type still says Long string or String instead of PropertyBase64String, you must click "Revert to Default".

  3. If that button is grayed out, your fastest solution is to rename the C# property in the model (e.g., from ChatScript to ChatScript_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.

Mar 02, 2026

Comments

Please login to comment.
Latest blogs
Inspect SaaS CMS Packages Without Losing Your Sanity (Package Explorer Update)

Optimizely export packages have quietly become more complex. Inline (nested) blocks in CMS 12 and PaaS solutions weren’t always displayed clearly,...

Allan Thraen | Mar 1, 2026 |

Unstoppable: Insights from Optimizely’s 2026 UK Partner day

Over 150 Optimizely partners met in Shoreditch for the 2026 London Partner Kick Off. The theme was very much Opal with a side order of Optimizely's...

Mark Welland | Feb 27, 2026

What you need to run better experiments today

A practical, end-to-end playbook for higher quality A/B tests: conditional activation, targeting, metrics, power, SRM, and decision discipline.

Hristo Bakalov | Feb 27, 2026 |