eclipse-ee4j / eclipse-ee4j/jersey
jersey-mp-rest-client 3.1.11 nondeterministically copies inbound provider headers into outbound requests
- Dominant language
- Java
- Stars
- 730
- Forks
- 382
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
Since jersey-mp-rest-client 3.1.11, headers returned by a registered InboundHeadersProvider can be copied automatically into an outbound request—even when no ClientHeadersFactory is registered.
The behavior is nondeterministic: different Rest Client instances in the same JVM can either propagate or omit the headers.
This may expose inbound Authorization, Cookie, forwarding, or gateway headers to a different destination.
## Expected behavior
Inbound headers should only be available as the first argument to an explicitly configured ClientHeadersFactory. Without such a factory, they should not be propagated.
Headers added with `RestClientBuilder.header(...)` should remain separate from inbound headers.
## Actual behavior
Jersey 3.1.11 stores the internal DefaultInboundHeaderProvider and application-provided InboundHeadersProvider instances in the same HashSet.
MethodModel.resolveCustomHeaders() accumulates provider results into one map. When DefaultInboundHeaderProvider is encountered, its update() method copies that entire accumulated map into outbound headers.
Therefore:
- Application provider first: inbound headers are copied outbound.
- Default provider first: only builder-provided headers are copied.
## Reproducer
```java
import java.net.URI;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.RestClientBuilder;
import org.glassfish.jersey.microprofile.restclient.InboundHeadersProvider;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class InboundHeadersProviderTest {
private static final String HEADER = "X-Inbound-Only";
private static final String RESULT = "X-Test-Leaked";
@Test
void inboundHeadersMustNotBeCopiedAutomatically() throws Exception {
Set observed = new HashSet<>();
for (int i = 0; i < 100; i++) {
TestClient client = RestClientBuilder.newBuilder()
.baseUri(URI.create("http://example.invalid"))
.register(new TestInboundHeadersProvider())
.register(new InspectingFilter())
.build(TestClient.class);
try {
try (Response response = client.invoke()) {
observed.add(Boolean.parseBoolean(
response.getHeaderString(RESULT)));
}
} finally {
((AutoCloseable) client).close();
}
}
assertEquals(Set.of(false), observed,
() -> "observed=" + observed);
}
@Path("/")
interface TestClient {
@GET
Response invoke();
}
static final class TestInboundHeadersProvider
implements InboundHeadersProvider {
@Override
public Map> inboundHeaders() {
return Map.of(HEADER, List.of("must-not-be-sent"));
}
@Override
public int hashCode() {
return 0;
}
}
static final class InspectingFilter implements ClientRequestFilter {
@Override
public void filter(ClientRequestContext context) {
boolean leaked = context.getHeaders().containsKey(HEADER);
context.abortWith(Response.ok()
.header(RESULT, leaked)
.build());
}
}
}
```
Contributor guide
Research direction
Start at MethodModel.resolveCustomHeaders() and DefaultInboundHeaderProvider.update(), using the supplied InboundHeadersProviderTest reproducer to trace how provider results reach outbound headers. Add regression coverage for multiple provider-registration orders and verify that inbound-only headers are never copied without a ClientHeadersFactory, while builder-provided headers remain separate.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- api, security
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100