CMS 10.6.891 must be installed. See CMS Statistics to check your site
version.
Decide which pattern to implement. For how the CMS middleware works and how to choose
between Option A and Option B, see Custom App CSRF Tokens.
Option A
Relies on the CMS OWIN middleware alone; suits AJAX-only actions.
Option B
Adds an MVC [ValidateAntiForgeryToken] layer over the middleware;
suits HTML form submissions that need a second independent layer.
The reference implementation for both patterns is the CopyPageTab sample project in
App_Data/xml/Custom/CopyPageTab.
Implement either of the two supported anti-forgery patterns, Option A or Option B, in a
custom tab application.
Important
State-changing requests that fail the
Cross-Site Request Forgery (CSRF) check are rejected with a 403 Forbidden error before
your controller action runs. The global ajaxSend hook is required for both patterns.
Resolve the configured token names and expose them on your view model.
Read the configured cookie and header names in the action that renders your view, and
pass them to the view model. Do not hard-code the defaults.
In Controllers/YourController.cs, add this code to your Index
(GET) action and a GetCsrfSetting helper method.
Resolving
CSRF token names in the controller
// In your Index (GET) action:
var model = new YourAppModel
{
CsrfCookieName = GetCsrfSetting("CookieName", "XSRF-TOKEN"),
CsrfHeaderName = GetCsrfSetting("HeaderName", "X-XSRF-TOKEN"),
// ...other model properties
};
return View(model);
// Helper — reads AntiForgery:CookieName / AntiForgery:HeaderName from appSettings,
// falling back to the middleware defaults when the key is absent.
private static string GetCsrfSetting(string key, string fallback)
{
var value = ConfigurationManager.AppSettings["AntiForgery:" + key];
return string.IsNullOrWhiteSpace(value) ? fallback : value.Trim();
}
In Models/YourAppModel.cs, add the two name properties to your
ICMSCustomApplicationModel implementation.
CSRF name properties on the view model
public string CsrfCookieName { get; set; }
public string CsrfHeaderName { get; set; }
Apply [HttpPost] to every state-changing action .
Without [HttpPost], the same action also answers GET requests, which
the middleware exempts from CSRF. An attacker can trigger it through a passive request
(for example, a hidden <img> tag) and bypass the token check
entirely.
HttpPost decoration for Option A and Option B actions
// Option A — CMS middleware is the sole CSRF layer.
[HttpPost]
public ActionResult Copy(string id)
{
// controller logic here
}
// Option B — MVC attribute adds a second, independent CSRF layer.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CheckOut(string id)
{
// controller logic here
}
Important
Ensure read-only actions use GET, not POST. The middleware exempts
safe methods, so GET actions require no token. Keeping read-only work on GET is part of
correct CSRF implementation.
In Views/YourApp/Index.cshtml, add the global jQuery
ajaxSend hook to your view's <head>.
The hook reads the XSRF-TOKEN cookie and attaches its value as the
configured header on every non-GET AJAX call. A single global hook covers all requests
in the app. Do not duplicate this logic per call.
Global ajaxSend hook for CSRF header attachment
// Names come from the view model, resolved server-side from CMS config.
var csrfCookieName = '@Model.CsrfCookieName'; // e.g. "XSRF-TOKEN"
var csrfHeaderName = '@Model.CsrfHeaderName'; // e.g. "X-XSRF-TOKEN"
function getCsrfToken() {
var match = ('; ' + document.cookie).split('; ' + csrfCookieName + '=');
if (match.length < 2) return null;
return decodeURIComponent(match.pop().split(';').shift());
}
// Attach the header on every non-GET AJAX request — covers both Option A and Option B.
$(document).ajaxSend(function (event, jqXHR, settings) {
var method = (settings.type || 'GET').toUpperCase();
if (method === 'GET' || method === 'HEAD') return;
var token = getCsrfToken();
if (token) jqXHR.setRequestHeader(csrfHeaderName, token);
});
// Surface 403s — a silent failure here is easy to mistake for a server error.
$(document).ajaxError(function (event, jqXHR) {
if (jqXHR.status === 403) {
showMessage('Your security token is missing or has expired. Please refresh the page and try again.');
}
});
For Option B only: Embed the MVC token in your form and submit the serialized
form data.
If you use [ValidateAntiForgeryToken] on the server action, the view
must also emit the hidden MVC token field. Use @Html.AntiForgeryToken()
inside the form and submit via $(form).serialize() so the field is
included in the POST body.
Add @Html.AntiForgeryToken() inside the form markup.
Submit the form with $(form).serialize() so the hidden token field
is included.
AJAX form submission with serialize() (Option B)
$('#myForm').on('submit', function (e) {
e.preventDefault();
$.ajax({
url: $(this).attr('action'),
method: 'POST',
data: $(this).serialize() // includes __RequestVerificationToken + other fields
}).done(function (r) { /* handle response */ });
});
Note
$(form).serialize() is required here. Passing a plain
data object (for example, data: { id: val }) omits the hidden token
field, so every submission fails MVC's anti-forgery check.