Search code examples
ajaxspring-mvcpostspring-securitycsrf

Ajax POST wont work with spring security _csrf but GET method works


I'm trying to submit data using jquery ajax post method but so far i failed to do that. I'm using spring security 4.0.4.RELEASE and Spring framework 4.2.5.RELEASE. After so many tons of googling about why GET works and POST didn't. I found out the _csrf token has something to do why POST method is not allowed. I try to add the token at the URL then it allow now the POST method but it retrieve null value to all data that pass to the data parameter. Here are my sample code.

Controller:

@RequestMapping(value = "/ajaxAddDeliveryType", method = RequestMethod.GET, produces = "application/json")
    @ResponseBody
    public JsonResponse addDeliveryType(
            @ModelAttribute(value = "delivery") DeliveryType deliveryType,
            BindingResult result) {

        System.out.println("deliveryType "+deliveryType.toString());

        JsonResponse res = new JsonResponse();

        /* Validate all the input. it return "SUCCESS" or "FAIL" status */
        jsonResponse(res, result, deliveryType);

        if (res.getStatus().equalsIgnoreCase("success")) {
            /*
             * If result status is Success insert the data into database
             */
            deliveryTypeService.saveDeliveryType(deliveryType);
        }

        return res;
    }


/*
     * This method will validate all input field in form and returning response.
     * If result has error detected it will set the status to "FAIL". If no
     * error occured in result it will set the status to "SUCCESS".
     */
    public JsonResponse jsonResponse(JsonResponse res, BindingResult result,
            DeliveryType deliveryType) {

        /* Set error message if text field is empty */

        ValidationUtils.rejectIfEmptyOrWhitespace(result,
                "mainte_delivery_type", "Delivery type can not be empty");

        ValidationUtils.rejectIfEmptyOrWhitespace(result, "delivery_weight",
                "Delivery weight not be empty");

        ValidationUtils.rejectIfEmptyOrWhitespace(result, "delivery_price",
                "Delivery price can not be empty.");

        if (!deliveryTypeService.isDeliveryTypeUnique(deliveryType.getId(),
                deliveryType.getMainte_delivery_type())) {

            /* Set status to fail */
            res.setStatus("FAIL");

            /* Set error message if delivery type already exist */
            result.rejectValue("mainte_delivery_type",
                    "Delivery type already exists. Please fill in different value");

            res.setResult(result.getAllErrors());

        }

        if (result.hasErrors()) {

            /* Set status to fail */
            res.setStatus("FAIL");

            /*
             * Collect all error messages for text field that not properly
             * assign value
             */
            res.setResult(result.getAllErrors());

        }

        /* Validate if the weight or price is more than zero / 0 */
        else if (deliveryType.getDelivery_price().toString()
                .equalsIgnoreCase("0")
                || deliveryType.getDelivery_weight().toString()
                        .equalsIgnoreCase("0")) {

            /* Set status to fail */
            res.setStatus("FAIL");

            /* Set error message if delivery type already exist */
            result.rejectValue("delivery_price",
                    "Delivery weight/price value should be more than '0'");

            res.setResult(result.getAllErrors());

        }

        else {

            deliveryTypes.clear(); /* Clear array list */
            deliveryTypes.add(deliveryType);
            /*
             * Add employee model object into list
             */
            res.setStatus("SUCCESS"); /* Set status to success */
            res.setResult(deliveryTypes); /* Return object into list */

        }

        return res;
    }

Console output when submit ajax POST method:

> deliveryType DeliveryType [id=0, mainte_delivery_type=null, delivery_price=null, delivery_weight=null]

Console output when submit ajax GET method:

deliveryType DeliveryType [id=0, mainte_delivery_type=test, delivery_price=123, delivery_weight=12]

Javacript code:

/*
 * This function will validate all the input field inside the form and return a
     * response if result got an error. otherwise if no error in result is found it
     * will insert the data into database
     */
    function validateAndInsertUsingAjax(action, message) {

        /* Disable button to prevent redundant ajax request */
        $("#btnDeliveryType").prop('disabled', true);


        var datastring = $("#myform").serialize();

        $.ajax({

            type : "GET",
            url : myContext + '/' + action+ '?_csrf=' + $("#token").val(),
            data : datastring,
            contentType : "application/json; charset=utf-8",
            datatype : "json",
            crossDomain : "TRUE",
            success : function(response) {
                var stringResponse = JSON.stringify(response)
                // we have the response

                console.log("response " + stringResponse);

                var obj = JSON.parse(stringResponse);

                if (obj.status == "SUCCESS") {
                    /*
                     * Enable button to make ajax request again after response
                     * return
                     */
                    $("#btnDeliveryType").prop('disabled', false);

                    var userInfo = "<ol>";

                    for (i = 0; i < obj.result.length; i++) {

                        /* Create html elements */

                        userInfo += "<br><li><b>Delivery Type</b> : "
                                + obj.result[i].mainte_delivery_type;

                        userInfo += "<br><li><b>Delivery weight (kg)</b> : "
                                + obj.result[i].delivery_weight;

                        userInfo += "<br><li><b>Delivery Price</b> : "
                                + obj.result[i].delivery_price;

                    }

                    userInfo += "</ol>";

                    /* Draw message in #info div */
                    $('#info').html(message + userInfo);

                    /* Show and hide div */
                    $('#error').hide('slow');
                    $('#info').show('slow');

                    /* Populate DataTable */
                    populateDataTable();

                    /* Hide modal */
                    $('#modalAddDeliveryType').modal('hide');

                } else {
                    /*
                     * Enable button to make ajax request again after response
                     * return
                     */
                    $("#btnDeliveryType").prop('disabled', false);

                    var errorInfo = "";

                    for (i = 0; i < response.result.length; i++) {

                        errorInfo += "<br>" + (i + 1) + ". "
                                + response.result[i].code;

                    }

                    /* Show error message from response */
                    $('#error').html(
                            "Please correct following errors: " + errorInfo);

                    /* Show and hide div */
                    $('#info').hide('slow');
                    $('#error').show('slow');

                }

            },

            /* xhr.status shows server respond */
            error : function(xhr, desc, err) {
                /*
                 * Enable button to make ajax request again after response return
                 */

                $("#btnDeliveryType").prop('disabled', false);
                if (xhr.status == 500) {
                    alert('Error: ' + "Server not respond ");
                }
                if (xhr.status == 403) {
                    alert('Error: ' + "Access Denied");
                }

            }

        });

    }

JSP page:

<div class="modal fade" id="modalAddDeliveryType" tabindex="-1"
        role="dialog" aria-labelledby="exampleModalCenterTitle"
        aria-hidden="true">
        <div class="modal-dialog modal-dialog-centered" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <h3 class="modal-title" id="exampleModalLongTitle"></h3>
                    <button type="button" class="close" data-dismiss="modal"
                        aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>

                <input type="hidden" id="token" name="${_csrf.parameterName}"
                            value="${_csrf.token}" />

                <!-- Form Text field -->
                <form:form method="GET" modelAttribute="delivery" name="myform"
                    id="myform">
                    <form:input type="hidden" path="id" id="id" />
                    <div class="modal-body">

                        <!-- Input Delivery type -->
                        <div>
                            <label for="mainte_delivery_type">Delivery type: </label>
                            <form:input path="mainte_delivery_type" id="mainte_delivery_type"
                                class="form-control" placeholder="Delivery type" />
                            <form:errors path="mainte_delivery_type" cssClass="error" />
                        </div>

                        <!-- Input Delivery weight -->
                        <div>
                            <label for="delivery_weight">Delivery weight (kg): </label>
                            <form:input type="number" min="0" path="delivery_weight"
                                id="delivery_weight" class="form-control"
                                placeholder="Delivery weight(kg)" />
                            <form:errors path="delivery_weight" cssClass="error" />
                        </div>

                        <!-- Input Delivery price -->
                        <div>
                            <label for="delivery_price">Delivery price: </label>
                            <form:input type="number" min="0" path="delivery_price"
                                id="delivery_price" class="form-control"
                                placeholder="Delivery price" />
                            <form:errors path="delivery_price" cssClass="error" />
                        </div>




                        <div id="error" class="error"></div>

                    </div>

                    <div class="modal-footer">

                        <!-- Close button -->
                        <button type="button" class="btn btn-secondary"
                            data-dismiss="modal">Close</button>

                        <!-- Register button -->
                        <input type="submit" class="btn btn-primary" value="Save"
                            id="btnDeliveryType" onClick="insertOrUpdateDeliveryType()" />

                    </div>
                </form:form>
            </div>
        </div>
    </div>

I hope somebody would provide me a better way to implement this POST method. I'm new at spring security.


Solution

  • CSRF tokens are used to prevent remote 3rd parties from forging requests. Basically, an attacker will copy the form exactly as is, and then force an unsuspecting user on a website controlled by the attacker, to send a request on behalf of the user, which will inevitably include the user's cookie, to a usually legitimate website.

    So, CSRF tokens are random to say the least, and locked down to sessions. It's like including a cookie in each form you submit, and GET requests should include these too, so long as an action is being completed.

    Assuming you are not attempting to do anything malicious here, and have full control of your bot, you can easily send an initial GET request to the website, establish a session, and get your CSRF token unique to your session from the HTML (will be embedded as a value within an <input> tag).

    EDIT: By the way, if you're in control of the destination site (one which is generating this CSRF token!), then obviously you can whitelist yourself, or send that information specifically to the user through AJAX. There are ways around this if that's the case.