Why are some URLs "forbidden" and some not in my web app

218 Views Asked by At

I have a web application that is experiencing weird behavior. When you try to bring up the app, it requests that you log in as expected, and takes you to the welcome page (/) you can then select either the profile (/profile) page or the search page (/search). If you try to access any of these pages without logging in, it redirects you to the login page as expected. When you try to submit search criteria or submit a password change, however, a 403 Forbidden is returned.

<security:http use-expressions="true">
    <security:intercept-url pattern="/resources/css/*" access="permitAll"  />
    <security:intercept-url pattern="/resources/images/*" access="permitAll"  />
    <security:intercept-url pattern="/login" access="permitAll"  />
    <security:intercept-url pattern="/logout" access="permitAll"  />
    <security:intercept-url pattern="/accessdenied" access="permitAll"  />
    <security:intercept-url pattern="/**" access="hasRole('ROLE_USER')"  />
    <security:form-login 
        login-page="/login" 
        default-target-url="/" 
        authentication-success-handler-ref="loginSuccessHandler" 
        authentication-failure-url="/accessdenied"
    />
    <security:logout 
        logout-success-url="/" 
        logout-url="/perform_logout"
        delete-cookies="JSESSIONID"
    />
</security:http>

urls:

/                       (Welcome Page [GET])
/search                 (Search Page [GET])
/search/data            (Search Query [POST])
/profile                (Profile Page [GET])
/profile/updatePassword (Profile Update [POST])

Profile Controller

@Controller
@RequestMapping({ "/profile" }) 
public class ProfileController {
    @Autowired
    UserService userService = null;
    @Autowired
    ProfileService profileService = null;

    @RequestMapping(value = { "/", "" }, method = RequestMethod.GET)
    public String getProfile(Model model) {
        Profile profile = profileService.getProfile();
        model.addAttribute("profile", profile);
        return "profile";
    }

    @RequestMapping(value = { "/updatePassword" }, method = RequestMethod.POST)
    public @ResponseBody AjaxResponse updatePassword(@RequestBody Profile profile) {
        // do stuff
        return new AjaxResponse(response, null, errors);
    }
}

Search Controller

@Controller
@RequestMapping({ "/search" }) 
public class StockKeepingUnitController {
    @Autowired(required = true)
    private SkuService skuService;
    @Autowired(required = true)
    private UserService userService;

    @RequestMapping(value = {"", "/"}, method = RequestMethod.GET)
    public String search() {
        return "search";
    }

    @RequestMapping(value = "/data", method = RequestMethod.POST)
    public @ResponseBody AjaxResponse data(@RequestBody SearchCriteria searchCriteria) {
        List<StockKeepingUnit> skus = null;
        try {
            String criteria = searchCriteria.getCriteria();
            skus = skuService.listSkusBySearch(criteria);
        } catch (Exception ex) {
            ex.printStackTrace();
            List<String> errors = new ArrayList<>();
            errors.add("Error saving ALOT.");
            return new AjaxResponse("ERROR", null, errors);
        }
        return new AjaxResponse("OK", skus, null);
    }
}

Search ajax

    $.ajax({url: "${pageContext.request.contextPath}/search/data"
        , method: "POST"
        , contentType: "application/json; charset=utf-8"
        , dataType: "json"
        , data: JSON.stringify(searchCriteria)
        , success: function(ajaxResponse) { /* ... */ }
        , error: function(xhr, status, error) { /* ... */ }
    });

Profile ajax

$.ajax({
    url: "${pageContext.request.contextPath}/profile/updatePassword",
    , method: "POST"
    , contentType: "application/json; charset=utf-8"
    , dataType: "json"
    , data: JSON.stringify(profile)
    , success : function(ajaxResponse) { /* ... */ }
    , error : function(xhr, status, error) { /* ... */ }
});

---EDIT--- jQuery for csrf

$(function() {
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    $(document).ajaxSend(function(e, xhr, options) {
        xhr.setRequestHeader(header, token);
    });
});

Also I just found out that if I reload each page, the POST submission works. Is there some way the CSRF token changes on each page? I am using jQuery Mobile btw.

1

There are 1 best solutions below

2
bcr666 On BEST ANSWER

The problem is caused because jQuery Mobile doesn't normally load header information on each page request, which is where the CSRF token is stored. So when navigating to a new page, it is using a stale CSRF token when doing a POST, which causes the 403 Forbidden. To overcome this, I forced JQM to link without ajax by including data-ajax="false" in each link to a page. For example:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<form action="<c:url value="/perform_logout" />" method="POST" name="logoutform">
    <input type="hidden" name="${_csrf.parameterName}" value = "${_csrf.token}" />          
</form>
<ul data-role="listview" data-theme="a" data-divider-theme="a" style="margin-top: -16px;" class="nav-search">
    <li data-icon="delete" style="background-color: #111;"><a href="#" data-rel="close">Close menu</a></li>
    <li><a href="${pageContext.request.contextPath}/search" data-ajax="false">Search</a></li>
    <li><a href="${pageContext.request.contextPath}/profile" data-ajax="false">Profile</a></li>
    <li><a href="#" onclick="document.logoutform.submit();">Logout</a></li>
</ul>