web-dev-qa-db-fra.com

appels jQuery Ajax et le Html.AntiForgeryToken ()

J'ai implémenté dans mon application l'atténuation des attaques CSRF en suivant les informations que j'ai lues sur certains billets de blog sur Internet. En particulier, ces publications ont été le moteur de ma mise en œuvre.

Fondamentalement, ces articles et recommandations disent que pour empêcher l'attaque de CSRF, tout le monde devrait implémenter le code suivant:

1) Ajoutez le [ValidateAntiForgeryToken] à chaque action qui accepte le verbe POST Http

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult SomeAction( SomeModel model ) {
}

2) Ajoutez l'assistant <%= Html.AntiForgeryToken() %> à l'intérieur des formulaires qui soumettent des données au serveur.

<div style="text-align:right; padding: 8px;">
    <%= Html.AntiForgeryToken() %>
    <input type="submit" id="btnSave" value="Save" />
</div>

Quoi qu'il en soit, dans certaines parties de mon application, je fais Ajax Ajax avec jQuery sur le serveur sans avoir aucune forme. Cela se produit par exemple lorsque je laisse l’utilisateur cliquer sur une image pour effectuer une action spécifique. 

Supposons que j'ai une table avec une liste d'activités. J'ai une image sur une colonne du tableau qui dit "Marquer l'activité comme étant terminée" et lorsque l'utilisateur clique sur cette activité, je fais le Ajax POST comme dans l'exemple suivant:

$("a.markAsDone").click(function (event) {
    event.preventDefault();
    $.ajax({
        type: "post",
        dataType: "html",
        url: $(this).attr("rel"),
        data: {},
        success: function (response) {
            // ....
        }
    });
});

Comment utiliser la <%= Html.AntiForgeryToken() %> dans ces cas? Devrais-je inclure l'appel d'assistance dans le paramètre data de l'appel Ajax?

Désolé pour le long post et merci beaucoup pour votre aide

MODIFIER:

Selon jayrdub answer j'ai utilisé de la manière suivante

$("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) {
            // ....
        }
    });
});
189
Lorenzo

J'utilise une simple fonction js comme celle-ci

AddAntiForgeryToken = function(data) {
    data.__RequestVerificationToken = $('#__AjaxAntiForgeryForm input[name=__RequestVerificationToken]').val();
    return data;
};

Puisque chaque formulaire d'une page aura la même valeur pour le jeton, il suffit de mettre quelque chose comme ceci dans votre page maître la plus haute.

<%-- used for ajax in AddAntiForgeryToken() --%>
<form id="__AjaxAntiForgeryForm" action="#" method="post"><%= Html.AntiForgeryToken()%></form>  

Puis dans votre appel ajax faire (édité pour correspondre à votre deuxième exemple)

$.ajax({
    type: "post",
    dataType: "html",
    url: $(this).attr("rel"),
    data: AddAntiForgeryToken({ id: parseInt($(this).attr("title")) }),
    success: function (response) {
        // ....
    }
});
233
JeremyWeir

J'aime la solution fournie par 360Airwalk, mais elle peut être un peu améliorée. 

Le premier problème est que si vous créez $.post() avec des données vides, jQuery n'ajoute pas d'en-tête Content-Type et, dans ce cas, ASP.NET MVC ne parvient pas à recevoir et à vérifier le jeton. Donc, vous devez vous assurer que l'en-tête est toujours là.

Une autre amélioration est la prise en charge de tous les verbes HTTP avec le contenu : POST, PUT, DELETE etc. Bien que vous puissiez utiliser uniquement des POST dans votre application, il est préférable de disposer d'une solution générique et de vérifier un jeton anti-falsification.

$(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);
            }
        }
    });
});
27
Bronx

Je sais qu'il y a beaucoup d'autres réponses, mais cet article est agréable et concis et vous oblige à vérifier tous vos HttpPosts, et pas seulement certains d'entre eux:

http://richiban.wordpress.com/2013/02/06/validating-net-mvc-4-anti-forgery-tokens-in-ajax-requests/

Il utilise des en-têtes HTTP au lieu d'essayer de modifier la collection de formulaires. 

Serveur

//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);
            }
        }
    }
}

Client

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 () {
        ...
    }
});
20
viggity

N'utilisez pas Html.AntiForgeryToken. Utilisez plutôt AntiForgery.GetTokens et AntiForgery.Validate à partir de Web API comme décrit dans Prévention des attaques de type CSRF (Cross-Site Request Forgery) .

19
Edward Brey

Je me sens comme un nécromancien avancé ici, mais c'est toujours un problème 4 ans plus tard dans MVC5.

Pour traiter correctement les demandes ajax, le jeton anti-falsification doit être transmis au serveur lors d'appels ajax. L'intégrer dans vos données et modèles de publication est compliqué et inutile. L'ajout du jeton en tant qu'en-tête personnalisé est propre et réutilisable. Vous pouvez le configurer pour ne pas avoir à vous rappeler de le faire à chaque fois.

Il existe une exception - ajax discret n'a pas besoin de traitement spécial pour les appels ajax. Le jeton est transmis comme d'habitude dans le champ de saisie masqué normal. Exactement la même chose qu'un POST ordinaire.

_Layout.cshtml

Dans _layout.cshtml, j'ai ce bloc JavaScript. Il n'écrit pas le jeton dans le DOM, mais utilise jQuery pour l'extraire du littéral d'entrée masqué généré par l'aide MVC. La chaîne Magic qui correspond au nom de l'en-tête est définie comme une constante dans la classe d'attribut.

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

Notez l'utilisation de guillemets simples dans la fonction beforeSend - l'élément d'entrée qui est rendu utilise des guillemets doubles qui rompraient le littéral JavaScript.

JavaScript client

Lorsque cela est exécuté, la fonction beforeSend ci-dessus est appelée et l'AntiForgeryToken est automatiquement ajouté aux en-têtes de la demande.

$.ajax({
  type: "POST",
  url: "CSRFProtectedMethod",
  dataType: "json",
  contentType: "application/json; charset=utf-8",
  success: function (data) {
    //victory
  }
});

Bibliothèque du serveur

Un attribut personnalisé est requis pour traiter le jeton non standard. Cela s'appuie sur la solution de @ viggity, mais gère correctement les opérations ajax discrètes. Ce code peut être caché dans votre bibliothèque commune

[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);
            }
        }
    }
}

Serveur/Contrôleur

Maintenant, vous appliquez simplement l'attribut à votre action. Mieux encore, vous pouvez appliquer l'attribut à votre contrôleur et toutes les demandes seront validées.

[HttpPost]
[ValidateAntiForgeryTokenOnAllPosts]
public virtual ActionResult CSRFProtectedMethod()
{
  return Json(true, JsonRequestBehavior.DenyGet);
}
15
Will D

Je ne faisais que mettre en œuvre ce problème dans mon projet actuel. Je l'ai fait pour tous les ajax-POST ayant besoin d'un utilisateur authentifié.

Tout d’abord, j’ai décidé de raccrocher à ma jappery ajax appelle donc je ne me répète pas trop souvent. Cet extrait de code javascript garantit que tous les appels ajax (post) ajouteront le jeton de validation de ma demande à la demande. Remarque: le nom __RequestVerificationToken est utilisé par le framework .Net afin que je puisse utiliser les fonctionnalités standard Anti-CSRF comme indiqué ci-dessous.

$(document).ready(function () {
    var securityToken = $('[name=__RequestVerificationToken]').val();
    $('body').bind('ajaxSend', function (Elm, xhr, s) {
        if (s.type == 'POST' && typeof securityToken != 'undefined') {
            if (s.data.length > 0) {
                s.data += "&__RequestVerificationToken=" + encodeURIComponent(securityToken);
            }
            else {
                s.data = "__RequestVerificationToken=" + encodeURIComponent(securityToken);
            }
        }
    });
});

Dans vos vues où vous avez besoin que le jeton soit disponible pour le javascript ci-dessus, utilisez simplement le HTML-Helper commun. Vous pouvez fondamentalement ajouter ce code où vous voulez. Je l'ai placé dans une instruction if (Request.IsAuthenticated):

@Html.AntiForgeryToken() // you can provide a string as salt when needed which needs to match the one on the controller

Dans votre contrôleur, utilisez simplement le mécanisme standard ASP.Net MVC Anti-CSRF. Je l'ai fait comme ça (même si j'ai utilisé du sel).

[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public JsonResult SomeMethod(string param)
{
    // do something
    return Json(true);
}

Avec Firebug ou un outil similaire, vous pouvez facilement voir comment vos demandes POST ont maintenant un paramètre __RequestVerificationToken ajouté.

15
360Airwalk

Je pense que tout ce que vous avez à faire est de vous assurer que l'entrée "__RequestVerificationToken" est incluse dans la demande POST. L’autre moitié des informations (le jeton contenu dans le cookie de l’utilisateur) est déjà envoyée automatiquement avec une demande AJAX POST.

Par exemple.,

$("a.markAsDone").click(function (event) {
    event.preventDefault();
    $.ajax({
        type: "post",
        dataType: "html",
        url: $(this).attr("rel"),
        data: { 
            "__RequestVerificationToken":
            $("input[name=__RequestVerificationToken]").val() 
        },
        success: function (response) {
            // ....
        }
    });
});
11
jball

Vous pouvez le faire aussi:

$("a.markAsDone").click(function (event) {
    event.preventDefault();

    $.ajax({
        type: "post",
        dataType: "html",
        url: $(this).attr("rel"),
        data: $('<form>@Html.AntiForgeryToken()</form>').serialize(),
        success: function (response) {
        // ....
        }
    });
});

Ceci utilise Razor, mais si vous utilisez la syntaxe WebForms, vous pouvez également utiliser les balises <%= %>

Suite à mon commentaire contre la réponse de @ JBall qui m'a aidé tout au long du processus, voici la réponse finale qui fonctionne pour moi. J'utilise MVC et Razor et je soumets un formulaire à l'aide de jQuery AJAX afin de pouvoir mettre à jour une vue partielle avec de nouveaux résultats et je ne voulais pas faire de postback complète (ni de scintillement de page).

Ajoutez la @Html.AntiForgeryToken() à l'intérieur du formulaire comme d'habitude.

Le code de mon bouton de soumission AJAX (c'est-à-dire un événement onclick) est:

//User clicks the SUBMIT button
$("#btnSubmit").click(function (event) {

//prevent this button submitting the form as we will do that via AJAX
event.preventDefault();

//Validate the form first
if (!$('#searchForm').validate().form()) {
    alert("Please correct the errors");
    return false;
}

//Get the entire form's data - including the antiforgerytoken
var allFormData = $("#searchForm").serialize();

// The actual POST can now take place with a validated form
$.ajax({
    type: "POST",
    async: false,
    url: "/Home/SearchAjax",
    data: allFormData,
    dataType: "html",
    success: function (data) {
        $('#gridView').html(data);
        $('#TestGrid').jqGrid('setGridParam', { url: '@Url.Action("GetDetails", "Home", Model)', datatype: "json", page: 1 }).trigger('reloadGrid');
    }
});

J'ai laissé l'action "success" car elle montre comment la vue partielle qui contient un MvcJqGrid est actualisée et comment elle est actualisée (grille très puissante de jqGrid et c'est un excellent wrapper MVC pour cela).

Ma méthode de contrôleur ressemble à ceci:

    //Ajax SUBMIT method
    [ValidateAntiForgeryToken]
    public ActionResult SearchAjax(EstateOutlet_D model) 
    {
        return View("_Grid", model);
    }

Je dois admettre que je ne suis pas un fan de la publication de données d'un formulaire entier en tant que modèle, mais si vous devez le faire, c'est un moyen qui fonctionne. MVC rend simplement la liaison de données trop facile, donc plutôt que de soumettre 16 valeurs individuelles (ou un FormCollection faiblement typé), tout va bien, je suppose. Si vous en savez plus, faites-le moi savoir car je souhaite produire un code MVC C # robuste.

4
Ralph Bacon

1.Define Function pour obtenir un jeton du serveur

@function
{

        public string TokenHeaderValue()
        {
            string cookieToken, formToken;
            AntiForgery.GetTokens(null, out cookieToken, out formToken);
            return cookieToken + ":" + formToken;                
        }
}

2.Obtenir un jeton et définir l'en-tête avant de l'envoyer au serveur

var token = '@TokenHeaderValue()';    

       $http({
           method: "POST",
           url: './MainBackend/MessageDelete',
           data: dataSend,
           headers: {
               'RequestVerificationToken': token
           }
       }).success(function (data) {
           alert(data)
       });

3. Validation sur serveur sur HttpRequestBase sur la méthode que vous gérez Post/get

        string cookieToken = "";
        string formToken = "";
        string[] tokens = Request.Headers["RequestVerificationToken"].Split(':');
            if (tokens.Length == 2)
            {
                cookieToken = tokens[0].Trim();
                formToken = tokens[1].Trim();
            }
        AntiForgery.Validate(cookieToken, formToken);
3

a trouvé cette idée très intelligente de https://Gist.github.com/scottrippey/3428114 pour chaque appel de $ .ajax, il modifie la demande et ajoute le jeton. 

// Setup CSRF safety for AJAX:
$.ajaxPrefilter(function(options, originalOptions, jqXHR) {
    if (options.type.toUpperCase() === "POST") {
        // We need to add the verificationToken to all POSTs
        var token = $("input[name^=__RequestVerificationToken]").first();
        if (!token.length) return;

        var tokenName = token.attr("name");

        // If the data is JSON, then we need to put the token in the QueryString:
        if (options.contentType.indexOf('application/json') === 0) {
            // Add the token to the URL, because we can't add it to the JSON data:
            options.url += ((options.url.indexOf("?") === -1) ? "?" : "&") + token.serialize();
        } else if (typeof options.data === 'string' && options.data.indexOf(tokenName) === -1) {
            // Append to the data string:
            options.data += (options.data ? "&" : "") + token.serialize();
        }
    }
});
3
masterlopau

Voici le moyen le plus simple que j'ai vu. Remarque: assurez-vous d'avoir "@ Html.AntiForgeryToken ()" dans votre vue.

  $("a.markAsDone").click(function (event) {
        event.preventDefault();
        var sToken = document.getElementsByName("__RequestVerificationToken")[0].value;
        $.ajax({
            url: $(this).attr("rel"),
            type: "POST",
            contentType: "application/x-www-form-urlencoded",
            data: { '__RequestVerificationToken': sToken, 'id': parseInt($(this).attr("title")) }
        })
        .done(function (data) {
            //Process MVC Data here
        })
        .fail(function (jqXHR, textStatus, errorThrown) {
            //Process Failure here
        });
    });
1
Dominic Sputo

Je sais que cela fait longtemps que cette question n'a pas été postée, mais j'ai trouvé une ressource vraiment utile, qui traite de l'utilisation d'AntiForgeryToken et la rend moins gênante à utiliser. Il fournit également le plugin jquery permettant d’inclure facilement un jeton antiforgery dans les appels AJAX:

Recettes de demande anti-falsification pour ASP.NET MVC et AJAX

Je ne contribue pas beaucoup, mais peut-être que quelqu'un le trouvera utile.

1
slawek

J'utilise une publication ajax pour exécuter une méthode de suppression (elle provient d'un scénario de visjs mais ce n'est pas pertinent). C'est ce que je sis:

Ceci est mon Index.cshtml

@Scripts.Render("~/bundles/schedule")
@Styles.Render("~/bundles/visjs")
@Html.AntiForgeryToken()

<!-- div to attach schedule to -->
<div id='schedule'></div>

<!-- div to attach popups to -->
<div id='dialog-popup'></div>

Tout ce que j'ai ajouté ici était @Html.AntiForgeryToken() pour que le jeton apparaisse dans la page

Ensuite, dans mon post ajax, j'ai utilisé:

$.ajax(
    {
        type: 'POST',
        url: '/ScheduleWorks/Delete/' + item.id,
        data: {
            '__RequestVerificationToken': 
            $("input[name='__RequestVerificationToken']").val()
              }
     }
);

Qui ajoute la valeur du jeton, extraite de la page, aux champs postés

Avant cela, j’essayais de mettre la valeur dans les en-têtes mais j’avais la même erreur

N'hésitez pas à poster des améliorations. Cela semble certainement être une approche simple que je peux comprendre

0
Nick.McDermaid

Légère amélioration de la solution 360Airwalk. Cela inclut le jeton Anti Forgery dans la fonction javascript. @ Html.AntiForgeryToken () n'a donc plus besoin d'être inclus dans chaque vue.

$(document).ready(function () {
    var securityToken = $('@Html.AntiForgeryToken()').attr('value');
    $('body').bind('ajaxSend', function (Elm, xhr, s) {
        if (s.type == 'POST' && typeof securityToken != 'undefined') {
            if (s.data.length > 0) {
                s.data += "&__RequestVerificationToken=" + encodeURIComponent(securityToken);
            }
            else {
                s.data = "__RequestVerificationToken=" + encodeURIComponent(securityToken);
            }
        }
    });
});
0
Barry MSIH

première utilisation @ Html.AntiForgeryToken () en html

 $.ajax({
        url: "@Url.Action("SomeMethod", "SomeController")",
        type: 'POST',
        data: JSON.stringify(jsonObject),
        contentType: 'application/json; charset=utf-8',
        dataType: 'json',
        async: false,
        beforeSend: function (request) {
            request.setRequestHeader("RequestVerificationToken", $("[name='__RequestVerificationToken']").val());
        },
        success: function (msg) {
            alert(msg);
        }
0
Amir Reza
function DeletePersonel(id) {

    var data = new FormData();
    data.append("__RequestVerificationToken", "@HtmlHelper.GetAntiForgeryToken()");

    $.ajax({
        type: 'POST',
        url: '/Personel/Delete/' + id,
        data: data,
        cache: false,
        processData: false,
        contentType: false,
        success: function (result) {
        }
    });
}

public static class HtmlHelper {
    public static string GetAntiForgeryToken() {
        System.Text.RegularExpressions.Match value = 
                System.Text.RegularExpressions.Regex.Match(System.Web.Helpers.AntiForgery.GetHtml().ToString(), 
                        "(?:value=\")(.*)(?:\")");
        if (value.Success) {
            return value.Groups[1].Value;
        }
        return "";
    }
}
0
ismail eski

Bon nombre de posts ici, aucun d’entre eux ne m’a aidé, des jours et des jours de google, et toujours pas plus loin, j’en suis arrivé au point de tout écrire à partir de l’application, puis j’ai remarqué cette petite pépite dans mon Web.confg

 <httpCookies requireSSL="false" domain="*.localLookup.net"/>

Maintenant, je ne sais pas pourquoi je l'ai ajouté, mais j'ai remarqué depuis, il est ignoré en mode débogage et non en mode production (IE installé sur IIS quelque part)

Pour moi, la solution était l'une des 2 options, car je ne me souviens pas pourquoi je l'ai ajoutée. Je ne peux donc pas être sûr que d'autres choses ne dépendent pas d'elle. Deuxièmement, le nom de domaine doit être tout en minuscule et un TLD différent de celui que j'ai déjà fait. dans * .localLookup.net 

Peut-être que ça aide peut-être que non. J'espère que ça aide quelqu'un

0
Agitated