Skip to content

WebTestClientRequestConverter mishandles cookies whose value contains = and crashes on cookie headers without = #1038

@config25

Description

@config25

Summary

WebTestClientRequestConverter.createRequestCookie(String) parses the Cookie header value with a naive split("=") and unconditionally accesses components[1]. This leads to two related problems:

  1. Data corruption — when a cookie value legitimately contains = (e.g. Base64-padded session IDs, JWTs in some encodings), only the substring before the first = is captured as the value, and the remainder is silently
    dropped.
  2. ArrayIndexOutOfBoundsException — when a cookie header value contains no = at all, components[1] throws.

Affected code

spring-restdocs-webtestclient/src/main/java/org/springframework/restdocs/webtestclient/WebTestClientRequestConverter.java, lines 107–110:

private RequestCookie createRequestCookie(String header) {
    String[] components = header.split("=");
    return new RequestCookie(components[0], components[1]);
}

There is no length check on components, and split("=") is called without a limit, so any = inside the value is also treated as a delimiter.

For comparison, MockMvcRequestConverter.extractCookies(...) reads cookies from the already-parsed MockHttpServletRequest#getCookies() and never touches the raw header string, so the equivalent path on the MockMvc side is
unaffected.

Scenario 1 — Cookie value containing = (data corruption)

= is a valid character inside a cookie value per RFC 6265 §4.1.1 (only ;, ,, whitespace, and a few control characters are excluded). It is also extremely
common in practice: Base64 padding (abc=, abcd==) is the obvious example, and many session/JWT cookies end up containing it.

This is reachable through the standard WebTestClient.cookie(name, value) API:

ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null))
    .configureClient()
    .baseUrl("http://localhost")
    .build()
    .get()
    .uri("/foo")
    .cookie("sessionId", "YWJjZGVm==")   // Base64-padded value
    .exchange()
    .expectBody()
    .returnResult();

OperationRequest request = this.converter.convert(result);

// Expected: value == "YWJjZGVm=="
// Actual:   value == "YWJjZGVm"  (everything from the first '=' onward is lost)
assertThat(request.getCookies()).extracting("value").containsExactly("YWJjZGVm==");

I verified this locally against main (commit 631b6c22); the assertion fails with:

Expecting actual:
  ["YWJjZGVm"]
to contain exactly (and in same order):
  ["YWJjZGVm=="]

The recorded OperationRequest no longer reflects the request that was actually sent, so any snippet generated from it will document an incorrect cookie value.

Scenario 2 — Cookie header without = (crash)

A Cookie header value without = is malformed, but it is reachable via WebTestClient's low-level header API:

ExchangeResult result = WebTestClient.bindToRouterFunction(RouterFunctions.route(GET("/foo"), (req) -> null))
    .configureClient()
    .baseUrl("http://localhost")
    .build()
    .get()
    .uri("/foo")
    .header(HttpHeaders.COOKIE, "malformed")
    .exchange()
    .expectBody()
    .returnResult();

this.converter.convert(result);

Verified locally on main (commit 631b6c22):

java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
    at org.springframework.restdocs.webtestclient.WebTestClientRequestConverter.createRequestCookie(WebTestClientRequestConverter.java:109)
    at org.springframework.restdocs.webtestclient.WebTestClientRequestConverter.extractCookies(WebTestClientRequestConverter.java:104)
    at org.springframework.restdocs.webtestclient.WebTestClientRequestConverter.convert(WebTestClientRequestConverter.java:62)

This is admittedly an unusual input, but since REST Docs runs as part of a test suite, an ArrayIndexOutOfBoundsException originating inside the converter can be confusing to track down — the stack trace doesn't make it
obvious that the cookie header is the cause. Scenario 1 above is the more important case; I'm mentioning this one mainly because both have the same root cause and would naturally be fixed together.

Possible fix

One straightforward option would be to split on only the first = and handle the no-= case explicitly, e.g.:

private RequestCookie createRequestCookie(String header) {
    int separator = header.indexOf('=');
    if (separator == -1) {
        return new RequestCookie(header, "");
    }
    return new RequestCookie(header.substring(0, separator), header.substring(separator + 1));
}

I'm happy to defer to whatever approach you prefer for the no-= case (empty string, skipping the cookie, or throwing a clearer error).

Environment

  • Spring REST Docs: main branch, commit 631b6c22
  • Reproduced via two new tests in WebTestClientRequestConverterTests, both of which fail against the current code at WebTestClientRequestConverter.java:109.

If this analysis looks right to you, I'd be glad to open a PR with a fix and regression tests for both scenarios.


Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions