This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new e735582b2d refuse CR and LF in HttpFetcher request headers (#3438)
e735582b2d is described below
commit e735582b2dbf996b48dfee976bf0314cb94f500e
Author: Naveed <[email protected]>
AuthorDate: Fri Oct 30 21:05:44 2026 +0530
refuse CR and LF in HttpFetcher request headers (#3438)
HttpFetcher put per-request and metadata-supplied headers on the
outgoing request as given. httpcore folds CR and LF in a header value
into spaces but writes the name verbatim, so a header name carrying
CRLF injected raw request lines. Check every header name against the
RFC 7230 token grammar and every value for control characters before
it is added to the request.
Generated-by: Claude Code (Claude Fable 5.1)
---
.../tika/pipes/fetcher/http/HttpFetcher.java | 46 +++++++++++++++++-----
.../tika/pipes/fetcher/http/HttpFetcherTest.java | 36 +++++++++++++++++
2 files changed, 73 insertions(+), 9 deletions(-)
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/HttpFetcher.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/HttpFetcher.java
index 68e2535d2b..efaf6e0106 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/HttpFetcher.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/main/java/org/apache/tika/pipes/fetcher/http/HttpFetcher.java
@@ -38,6 +38,7 @@ import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Pattern;
import com.nimbusds.jose.JOSEException;
import org.apache.commons.io.IOUtils;
@@ -94,6 +95,9 @@ public class HttpFetcher extends AbstractTikaExtension
implements Fetcher, Range
}
+ // RFC 7230 token
+ private static final Pattern HEADER_NAME =
Pattern.compile("[!#$%&'*+.^_`|~0-9A-Za-z-]+");
+
private HttpFetcherConfig httpFetcherConfig = new HttpFetcherConfig();
private HttpClientFactory httpClientFactory = new HttpClientFactory();
@@ -175,7 +179,7 @@ public class HttpFetcher extends AbstractTikaExtension
implements Fetcher, Range
.build();
}
- private void setHttpRequestHeaders(Metadata metadata, HttpGet get) {
+ private void setHttpRequestHeaders(Metadata metadata, HttpGet get) throws
TikaException {
if (!StringUtils.isBlank(userAgent)) {
get.setHeader(USER_AGENT, userAgent);
}
@@ -194,16 +198,34 @@ public class HttpFetcher extends AbstractTikaExtension
implements Fetcher, Range
}
}
- private static void parseHeaderAndPutOnRequest(HttpGet get, String
httpRequestHeader) {
+ private static void parseHeaderAndPutOnRequest(HttpGet get, String
httpRequestHeader)
+ throws TikaException {
String[] parts = httpRequestHeader
.trim().split(":", 2);
if (parts.length >= 2) {
String key = parts[0].trim();
String value = parts[1].trim();
+ checkHeader(key, value);
get.setHeader(key, value);
}
}
+ /**
+ * httpcore folds CR and LF in a value into spaces but writes the name as
given, so a
+ * name from per-request config that carries CRLF would inject request
lines.
+ */
+ private static void checkHeader(String name, String value) throws
TikaException {
+ if (!HEADER_NAME.matcher(name).matches()) {
+ throw new TikaException("Invalid HTTP header name: " + name);
+ }
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if ((c < 0x20 && c != '\t') || c == 0x7f) {
+ throw new TikaException("Invalid control character in HTTP
header " + name);
+ }
+ }
+ }
+
private HttpFetcherRequestConfig
getAdditionalHttpFetcherConfig(ParseContext parseContext)
throws TikaConfigException {
if (parseContext.hasJsonConfig("http-fetcher")) {
@@ -234,14 +256,18 @@ public class HttpFetcher extends AbstractTikaExtension
implements Fetcher, Range
httpGet.setHeader(USER_AGENT, httpFetcherConfig.getUserAgent());
}
if (additionalFetcherConfig != null &&
additionalFetcherConfig.getHttpRequestHeaders() != null) {
- additionalFetcherConfig.getHttpRequestHeaders()
- .getHeaders()
- .forEach(httpGet::addHeader);
+ for (Map.Entry<String, String> e :
additionalFetcherConfig.getHttpRequestHeaders()
+ .getHeaders().entries()) {
+ checkHeader(e.getKey(), e.getValue());
+ httpGet.addHeader(e.getKey(), e.getValue());
+ }
}
if (httpFetcherConfig.getHttpRequestHeaders() != null) {
- httpFetcherConfig.getHttpRequestHeaders()
- .getHeaders()
- .forEach(httpGet::addHeader);
+ for (Map.Entry<String, String> e :
httpFetcherConfig.getHttpRequestHeaders()
+ .getHeaders().entries()) {
+ checkHeader(e.getKey(), e.getValue());
+ httpGet.addHeader(e.getKey(), e.getValue());
+ }
}
if (jwtGenerator != null) {
try {
@@ -252,7 +278,8 @@ public class HttpFetcher extends AbstractTikaExtension
implements Fetcher, Range
}
}
- private static void placeHeaderOnGetRequest(HttpGet get, String
httpRequestHeader) {
+ private static void placeHeaderOnGetRequest(HttpGet get, String
httpRequestHeader)
+ throws TikaException {
int idxOfEquals = httpRequestHeader.indexOf(':');
if (idxOfEquals == -1) {
return;
@@ -263,6 +290,7 @@ public class HttpFetcher extends AbstractTikaExtension
implements Fetcher, Range
String headerValue = httpRequestHeader
.substring(idxOfEquals + 1)
.trim();
+ checkHeader(headerKey, headerValue);
get.setHeader(headerKey, headerValue);
}
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/test/java/org/apache/tika/pipes/fetcher/http/HttpFetcherTest.java
b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/test/java/org/apache/tika/pipes/fetcher/http/HttpFetcherTest.java
index b6bda58bc7..0a6501dd55 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/test/java/org/apache/tika/pipes/fetcher/http/HttpFetcherTest.java
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/test/java/org/apache/tika/pipes/fetcher/http/HttpFetcherTest.java
@@ -35,6 +35,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
@@ -261,6 +262,41 @@ class HttpFetcherTest extends TikaTest {
}
+ /**
+ * httpcore writes a header name as given (only a value has CR and LF
folded), so a
+ * per-request header name carrying CRLF would inject request lines.
+ */
+ @Test
+ public void testHeadersWithCrLfAreRefused() throws Exception {
+ HttpResponse response = buildMockResponse(HttpStatus.SC_OK,
IOUtils.toInputStream(CONTENT, Charset.defaultCharset()));
+ HttpClient httpClient = Mockito.mock(HttpClient.class);
+
Mockito.when(httpClient.execute(ArgumentMatchers.any(HttpUriRequest.class),
ArgumentMatchers.any(HttpContext.class)))
+ .thenReturn(response);
+ httpFetcher.setHttpClient(httpClient);
+
+ ParseContext nameContext = new ParseContext();
+ nameContext.setJsonConfig("http-fetcher",
OBJECT_MAPPER.writeValueAsString(
+ Map.of("httpRequestHeaders", Map.of("map", Map.of(
+ "X-Test\r\nInjected: yes\r\n\r\nPOST /evil
HTTP/1.1\r\nHost: h",
+ List.of("v"))))));
+ assertThrows(TikaException.class,
+ () -> httpFetcher.fetch("http://localhost", new Metadata(),
nameContext));
+
+ ParseContext valueContext = new ParseContext();
+ valueContext.setJsonConfig("http-fetcher",
OBJECT_MAPPER.writeValueAsString(
+ Map.of("httpRequestHeaders", Map.of("map", Map.of(
+ "X-Test", List.of("v\r\nInjected: yes"))))));
+ assertThrows(TikaException.class,
+ () -> httpFetcher.fetch("http://localhost", new Metadata(),
valueContext));
+
+ Metadata metadata = new Metadata();
+ metadata.set(Property.externalText("httpRequestHeaders"),
"X-Test\r\nInjected: yes: v");
+ assertThrows(TikaException.class,
+ () -> httpFetcher.fetch("http://localhost", metadata, new
ParseContext()));
+
+ Mockito.verifyNoInteractions(httpClient);
+ }
+
@Test
@Disabled("requires network connectivity")
public void testRedirect() throws Exception {