Description

The out-of-the-box behavior is that unauthenticated calls trigger session creation but authenticated calls do NOT trigger session creation. This seems backwards.

An attacker may potentially consume a lot of server memory via sending malicious http requests to create an unbounded number of sessions, so there is potential for a DoS attack.

To Reproduce

This authenticates and returns 200 but does not create a session. Note in the logs "Failed to create a session, as response has been committed. Unable to store SecurityContext."

curl -kv --user admin:admin http://localhost:9000/login

Here, the first call creates an anonymous session and returns 401 (no credentials were passed) then passing the session id back on the second call associates that session with the username. It seems like out of the box authenticated session creation only works if we make a preflight request

curl -kv -b cookies.txt -c cookies.txt -X OPTIONS http://localhost:9000/login curl -kv -b cookies.txt -c cookies.txt --user admin:admin http://localhost:9000/login cat cookies.txt

Expected behavior

I expected that the default session creation policy be IF_REQUIRED instead of null (when it is IF_REQUIRED then we have the expected behavior of a session being created and saved upon successful authentication), and that unauthenticated calls would NOT have a session created.

Sample

Here is a link to a GitHub repository with a minimal, reproducible sample.

Comment From: jzheaux

Hi, @thinkbigthings. Thanks for the report.

IF_REQUIRED is the default, which you can see here: https://github.com/spring-projects/spring-security/blob/4e19b34094226c6c80125f027d6cd80d0e8c6226/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java#L524

I believe what's going on in your application is it's using an HttpSessionRequestCache to remember the authenticated request and replay it post-login. If I change the session management policy to IF_REQUIRED in your application as you described, HttpSessionRequestCache is still used and a session is still created.

If I change the setting to:

.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

or instead, configure the request cache like so:

.requestCache((cache) -> cache.requestCache(new NullRequstCache()))

then no session is created.

This is a known tradeoff with the request cache feature, though I don't think that fact is directly addressed in the documentation. I'll leave this ticket open to look into clarifying this point.

There may also be some merit in changing the default to NullRequestCache, or to HttpSessionRequestCache#setCreateSessionAllowed(false), though that will require a broader team discussion due to the impact it would have on numerous existing applications. Please see https://github.com/spring-projects/spring-security/issues/4242#issuecomment-417710155 for some further reading.

If you disagree or feel like I'm missing something, will you please simplify your sample to be the minimum code necessary to illustrate your point? This will help us collaborate faster.

Comment From: thinkbigthings

Thank you @jzheaux for the thoughtful response.

When I stepped into session.sessionCreationPolicy() with the debugger, I saw that the original value was null, but didn't realize it returned the expected default value later if it was still null, so that was my misunderstanding.

However, setting the SessionCreationPolicy to the default value of IF_REQUIRED should result in the same behavior as not setting it at all, yet we observe different behavior (creating vs not creating a session for an authenticated call) depending on whether the value is set to default or not set at all. That is definitely confusing and still seems like a bug. Maybe this is due to the interaction with .propertiesThatRequireImplicitAuthentication() ?

What do you think?

Comment From: jzheaux

I see, @thinkbigthings, thanks for the clarification.

In 6.0, a change was made to require authentication filters to interact with the session directly instead of relying on the global SessionManagementFilter. You can read more about this in the 5.8 migration guide.

Certain settings on the sessionManagement DSL still require that SessionManagementFilter be used and sessionCreationPolicy is one of those. So, when an application sets a SessionCreationPolicy, the DSL interprets this as opting into the old 5.x behavior (having a global session filter). It sounds to me like what you want is to accept the 6.0 session management defaults.

When you go with the 6.0 defaults, then a request like this:

http -a admin:admin :9000/login

doesn't create a session because BasicAuthenticationFilter is stateless, or IOW, the authentication filter that was used did not interact with the session.

When you override the defaults and configure the SessionManagementFIlter, then it behaves how things did in 5.x. In other words:

http -a admin:admin :9000/login

does create a session because the SessionManagementFilter decided to.

Comment From: thinkbigthings

@jzheaux Why do you say "It sounds to me like what you want is to accept the 6.0 session management defaults."?

What I'm hearing is that 6.0 defaults to stateless and creates no sessions. So if I am on 6.0 and still want to use sessions, I need to explicitly set the session creation policy which triggers the use of the SessionManagementFilter because that is the only way for the system to create sessions. Is that correct?

Maybe there is some confusion around saying "the default SessionCreationPolicy is IF_NEEDED" ... it would seem that the default of Spring Security is STATELESS but if the SessionManagementFilter is in play then the default policy is IF_NEEDED. Is that correct?

Comment From: jzheaux

if the SessionManagementFilter is in play then the default policy is IF_NEEDED. Is that correct?

If the SessionManagementFilter is in play, then IF_REQUIRED is the default, yes. This has implications for how SessionManagementFilter is configured. The setting is meaningless without SessionManagementFilter in the filter chain.

What I'm hearing is that 6.0 defaults to stateless and creates no sessions

Not quite. If the authentication mechanism used is stateless then no session is created. The reason the admin:admin request is creating no session when using Spring Security 6 defaults is that request is using HTTP Basic, which creates no session. If your request instead used Form Login, then a session would have been created since Form Login requires the session.

Also, there is still the request cache. By default, Spring Security will store unauthenticated requests in the session.

So if I am on 6.0 and still want to use sessions, I need to explicitly set the session creation policy which triggers the use of the SessionManagementFilter because that is the only way for the system to create sessions.

No. Spring Security 6 still uses the session (see above). What has changed is that it is now the responsibility of each individual authentication filter instead of a global session management filter. SessionManagementFilter is still present for backward compatibility reasons.

Using 6.0 idioms, if your app is using HTTP Basic for authentication and you want those results stored in the session, you'd now configure the BasicAuthenticationFilter (an authentication filter) instead of the (global) SessionManagementFilter. This is because each authentication filter's session management is now its own responsibility.

Comment From: jzheaux

For additional clarity, here are some example requests based on an OOTB Spring Security 6.0 Boot application:

request result reason
http -a user:$PASSWORD :8080 no session created HTTP Basic stored nothing
http --form POST :8080 username=user "password=$PASSWORD" session created Form Login stored the user
http :8080 session created request cache stored the unauthenticated request

Comment From: thinkbigthings

@jzheaux

if the SessionManagementFilter is in play then the default policy is IF_NEEDED. Is that correct?

If the SessionManagementFilter is in play, then IF_REQUIRED is the default, yes. This has implications for how SessionManagementFilter is configured. The setting is meaningless without SessionManagementFilter in the filter chain.

Ok, this makes sense, thank you.

So if I am on 6.0 and still want to use sessions, I need to explicitly set the session creation policy which triggers the use of the SessionManagementFilter because that is the only way for the system to create sessions.

...

Using 6.0 idioms, if your app is using HTTP Basic for authentication and you want those results stored in the session, you'd now configure the BasicAuthenticationFilter (an authentication filter) instead of the (global) SessionManagementFilter. This is because each authentication filter's session management is now its own responsibility.

When you say "you'd now configure BasicAuthenticationFilter" do you mean configure it to be present in the filter chain or configure it to manage sessions? I see that filter in the logs as being included in the list of filters configured by spring security already, and the BasicAuthenticationFilter javadocs don't mention anything about configuring it for session management.

It sounds like the behavior I originally posted is the expected behavior for Spring Security. If we have gone too far afield from the original question, we can close this and I can take my configuration questions back to StackOverflow. But I feel like we're pretty close and I want to use Spring Security 6 in the most future-proof way I can while the topic is fresh!

Comment From: jzheaux

When you say "you'd now configure BasicAuthenticationFilter" do you mean configure it to be present in the filter chain or configure it to manage sessions?

You'd configure it to store the security context in a session. For example, you can do:

@Bean 
SecurityFilterChain appEndpoints(HttpSecurity http) {
    http
        .authorizeHttpRequests(...)
        .httpBasic((basic) -> basic
            .withObjectPostProcessor(new ObjectPostProcessor<BasicAuthenticationFilter>() {
                public BasicAuthenticationFilter postProcess(BasicAuthenticationFilter filter) {
                    filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository());
                    return filter;
                }
            })
        // ... other configs
        ;

    return http.build();
}

There may be some value in making that simpler in the event that storing HTTP Basic in a session proves common. Hypothetically, Spring Security could be changed to:

@Bean 
SecurityFilterChain appEndpoints(HttpSecurity http) {
    http
        .authorizeHttpRequests(...)
        .httpBasic((basic) -> basic.securityContextRepository(new HttpSessionSecurityContextRepository()))
        // ... other configs
        ;

    return http.build();
}

And if you feel like this is something that would help you, please feel free to follow or contribute to https://github.com/spring-projects/spring-security/issues/12031.

Thanks very much for reaching out, @thinkbigthings. I've created https://github.com/spring-projects/spring-security/issues/12519 to take your points into consideration in the docs.