Skip to content

SessionManagementFilter thread safety #5775

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
daniloarcidiacono opened this issue Sep 6, 2018 · 8 comments
Open

SessionManagementFilter thread safety #5775

daniloarcidiacono opened this issue Sep 6, 2018 · 8 comments
Labels
status: waiting-for-triage An issue we've not yet triaged

Comments

@daniloarcidiacono
Copy link

I'm facing some problems with concurrent session management, in particular when multiple concurrent requests are performed. Using this configuration:

 http
    .sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
        .maximumSessions(1)
            .sessionRegistry(sessionRegistry)
            .maxSessionsPreventsLogin(true)

a given user can create only one session at a time. It works, but when I try to login multiple times with many threads, the checks performed by ConcurrentSessionControlAuthenticationStrategy are bypassed. Looking at the implementation it seems that this class is not designed with thread-safety in mind:

    final List<SessionInformation> sessions = sessionRegistry.getAllSessions(
            authentication.getPrincipal(), false);

    int sessionCount = sessions.size();
    int allowedSessions = getMaximumSessionsForThisUser(authentication);

    if (sessionCount < allowedSessions) {
        // They haven't got too many login sessions running at present
        return;
    }

    if (allowedSessions == -1) {
        // We permit unlimited logins
        return;
    }

Using an aspect to wrap the session authentication strategy inside a synchronized block fixes the problem, but I wonder if there is a better approach (or some configuration I've missed).

I've created a Spring Boot sample application reproducing the bug, hosted in this GitHub repository. The main class is PeakTest along with force-sync property (please note that due to the non-deterministic nature of the test, results change between runs).

Thanks

@vpavic
Copy link
Contributor

vpavic commented Sep 6, 2018

Just to note that this was originally reported as spring-projects/spring-session#1181.

@jzheaux
Copy link
Contributor

jzheaux commented Sep 10, 2018

@daniloarcidiacono One thing to note is that the list of actions that are taken when a session is created is in the following order:

  1. Check the session registry for a concurrent session violation (via ConcurrentSessionControlAuthenticationStrategy)
  2. Mitigate session fixation (via ChangeSessionIdAuthenticationStrategy)
  3. Register the session with the session registry (via RegisterSessionAuthenticationStrategy)

The reason your test is failing is not really due to a race condition, but simply the fact that this order was designed to protect a human from logging in twice, not an automated test.

Note that the check is performed first, which means if two requests come in close together and before the cap is hit, they will both pass the check (Step 1) since their sessions have not yet been registered (Step 3).

So, one thing that you could try would be to add the check at the end of the list:

  1. Check
  2. Mitigate
  3. Register
  4. Check

I made this change myself in your sample app and now if two requests come in close together, then both are rejected (it fails with Expected: 1 Actual: 0 instead of Expected: 1 Actual: 2 or more). My change looks like this:

protected void configure(HttpSecurity http) {
    http
        .sessionManagement()
            .sessionAuthenticationStrategy(sessionAuthenticationStrategy())
            .maximumSessions(1)
                .sessionRegistry(sessionRegistry)
            // ... your other configs
}

SessionAuthenticationStrategy sessionAuthenticationStrategy() {
    SessionAuthenticationStrategy mitigate = new ChangeSessionIdAuthenticationStrategy();

    ConcurrentSessionControlAuthenticationStrategy check =
            new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry);
    check.setMaximumSessions(1);
    check.setExceptionIfMaximumExceeded(true);

    RegisterSessionAuthenticationStrategy register =
            new RegisterSessionAuthenticationStrategy(sessionRegistry);

    return new CompositeSessionAuthenticationStrategy(
            Arrays.asList(check, mitigate, register, check));
}

This isn't quite what you were asking for (your test will now fail occasionally because all requests were rejected), but it might be an effective workaround.

@lrozenblyum
Copy link
Contributor

@jzheaux the workaround might help however it may cause the deny of access in legal situations... Ideally we need a 100% bullet-proof solution that will allow and deny access atomically depending on the current session registry state

@lrozenblyum
Copy link
Contributor

The solution I've applied in my code is decorating the whole CompositeSessionAuthenticationStrategy with a synchronized decorator to ensure logical atomicity of check + register

@rwinch
Copy link
Member

rwinch commented Dec 21, 2018

@lrozenblyum please keep in mind that won't work when you are working with multiple application servers. The only way I see around this is to check every request which adds a performance cost.

@lrozenblyum
Copy link
Contributor

@rwinch about multiple application servers, do you mean a scenario when there is something like load balancing which selects target server to handle the request, so that the sessions live not in a single JVM/app. server?

@rwinch
Copy link
Member

rwinch commented Jan 7, 2019

Yes

@lrozenblyum
Copy link
Contributor

Wondering: is the issue still valid for non-cluster environments?
#3189 which was one of its reasons was fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: waiting-for-triage An issue we've not yet triaged
Projects
None yet
Development

No branches or pull requests

6 participants