user2800089
user2800089

Reputation: 2381

Exception evaluating SpringEL expression: "_csrf.token" with Get request

I have a spring boot 2.5.1 application having spring security and thymleaf.

In order to pass csrf token, i followed the spring documentation link https://docs.spring.io/spring-security/site/docs/5.0.x/reference/html/csrf.html#csrf-include-csrf-token-ajax

index.html contain header as mentioned in above link

<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>

and below is the configuration to include token with all ajax requests

$(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);
    });
});

However, after adding above changes, existing junit test case started failing

@AutoConfigureMockMvc
@WebMvcTest(MyUserInterfaceController.class)
class MyUserInterfaceControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void shouldGetIndexPageWithNoUnknownUserName() throws Exception {
        mockMvc.perform(get("/"))
                .andExpect(status().isOk())
                .andExpect(view().name("index"))
                .andExpect(model().attribute("username", "UNKNOWN_USER"));
    }
}

Above testcase was passing until I made csrf related changes. However, below test case is passing with or without csrf changes.

    @SneakyThrows
    @Test
    @WithMockTestUser
    void shouldSaveSettlementRemark() {
        //var userDtos= ...
        mockMvc.perform(post("/users")
                .contentType(APPLICATION_JSON)
                .content(userDtos))
                .andDo(print())
                .andExpect(status().isOk());
    }

As per spring documentation if spring security jar is in classpath , csrf is enabled by default & hence post requests expects csrf token otherwise request is forbidden. Testcases written using MockMvc are not adhering to this behaviour.

Why GET request is failing with below stacktrace while POST request is passing ? How to fix this?

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "_csrf.token" (template: "index" - line 6, col 24)

    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:626)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
    at org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:72)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:733)
    at org.springframework.mock.web.MockFilterChain$ServletFilterProxy.doFilter(MockFilterChain.java:167)
    at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)

On running application , UI is being rendered but at backend , even when no user requests are initiated, thymeleaf is trying to render index.html which is throwing below error.

":"org.thymeleaf.TemplateEngine","message":"[THYMELEAF][http-nio-8080-exec-1] Exception processing template \"index\": An error happened during template parsing (template: \"templates/index.html\")","stack_trace":"<#24d6b41b> o.s.e.s.SpelEvaluationException: EL1007E: Property or field 'token' cannot be found on null\n\tat o.s.e.s.a.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:213)

Both the above error I can fix by adding a null check as below:

Is this right approach or am i compromising with security of application?

<meta name="_csrf" th:content="${_csrf != null} ? ${_csrf.token} : '' "/>
<meta name="_csrf_header" th:content="${_csrf != null} ? ${_csrf.headerName} : '' "/>

WebSecurity configuration as below:

@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomProperties customProperties;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        if (customProperties.isEnabled()) {
            log.info("authentication enabled.");
            http.authorizeRequests()
                    .anyRequest()
                    .authenticated()
                    .and()
                    .oauth2Login()
                    .redirectionEndpoint()
                    .baseUri(customProperties.getAuthorizationResponseUri())
                    .and()
                    .userInfoEndpoint(userInfo -> userInfo.userService(new CustomOAuth2UserService()::loadUser));
        } else {
            log.info("authentication disabled.");
            http.authorizeRequests()
                    .anyRequest()
                    .permitAll();
        }
    }
}

Upvotes: 0

Views: 3650

Answers (1)

Lee Greiner
Lee Greiner

Reputation: 1082

By default, when csrf is enabled, Spring Security generates the csrf token when a session is started. If you have no session there will be no csrf token to be had. Your second test uses an authenticated user, will have a session, and succeeds.

You can use the following meta tags to handle this situation and when csrf is disabled:

<meta sec:authorize="isFullyAuthenticated()" th:if="${_csrf}" name="_csrf" th:content="${_csrf.token}"/>
<meta sec:authorize="isFullyAuthenticated()" th:if="${_csrf}" name="_csrf_header" th:content="${_csrf.headerName}"/>
<meta sec:authorize="isFullyAuthenticated()" th:if="${_csrf}" name="_csrf_parameter" th:content="${_csrf.parameterName}"/>

An alternative is to change the csrf token strategy in your configuration:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
    // other security information ...
   .and().csrf().csrfTokenRepository(new HttpSessionCsrfTokenRepository())
}

And to use the following meta tags:

<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
<meta name="_csrf_parameter" th:content="${_csrf.parameterName}"/>

I'm not sure why your POST is working though. With csrf enabled I would think you would have to use the .with(csrf()) as shown below.

@SneakyThrows
@Test
@WithMockTestUser
void shouldSaveSettlementRemark() {
    //var userDtos= ...
    mockMvc.perform(post("/users")
            .with(csrf())
            .contentType(APPLICATION_JSON)
            .content(userDtos))
            .andDo(print())
            .andExpect(status().isOk());
}

Upvotes: 1

Related Questions