1

In this post on stackoverflow, it is established that HttpSession is not thread-safe. Specifically,

The Developer has the responsibility for thread-safe access to the attribute objects themselves. This will protect the attribute collection inside the HttpSession object from concurrent access, eliminating the opportunity for an application to cause that collection to become corrupted.

However, looking at the implementation of Catalina's CsrfPreventionFilter, I see no synchronization applied:

HttpSession session = req.getSession(false);

@SuppressWarnings("unchecked")
LruCache<String> nonceCache = (session == null) ? null
        : (LruCache<String>) session.getAttribute(
                Constants.CSRF_NONCE_SESSION_ATTR_NAME);


...

if (nonceCache == null) {
    nonceCache = new LruCache<String>(nonceCacheSize);
    if (session == null) {
        session = req.getSession(true);
    }
    session.setAttribute(
            Constants.CSRF_NONCE_SESSION_ATTR_NAME, nonceCache);
}

Could you please explain if this kind of access is safe, and why?


Update: In response to Andrew's answer below: Please check the following code. In function process() of ThreadDemo, while req is a local variable, it's not thread-safe, as it holds reference to the shared object session.

import java.util.HashMap;
import java.util.Map;

public class Main {

    public static void main(String[] args) {
        Session session = new Session();
        int len = 10;

        ThreadDemo[] t = new ThreadDemo[len];

        for (int i = 0; i < len; i++) {
            t[i] = new ThreadDemo(session);
            t[i].start();
        }
    }
}

class ThreadDemo extends Thread {
    private final Session session;

    ThreadDemo(Session session) {
        this.session = session;
    }

    @Override
    public void run() {
        Request request = new HttpRequest(session);
        process(request);
    }

    private void process(Request request) {
        HttpRequest req = (HttpRequest) request;
        Session session = req.getSession();

        if (session.getAttribute("TEST") == null)
            session.putAttribute("TEST", new Object());
        else
            session.clearAttributes();

        System.out.println(session.getAttribute("TEST") == null);
    }
}

class Session {
    private final Map<String, Object> session;

    Session() {
        session = new HashMap<>();
    }

    Object getAttribute(String attr) {
        return session.get(attr);
    }

    void putAttribute(String attr, Object o) {
        session.put(attr, o);
    }

    void clearAttributes() {
        session.clear();
    }
}

class Request {
    private final Session session;

    Request(Session session) {
        this.session = session;
    }

    Session getSession() {
        return session;
    }
}

class HttpRequest extends Request {
    HttpRequest(Session session) {
        super(session);
    }
}
Sadeq Dousti
  • 3,346
  • 6
  • 35
  • 53

1 Answers1

0

From the source

HttpSession session = req.getSession(false);  // line 196

req is local so is thread safe, and if concurrent requests are received where the above session (unique to each thread) results in null, then it's reasonably safe to assume the concurrent requests represent different users (or at least unrelated requests from a single user).

Later, a new session is established (unique to to each thread):

session = req.getSession(true);   // line 217

At this point there is no way for another concurrent request to be related to the above non-null session since the client has not yet received a response so the client does not yet know its session ID to send during a subsequent request.

So it's safe to create the session attribute for the just created session:

session.setAttribute(Constants.CSRF_NONCE_SESSION_ATTR_NAME, nonceCache);  // line 219

Then a response is sent back to the client with the session ID. The next request includes the session ID so

session = req.getSession(true);   // line 217

will not be null. And similarly, nonceCache will not be null so this whole block

if (nonceCache == null) {
    nonceCache = new LruCache<String>(nonceCacheSize);
    if (session == null) {
        session = req.getSession(true);
    }
    session.setAttribute(
        Constants.CSRF_NONCE_SESSION_ATTR_NAME, nonceCache);
}

will be skipped during subsequent requests related to the existing session.

Access to the elements of the cache are synchronized in the nested LruCache class lines 358 and 364.

Andrew S
  • 2,509
  • 1
  • 12
  • 14
  • Thanks for the answer. I think the part you claim "`req` is local so is thread safe" is not correct, since `req` holds a reference to the shared object `session`. Please see my updated question for a sample code that shows my point. – Sadeq Dousti Oct 06 '18 at 10:13
  • The updated example assumes `session` already exists, which is not the case. `req.getssion(false)` returns null if it does not already exist. If it doesn't already exist then the session and cache are initialized. No other concurrent request could access the same session since no client would know the session ID associated with the just initialized session. If `req.getSession(false)` is not null, then the session was previously initialized during an earlier request and that initialization block is skipped. – Andrew S Oct 08 '18 at 13:44
  • Assume first thread calls `req.getssion(false)`, which returns `null`. So, a new `nonceCache` is created. Below, the thread checks if session is null, creates the session using `req.getssion(false)`. At this moment, a context switch happens, and the second thread kicks in. `req.getssion(false)` is executed. Since no synchronization took place, JMM does not guarantee that the second thread sees the changes made by the first thread. The second thread may create a new session as well. See [visibility failure](https://www.ibm.com/developerworks/library/j-jtp09238/index.html) for more info. – Sadeq Dousti Oct 08 '18 at 17:57
  • The second thread will also create a new session **if the client does not send the session ID associated with an already existing client**. But there's no way for a client to know the session ID if the first thread had not already completed. In the case of a "double click" there's no way for the filter to know the two threads are related since there is not yet an existing session. So there could be a session created which immediately becomes orphaned, but it's still thread safe. – Andrew S Oct 08 '18 at 18:30
  • I can think of other problems, say one thread obtains the session and adds a nonce to it, which remains "invisible" to another thread because the second one obtained a different copy of the session. From what I see, subtle cases may occur which threaten thread safety. I think we should not check them case-by-case to see if they align with our program semantics, as they may subtly skip our grasp. I'll add a pull request for this ASAP. – Sadeq Dousti Oct 09 '18 at 07:16