Saturday, September 4, 2010

Authenticating in GWT

Now that we have some basic ground work done for creating accounts and logging in, we can integrate this into our GWT application. We always need to assume that the client is hostile, that the JavaScript code has been changed, or someone is calling our RPC methods directly. None of our authentication checking should be done in the GWT client, except for stuff that decides which pages should be shown. Any method that then provides data to these pages must still do a proper permissions and authentication check before returning anything.

The code is available on github, and the tag for these revisions is add-gwt-authentication.

No more client passwords


Since we're no longer using GWT to enter the user's login information, we no longer need any of the methods we had that passed entered passwords through RPC, namely checkUserPassword() and authenticatedUser(). The password can be sent once through HTTPS on our login form, and a session cookie is set on the client. That cookie maps to a session stored in both the App Engine datastore, as well as memcache. We can use the session under App Engine to store information about the session, such as whether the user is authenticated, and no client program can change the value without going through an RPC call.

So our UserService RemoteService is stripped down to an isAuthenticated() method.

src/com/lisedex/volinfoman/server/UserServiceImpl.java
        @Override
        public boolean isAuthenticated() {
                return SessionHandler.isAuthenticated(getSession());
        }

        private HttpSession getSession() {
                return this.getThreadLocalRequest().getSession();
        }

When we call the isAuthenticated() method, if our session cookie is set, it will be passed to the server in the Cookie header.

Oops. I just made a security hole.


Writing this rattled something loose in the back of my brain, which is apparently the Login Security FAQ for GWT.

The server should not rely on the session ID that comes in the Cookie header, as doing so opens up the cross-site request forgery (XSRF or CSRF) vulnerability.

In short, if the server relies on the Cookie header for the session, and the user's session has not expired, then another site could have a link to something like http://www.vulnerablesite.com/user?deleteMe, which could be even hidden as an <img src> tag. The user's session cookie would automatically be sent with the deleteMe request, and the vulnerable server would go ahead and process the request. If we require the session to be submitted in the RPC payload, the link would have to be something like http://www.vulnerablesite.com/user?deleteMe?sessionId=787asdfasdf. Since the evil site has no way to access the user's session cookie, it can't generate a usable link. Hole closed.

So let's rewind, patch the hole, and do this again. The new code is available under the fix-csrf-hole tag on github.

Fixing the CSRF vulnerability


In looking for a pretty solution to this issue, I came across references to CSRF being addressed in GWT 2.1. The current RpcRequestBuilder in 2.0.4 adds a custom header, X-GWT-Permutation, and it looks like the code in 2.1 at least checks if this header is set. But it doesn't do anything beyond an existence test, on the idea that CSRF attacks can't add custom HTTP headers. However, apparently by using Adobe Flash, you can add custom headers. It would appear that this simple test is not enough. But since 2.1 is not yet out, I'm not sure if this is the extent of what will be checked. Assuming that Google will probably handle it correctly in the end, I decided not to spend a ton of time architecting an elegant solution.

src/com/lisedex/volinfoman/client/data/UserService.java
       boolean isAuthenticated(final String sessionId);

All of my UserService RPC methods will also pass a copy of the JSESSIONID cookie in its parameters. The servlet will then compare the passed copy with the header cookie, and if they match, we assume its a valid RPC call. There's still a possibility that someone could sniff the cookie, but since this software is not exactly financial software, I'm pretty comfortable with the level of security this adds. More elaborate security could be added by using a nonce, or maybe a hash across the session and the command name.

src/com/lisedex/volinfoman/server/UserServiceImpl.java
        public boolean isAuthenticated(final String sessionId) {
                if (validateSessionId(sessionId)) {
                        return SessionHandler.isAuthenticated(getSession());
                } else {
                        LOG.severe("Possible CSRF.  JSESSIONID cookie does " +
                           "not match included session id");
                        return false;
                }
        }

        private HttpSession getSession() {
                return this.getThreadLocalRequest().getSession();
        }

        private boolean validateSessionId(String sessionId) {
                String headerSessionId = (String) getSession().getId();
                if (sessionId != null && headerSessionId != null
                                && headerSessionId.equals(sessionId)) {
                        return true;
                }
                return false;
        }

In each RPC method, we call validateSessionId, which gets the session id from the HttpServletRequest, and compares it with the passed session id. Assuming they match, the RPC method will continue.

src/com/lisedex/volinfoman/server/SessionHandler.java
        public static final String AUTHENTICATED = "authenticated";

        public static void setAuthenticated(HttpSession session,
                        boolean authenticated) {
                session.setAttribute(AUTHENTICATED, authenticated);
        }

        public static boolean isAuthenticated(HttpSession session) {
                if (session == null)
                        return false;

                Boolean auth = (Boolean) session.getAttribute(AUTHENTICATED);
                if (auth == null) {
                        return false;
                } else {
                        return auth.booleanValue();
                }
        }

In the case of isAuthenticated, we call SessionHandler.isAuthenticated, a static method that operates on the HttpServletRequest session, and just gets the value of the authenticated attribute stored in the session table in the datastore.

In our Login servlet, we call the setAuthenticated() method to mark the session as authenticated if the login is successful, and before redirecting the user to the GWT application.

The new home page, MainPageView, is just a SimplePanel, which should allow us to do a very small application load before checking whether the session is authenticated. If it is, we can put a new widget which contains the actual layout in the panel.

src/com/lisedex/volinfoman/client/MainPagePresenter.java
    private static final String HOME_PAGE_URL = "/index.html";

    private UserServiceAsync userService = null;

    @Override
    public void bind() {
        view.setContent(new HTMLPanel(LOADING_MESSAGE));
        userService.isAuthenticated(
                Cookies.getCookie(Volinfoman.SESSION_COOKIE),
                    new AsyncCallback<Boolean>() {
                        @Override
                        public void onSuccess(Boolean result) {
                                if (result.booleanValue() == true) {
                                        view.setContent(new HTMLPanel(
                                            "<h1>AUTHENTICATED</h1>"));
                                } else {
                                        view.setContent(new HTMLPanel(
                                                NOT_AUTHENTICATED_MESSAGE));
                                        Window.Location.replace(HOME_PAGE_URL);
                                }
                        }

                        @Override
                        public void onFailure(Throwable caught) {
                                view.setContent(new HTMLPanel(RPC_FAILURE_MESSAGE));
                        }
                });
    }

We pop a loading message in as the widget in our SimplePanel, and call the RPC isAuthenticated method. Cookies.getCookie is a static method which gives us access to the browser's cookies for our site. If we get a successful response for our RPC method, and it turns out that our session is marked as authenticated, we put up another message just informing us that we authenticated successfully. In the future, we'd set the SimplePanel's widget to some sort of layout panel, and set up our application. If our session is not marked as authenticated, we redirect the browser back to the login page. The Window class gives us access to some of the user's browser's properties and events. In this case, Window.Location allows us to change the current URL.

Paying attention


This definitely shows me how, if I'm not paying attention, security lapses can easily slip right by. Writing the blog entries actually helps me reparse the stuff that I've written, and catch some of my mistakes before I move on, but even then I almost blew right past this one.

The Live HTTP Headers plugin for Firefox made testing and playing with this vulnerability really easy. I could simply catch a remote call, and then replay it with whatever modifications I wanted to make: header modifications or deletions, payload changes. It's pretty smooth. I also made some packet captures, but this plugin is far easier.

I think it's time to start making some sort of user configuration wizard, where people can set up their volunteering preferences, or, if they happen to be a volunteer coordinator, the volunteer opportunities for their organization.

No comments:

Post a Comment