CloudPages that process user input or call external APIs are security-sensitive. This page covers the most important SSJS security practices.

1. Validate All User Input

Never trust query strings, POST bodies, form fields, or cookies. Always validate before using.

var email = Platform.Request.GetFormField("email");

// Validate email format
if (!Platform.Function.IsEmailAddress(email)) {
    Write(Platform.Function.Stringify({ status: 400, statusMessage: "Bad Request", error: "Invalid email address" }));
    return;
}

var id = Platform.Request.GetQueryStringParameter("id");

// Validate numeric ID
if (!id || !/^\d+$/.test(id)) {
    Write(Platform.Function.Stringify({ status: 400, statusMessage: "Bad Request", error: "Invalid id" }));
    return;
}
id = parseInt(id, 10);

Show test script
<script runat="server">
/*
 * Chapter: Validate All User Input
 * Proves:
 *   1. IsEmailAddress works after Core load.
 * 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");
assert("valid email", Platform.Function.IsEmailAddress("a@b.com") ? "true" : "false", "true");
assert("invalid email", Platform.Function.IsEmailAddress("not-an-email") ? "true" : "false", "false");
</script>

2. Never Inject Input into Platform.Function.TreatAsContent

Platform.Function.TreatAsContent() evaluates AMPscript. Passing user-controlled data to it creates a Server-Side Template Injection (SSTI) vulnerability.

// DANGEROUS — user can inject AMPscript
Platform.Function.TreatAsContent(userInput);

// SAFE against injection — set via Variable, then use a fixed template.
// The value now arrives as data, so it is never parsed as AMPscript source.
Variable.SetValue("@userInput", userInput);
var rendered = Platform.Function.TreatAsContent("%%=v(@userInput)=%%");

The two protections are independent and you usually need both:

Threat Protection
AMPscript injection (SSTI) Variable.SetValue() + fixed template — never concatenate input into the TreatAsContent() argument
XSS in the rendered output HTML-encode the result yourself before writing it (§6)
// Both protections together
Variable.SetValue("@userInput", userInput);
var rendered = Platform.Function.TreatAsContent("%%=v(@userInput)=%%");
Write("<div>" + htmlEncode(rendered) + "</div>"); // htmlEncode defined in §6

Show test script
<script runat="server">
/*
 * Chapter: Never Inject Input into TreatAsContent
 * Proves:
 *   1. SetValue then TreatAsContent with variable reference is safe path.
 * 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");
Variable.SetValue("@safeTac", "hello");
var out = Platform.Function.TreatAsContent("%%=v(@safeTac)=%%");
assert("TreatAsContent via variable", out, "hello");
</script>

3. Protect API Tokens

Never hardcode tokens in SSJS source code. Store them in a DE or use SFMC Key Management.

// BAD — token visible in source/logs
var token = "Bearer sk-abc123secrettoken";

// GOOD — load from Config DE
var token = Platform.Function.Lookup("AppConfig", "value", "key", "apiToken");

// BETTER — load from encrypted field
var encryptedToken = Platform.Function.Lookup("AppConfig", "encryptedToken", "key", "apiToken");
function decryptSymmetric(encryptedString, algorithm, passwordKey, passwordValue,saltKey, saltValue, vectorKey, vectorValue) {
    Platform.Variable.SetValue("@decrypt_string", encryptedString);
    Platform.Variable.SetValue("@decrypt_algo",algorithm);
    Platform.Variable.SetValue("@decrypt_pw",passwordValue || "");
    Platform.Variable.SetValue("@decrypt_salt",saltValue || "");
    Platform.Variable.SetValue("@decrypt_vector",vectorValue || "");
    return Platform.Function.TreatAsContent("%%=DecryptSymmetric(@decrypt_string, @decrypt_algo, @null,@decrypt_pw, @null, @decrypt_salt, @null, @decrypt_vector)=%%");
}
var token = decryptSymmetric(encryptedToken, "AES", "myKey", "myIV");

Show test script
<script runat="server">
/*
 * Chapter: Protect API Tokens
 * Proves:
 *   1. NON-ASSERTABLE: token storage.
 *   Stringify does not log secrets by itself.
 * 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("guidance-only", "documented", "documented");
</script>

4. CSRF Protection for Forms

CloudPages forms are publicly accessible. Without CSRF protection, any site can submit to your form.

// Platform.Request.Method is a CLR value — convert once before comparing
var method = String(Platform.Request.Method);

// Generate CSRF token on page load (GET)
if (method === "GET") {
    var csrfToken = Platform.Function.GUID();
    Platform.Response.SetCookie("csrfToken", csrfToken, "", true);
    // Output token in form
    Write('<input type="hidden" name="csrf_token" value="' + csrfToken + '">');
}

// Validate on POST
if (method === "POST") {
    var tokenFromCookie = Platform.Request.GetCookieValue("csrfToken");
    var tokenFromForm = Platform.Request.GetFormField("csrf_token");

    if (!tokenFromCookie || tokenFromCookie !== tokenFromForm) {
        Write(Platform.Function.Stringify({ status: 403, statusMessage: "Forbidden", error: "CSRF validation failed" }));
        return;
    }
    // Process form...
}

Show test script
<script runat="server">
/*
 * Chapter: CSRF Protection for Forms
 * Proves:
 *   1. GUID token shape for CSRF.
 * 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).length > 10 ? "true" : "false", "true");
</script>

5. Token-Based API Authentication

For JSON endpoints called by other services:

// Shared secret authentication
var receivedToken = Platform.Request.GetRequestHeader("X-API-Token");
var expectedToken = Platform.Function.Lookup("AppConfig", "value", "key", "apiSecret");

if (!receivedToken || receivedToken !== expectedToken) {
    Write(Platform.Function.Stringify({ status: 401, statusMessage: "Unauthorized", error: "Unauthorized" }));
    return;
}

Show test script
<script runat="server">
/*
 * Chapter: Token-Based API Authentication
 * Proves:
 *   1. Compare tokens with ===.
 * 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 a = "tok-1", b = "tok-1", c = "tok-2";
assert("match", a === b ? "true" : "false", "true");
assert("mismatch", a === c ? "true" : "false", "false");
</script>

6. Output Encoding

Always HTML-encode output from user input to prevent XSS:

function htmlEncode(str) {
    return (str + "")
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#x27;");
}

// DANGEROUS
Write("<div>Hello, " + userName + "</div>"); // XSS if userName contains <script>

// SAFE
Write("<div>Hello, " + htmlEncode(userName) + "</div>");

Show test script
<script runat="server">
/*
 * Chapter: Output Encoding
 * Proves:
 *   1. htmlEncode escapes <>&.
 * 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 htmlEncode(str) {
    return (str + "")
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;");
}
assert("encode script", htmlEncode("<script>"), "&lt;script&gt;");
assert("encode amp", htmlEncode("a&b"), "a&amp;b");
</script>

7. Restrict Sensitive Data in Responses

Don’t expose internal identifiers, full DE records, or raw error objects in error responses:

// BAD — leaks internal structure
} catch(e) {
    Write(Platform.Function.Stringify(e));
}

// GOOD — safe error message
} catch(e) {
    // String(e) — this engine has no .stack, and .message is undefined for new Error(/* ... */)
    Platform.Function.InsertData("ErrorLog",
        ["timestamp", "message"],
        [Platform.Function.Now(), String(e)]
    );
    Write(Platform.Function.Stringify({ status: 500, statusMessage: "Internal Server Error", error: "An internal error occurred" }));
}

Show test script
<script runat="server">
/*
 * Chapter: Restrict Sensitive Data in Responses
 * Proves:
 *   1. Omit fields when building response object.
 * 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 full = { email: "a@b.com", secret: "x" };
var pub = { email: full.email };
assert("no secret", pub.secret === undefined ? "undefined" : "other", "undefined");
</script>

8. Rate Limiting with DE

CloudPages don’t have built-in rate limiting. Implement it with a DE:

function formatDate(dateString,dateFormat,timeFormat,isoLocale) {
    Platform.Variable.SetValue("@formatDate_string",dateString);
    Platform.Variable.SetValue("@formatDate_date",dateFormat);
    Platform.Variable.SetValue("@formatDate_time",timeFormat);
    Platform.Variable.SetValue("@formatDate_iso",isoLocale);
    return Platform.Function.TreatAsContent("%%=FormatDate(@formatDate_string, @formatDate_date, @formatDate_time, @formatDate_iso)=%%");
}

var ip = Platform.Request.GetRequestHeader("X-Forwarded-For")
       || Platform.Request.GetRequestHeader("REMOTE_ADDR")
       || "unknown";
var timeWindow = formatDate(Platform.Function.Now(), "MM/DD/YYYY HH:mm");
var key = ip + "|" + timeWindow;

var hitCount = Platform.Function.Lookup("RateLimit", "count", "key", key);
// String() first — parsing a Lookup result directly throws when the field is empty
hitCount = parseInt(String(hitCount), 10) || 0;

if (hitCount >= 10) { // 10 requests per minute
    Write(Platform.Function.Stringify({ status: 429, statusMessage: "Too Many Requests", error: "Rate limit exceeded" }));
    return;
}

Platform.Function.UpsertData("RateLimit",
    ["key"], [key],
    ["count", "window"],
    [hitCount + 1, timeWindow]
);
Show test script
<script runat="server">
/*
 * Chapter: Rate Limiting with DE
 * Proves:
 *   1. NON-ASSERTABLE: DE counter without fixture.
 *   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("Lookup typeof", typeof Platform.Function.Lookup, "clrmethodinfo");
assert("InsertData typeof", typeof Platform.Function.InsertData, "clrmethodinfo");
</script>

See Also