diff --git a/core/src/main/java/feign/template/HeaderTemplate.java b/core/src/main/java/feign/template/HeaderTemplate.java index cddcb9e9d..e23a5d6ee 100644 --- a/core/src/main/java/feign/template/HeaderTemplate.java +++ b/core/src/main/java/feign/template/HeaderTemplate.java @@ -141,7 +141,10 @@ private HeaderTemplate(String name, Iterable values, Charset charset, bo public Collection getValues() { return Collections.unmodifiableList( - this.values.stream().map(Template::toString).collect(Collectors.toList())); + this.values.stream() + .map(Template::toString) + .map(HeaderTemplate::stripCrlf) + .collect(Collectors.toList())); } public List getVariables() { @@ -167,7 +170,7 @@ public String expand(Map variables) { continue; } - expanded.add(result); + expanded.add(stripCrlf(result)); } } @@ -178,4 +181,16 @@ public String expand(Map variables) { return result.toString(); } + + /** + * Removes carriage return and line feed characters from a resolved header value. Values are + * written to the header verbatim, so a CR or LF in a value would terminate the header line and + * inject additional headers. + * + * @param value to sanitise. + * @return the value without CR or LF characters. + */ + private static String stripCrlf(String value) { + return value.replace("\r", "").replace("\n", ""); + } } diff --git a/core/src/test/java/feign/RequestTemplateTest.java b/core/src/test/java/feign/RequestTemplateTest.java index 622e9cf94..b2afd1cea 100644 --- a/core/src/test/java/feign/RequestTemplateTest.java +++ b/core/src/test/java/feign/RequestTemplateTest.java @@ -197,6 +197,17 @@ void resolveTemplateWithHeaderSubstitutionsNotAtStart() { .hasHeaders(entry("Authorization", Collections.singletonList("Bearer 1234"))); } + @Test + void resolveTemplateStripsCrlfFromHeaderSubstitution() { + RequestTemplate template = + new RequestTemplate().method(HttpMethod.GET).header("X-Custom", "{value}"); + + template = template.resolve(mapOf("value", "legit\r\nX-Injected: evil")); + + assertThat(template) + .hasHeaders(entry("X-Custom", Collections.singletonList("legitX-Injected: evil"))); + } + @Test void resolveTemplateWithHeaderWithEscapedCurlyBrace() { RequestTemplate template = diff --git a/core/src/test/java/feign/template/HeaderTemplateTest.java b/core/src/test/java/feign/template/HeaderTemplateTest.java index 40a33245b..0ff70c9e8 100644 --- a/core/src/test/java/feign/template/HeaderTemplateTest.java +++ b/core/src/test/java/feign/template/HeaderTemplateTest.java @@ -122,6 +122,24 @@ void it_should_support_http_date() { .isEqualTo("Wed, 4 Jul 2001 12:08:56 -0700"); } + @Test + void it_should_strip_crlf_from_expanded_values() { + HeaderTemplate headerTemplate = + HeaderTemplate.create("X-Custom", Collections.singletonList("{value}")); + assertThat( + headerTemplate.expand(Collections.singletonMap("value", "legit\r\nX-Injected: evil"))) + .isEqualTo("legitX-Injected: evil"); + } + + @Test + void it_should_strip_crlf_from_literal_values() { + HeaderTemplate headerTemplate = + HeaderTemplate.create("X-Custom", Collections.singletonList("legit\r\nX-Injected: evil")); + assertThat(new ArrayList<>(headerTemplate.getValues())) + .containsExactly("legitX-Injected: evil"); + assertThat(headerTemplate.expand(Collections.emptyMap())).isEqualTo("legitX-Injected: evil"); + } + @Test void it_should_support_json_literal_values() { HeaderTemplate headerTemplate =