Cross-site request forgery (CSRF) attacks trick a user’s browser into submitting an authenticated request to your Cloud Page without their knowledge. The standard defence is a server-generated, single-use token that is tied to the user’s session and verified on submission.


Pattern Overview

  1. Render phase — generate a random token, store it alongside the subscriber key in a DE, embed it in the form as a hidden field.
  2. Submit phase — read the submitted token, look it up in the DE, verify it matches the expected subscriber key, then delete it so it cannot be replayed.

Show test script
<script runat="server">
/*
 * Chapter: Pattern Overview
 * Proves:
 *   1. GUID token + Variable storage pattern.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
var token = Platform.Function.GUID();
Platform.Variable.SetValue("@csrf", token);
assert("token round-trip", Platform.Variable.GetValue("@csrf"), token);
</script>

Setup: Token Storage DE

Create a Data Extension named CSRFTokens with these fields:

Field Type Is Primary Key
Token Text (50) Yes
SubscriberKey Text (254) No
CreatedAt Date No

Show test script
<script runat="server">
/*
 * Chapter: Setup Token Storage DE
 * Proves:
 *   1. NON-ASSERTABLE: DE schema.
 *   InsertData/Lookup typeof.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
assert("InsertData", typeof Platform.Function.InsertData, "clrmethodinfo");
assert("Lookup", typeof Platform.Function.Lookup, "clrmethodinfo");
</script>

Step 1 — Render the Form

Platform.Load("core", "1.1.5");

var subKey = Platform.Variable.GetValue("@SubscriberKey");
if (!subKey) { subKey = "anon_" + Platform.Function.GUID(); }

// Generate a random token and store it
var token = Platform.Function.GUID();
Platform.Function.InsertData(
    "CSRFTokens",
    ["Token", "SubscriberKey", "CreatedAt"],
    [token, subKey, Platform.Function.Now()]
);

Embed the token in your form:

<form method="post" action="%%=CloudPagesURL(123)=%%">
  <input type="hidden" name="csrfToken" value="%%=v(@token)=%%">
  <!-- other form fields -->
  <button type="submit">Submit</button>
</form>

Note: The form value is set via AMPscript’s %%=v(@token)=%% after assigning the SSJS variable to an AMPscript variable with Variable.SetValue("@token", token).

Platform.Variable.SetValue("@token", token);

Show test script
<script runat="server">
/*
 * Chapter: Step 1 Render the Form
 * Proves:
 *   1. GUID non-empty for hidden field.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
var token = Platform.Function.GUID();
assert("token length", ("" + token).replace(/-/g, "").length >= 32 ? "true" : "false", "true");
</script>

Step 2 — Validate on Submission

Platform.Load("core", "1.1.5");

var submittedToken = Platform.Request.GetFormField("csrfToken");
var subKey = Platform.Variable.GetValue("@SubscriberKey");

if (!submittedToken) {
    Write(Platform.Function.Stringify({status: 403, statusMessage: "Forbidden", error: "Missing security token."}));
    Platform.Function.RaiseError("Missing CSRF token", true);
}

// Look up the token
// String() first — a Lookup result throws on a truthiness test when the field is empty
var storedKey = String(Platform.Function.Lookup(
    "CSRFTokens", "SubscriberKey", "Token", submittedToken
));

if (storedKey === "" || storedKey === "null" || storedKey !== subKey) {
    Write(Platform.Function.Stringify({status: 403, statusMessage: "Forbidden", error:"Invalid or expired security token."}));
    Platform.Function.RaiseError("CSRF token mismatch", true);
}

// Token is valid — delete it immediately (single-use)
Platform.Function.DeleteData("CSRFTokens", ["Token"], [submittedToken]);

// Continue with form processing...

Show test script
<script runat="server">
/*
 * Chapter: Step 2 Validate on Submission
 * Proves:
 *   1. Missing token → 403 shape; mismatch reject.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
Platform.Load("core", "1.1.5");
function validateCsrf(posted, expected) {
    if (!posted) return { status: 403, error: "Missing security token." };
    if (posted !== expected) return { status: 403, error: "Invalid or expired security token." };
    return { status: 200 };
}
var miss = validateCsrf("", "abc");
assert("missing 403", miss.status, 403);
var bad = validateCsrf("x", "abc");
assert("mismatch 403", bad.status, 403);
var ok = validateCsrf("abc", "abc");
assert("ok 200", ok.status, 200);
assert("Stringify 403", Platform.Function.Stringify(miss).indexOf("403") >= 0 ? "true" : "false", "true");
</script>

Cleanup: Expire Old Tokens

Tokens should not accumulate. Run this in an Automation on a schedule (e.g. hourly) to remove tokens older than 30 minutes:

var proxy = new Script.Util.WSProxy();
function dateAdd(timestamp,intervalToAdd,intervalType) {
    Platform.Variable.SetValue("@dateAdd_ts",timestamp);
    Platform.Variable.SetValue("@dateAdd_add",intervalToAdd);
    Platform.Variable.SetValue("@dateAdd_type",intervalType);
    return Platform.Function.TreatAsContent("%%=DateAdd(@dateAdd_ts, @dateAdd_add, @dateAdd_type)=%%");
}
var cutoff = dateAdd(Platform.Function.Now(), -30, "MI");
var filter = {
    Property: "CreatedAt",
    SimpleOperator: "lessThan",
    Value: cutoff
};
var stale = proxy.retrieve("DataExtensionObject[CSRFTokens]", ["Token"], filter);
for (var i = 0; i < stale.Results.length; i++) {
    Platform.Function.DeleteData("CSRFTokens", ["Token"], [stale.Results[i].Token]);
}
Show test script
<script runat="server">
/*
 * Chapter: Cleanup Expire Old Tokens
 * Proves:
 *   1. dateAdd via TreatAsContent returns a string.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = "" + ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function typeOfThunk(fn) {
    try { return "" + fn(); } catch (ex) { return "THREW:" + ("" + ex.message); }
}
function dateAdd(timestamp, intervalToAdd, intervalType) {
    Platform.Variable.SetValue("@dateAdd_ts", timestamp);
    Platform.Variable.SetValue("@dateAdd_add", intervalToAdd);
    Platform.Variable.SetValue("@dateAdd_type", intervalType);
    return Platform.Function.TreatAsContent("%%=DateAdd(@dateAdd_ts, @dateAdd_add, @dateAdd_type)=%%");
}
var cutoff = dateAdd(Platform.Function.Now(), -30, "MI");
assert("cutoff non-empty", ("" + cutoff).length > 0 ? "true" : "false", "true");
</script>

See Also