Llamadas jQuery Ajax y Html.AntiForgeryToken()
He implementado en mi aplicación la mitigación de ataques CSRF siguiendo la información que leí en alguna publicación de blog en Internet. En particular, estas publicaciones han sido el motor de mi implementación.
- Mejores prácticas para ASP.NET MVC del equipo de contenido para desarrolladores de herramientas web y ASP.NET
- Anatomía de un ataque de falsificación de solicitudes entre sitios del blog de Phil Haack
- AntiForgeryToken en ASP.NET MVC Framework: atributos Html.AntiForgeryToken y ValidateAntiForgeryToken del blog de David Hayden
Básicamente, esos artículos y recomendaciones dicen que para prevenir el ataque CSRF cualquiera debería implementar el siguiente código:
Agregue el
[ValidateAntiForgeryToken]
en cada acción que acepte el verbo POST Http[HttpPost] [ValidateAntiForgeryToken] ActionResult público SomeAction (modelo SomeModel) {}
Agregue el
<%= Html.AntiForgeryToken() %>
ayudante dentro de los formularios que envía datos al servidor
De todos modos, en algunas partes de mi aplicación estoy haciendo POST Ajax con jQuery al servidor sin tener ningún formulario. Esto sucede, por ejemplo, cuando permito que el usuario haga clic en una imagen para realizar una acción específica.
Supongamos que tengo una tabla con una lista de actividades. Tengo una imagen en una columna de la tabla que dice "Marcar actividad como completada" y cuando el usuario hace clic en esa actividad estoy haciendo el Ajax POST como en el siguiente ejemplo:
$("a.markAsDone").click(function (event) {
event.preventDefault();
$.ajax({
type: "post",
dataType: "html",
url: $(this).attr("rel"),
data: {},
success: function (response) {
// ....
}
});
});
¿ Cómo puedo utilizar el <%= Html.AntiForgeryToken() %>
en estos casos? ¿Debo incluir la llamada de ayuda dentro del parámetro de datos de la llamada Ajax?
Perdón por la publicación tan larga y muchas gracias por ayudar.
EDITAR :
Según la respuesta de jayrdub , lo he usado de la siguiente manera
$("a.markAsDone").click(function (event) {
event.preventDefault();
$.ajax({
type: "post",
dataType: "html",
url: $(this).attr("rel"),
data: {
AddAntiForgeryToken({}),
id: parseInt($(this).attr("title"))
},
success: function (response) {
// ....
}
});
});
Yo uso una función js simple como esta
AddAntiForgeryToken = function(data) {
data.__RequestVerificationToken = $('#__AjaxAntiForgeryForm input[name=__RequestVerificationToken]').val();
return data;
};
Dado que cada formulario en una página tendrá el mismo valor para el token, simplemente coloque algo como esto en su página maestra superior.
<%-- used for ajax in AddAntiForgeryToken() --%>
<form id="__AjaxAntiForgeryForm" action="#" method="post"><%= Html.AntiForgeryToken()%></form>
Luego, en su llamada ajax, haga (editado para que coincida con su segundo ejemplo)
$.ajax({
type: "post",
dataType: "html",
url: $(this).attr("rel"),
data: AddAntiForgeryToken({ id: parseInt($(this).attr("title")) }),
success: function (response) {
// ....
}
});
Me gusta la solución proporcionada por 360Airwalk, pero puede mejorarse un poco.
El primer problema es que si lo hace $.post()
con datos vacíos, jQuery no agrega un Content-Type
encabezado y, en este caso, ASP.NET MVC no puede recibir ni verificar el token. Por lo tanto, debes asegurarte de que el encabezado esté siempre ahí.
Otra mejora es la compatibilidad con todos los verbos HTTP con contenido : POST, PUT, DELETE, etc. Aunque puede usar solo POST en su aplicación, es mejor tener una solución genérica y verificar que todos los datos que reciba con cualquier verbo tengan protección contra falsificación. simbólico.
$(document).ready(function () {
var securityToken = $('[name=__RequestVerificationToken]').val();
$(document).ajaxSend(function (event, request, opt) {
if (opt.hasContent && securityToken) { // handle all verbs with content
var tokenParam = "__RequestVerificationToken=" + encodeURIComponent(securityToken);
opt.data = opt.data ? [opt.data, tokenParam].join("&") : tokenParam;
// ensure Content-Type header is present!
if (opt.contentType !== false || event.contentType) {
request.setRequestHeader( "Content-Type", opt.contentType);
}
}
});
});
Sé que hay muchas otras respuestas, pero este artículo es agradable y conciso y te obliga a revisar todos tus HttpPosts, no solo algunos de ellos:
http://richiban.wordpress.com/2013/02/06/validating-net-mvc-4-anti-forgery-tokens-in-ajax-requests/
Utiliza encabezados HTTP en lugar de intentar modificar la colección de formularios.
Servidor
//make sure to add this to your global action filters
[AttributeUsage(AttributeTargets.Class)]
public class ValidateAntiForgeryTokenOnAllPosts : AuthorizeAttribute
{
public override void OnAuthorization( AuthorizationContext filterContext )
{
var request = filterContext.HttpContext.Request;
// Only validate POSTs
if (request.HttpMethod == WebRequestMethods.Http.Post)
{
// Ajax POSTs and normal form posts have to be treated differently when it comes
// to validating the AntiForgeryToken
if (request.IsAjaxRequest())
{
var antiForgeryCookie = request.Cookies[AntiForgeryConfig.CookieName];
var cookieValue = antiForgeryCookie != null
? antiForgeryCookie.Value
: null;
AntiForgery.Validate(cookieValue, request.Headers["__RequestVerificationToken"]);
}
else
{
new ValidateAntiForgeryTokenAttribute()
.OnAuthorization(filterContext);
}
}
}
}
Cliente
var token = $('[name=__RequestVerificationToken]').val();
var headers = {};
headers["__RequestVerificationToken"] = token;
$.ajax({
type: 'POST',
url: '/Home/Ajax',
cache: false,
headers: headers,
contentType: 'application/json; charset=utf-8',
data: { title: "This is my title", contents: "These are my contents" },
success: function () {
...
},
error: function () {
...
}
});
Me siento como un nigromante avanzado aquí, pero esto sigue siendo un problema 4 años después en MVC5.
Para manejar las solicitudes ajax correctamente, el token antifalsificación debe pasarse al servidor en las llamadas ajax. Integrarlo en los datos y modelos de sus publicaciones es complicado e innecesario. Agregar el token como un encabezado personalizado es limpio y reutilizable, y puedes configurarlo para no tener que acordarte de hacerlo cada vez.
Hay una excepción: el ajax discreto no necesita un tratamiento especial para las llamadas ajax. El token se pasa como de costumbre en el campo de entrada oculto normal. Exactamente igual que un POST normal.
_Diseño.cshtml
En _layout.cshtml tengo este bloque de JavaScript. No escribe el token en el DOM, sino que utiliza jQuery para extraerlo del literal de entrada oculto que genera MVC Helper. La cadena mágica que es el nombre del encabezado se define como una constante en la clase de atributo.
<script type="text/javascript">
$(document).ready(function () {
var isAbsoluteURI = new RegExp('^(?:[a-z]+:)?//', 'i');
//http://stackoverflow.com/questions/10687099/how-to-test-if-a-url-string-is-absolute-or-relative
$.ajaxSetup({
beforeSend: function (xhr) {
if (!isAbsoluteURI.test(this.url)) {
//only add header to relative URLs
xhr.setRequestHeader(
'@.ValidateAntiForgeryTokenOnAllPosts.HTTP_HEADER_NAME',
$('@Html.AntiForgeryToken()').val()
);
}
}
});
});
</script>
Tenga en cuenta el uso de comillas simples en la función beforeSend: el elemento de entrada que se representa utiliza comillas dobles que romperían el literal de JavaScript.
JavaScript del cliente
Cuando esto se ejecuta, se llama a la función beforeSend anterior y el AntiForgeryToken se agrega automáticamente a los encabezados de la solicitud.
$.ajax({
type: "POST",
url: "CSRFProtectedMethod",
dataType: "json",
contentType: "application/json; charset=utf-8",
success: function (data) {
//victory
}
});
Biblioteca del servidor
Se requiere un atributo personalizado para procesar el token no estándar. Esto se basa en la solución de @viggity, pero maneja correctamente el ajax discreto. Este código se puede guardar en su biblioteca común.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ValidateAntiForgeryTokenOnAllPosts : AuthorizeAttribute
{
public const string HTTP_HEADER_NAME = "x-RequestVerificationToken";
public override void OnAuthorization(AuthorizationContext filterContext)
{
var request = filterContext.HttpContext.Request;
// Only validate POSTs
if (request.HttpMethod == WebRequestMethods.Http.Post)
{
var headerTokenValue = request.Headers[HTTP_HEADER_NAME];
// Ajax POSTs using jquery have a header set that defines the token.
// However using unobtrusive ajax the token is still submitted normally in the form.
// if the header is present then use it, else fall back to processing the form like normal
if (headerTokenValue != null)
{
var antiForgeryCookie = request.Cookies[AntiForgeryConfig.CookieName];
var cookieValue = antiForgeryCookie != null
? antiForgeryCookie.Value
: null;
AntiForgery.Validate(cookieValue, headerTokenValue);
}
else
{
new ValidateAntiForgeryTokenAttribute()
.OnAuthorization(filterContext);
}
}
}
}
Servidor/Controlador
Ahora simplemente aplica el atributo a su Acción. Aún mejor, puede aplicar el atributo a su controlador y todas las solicitudes serán validadas.
[HttpPost]
[ValidateAntiForgeryTokenOnAllPosts]
public virtual ActionResult CSRFProtectedMethod()
{
return Json(true, JsonRequestBehavior.DenyGet);
}