Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@
**Vulnerability:** A CRLF injection vulnerability was present in `DownloadGetRequestHandler.java` where the unsanitized `info.getFileMimeType()` was passed directly to `servletResponse.setHeader` to form the `Content-Type` header. An attacker could craft a malicious mime-type containing carriage returns and line feeds to inject arbitrary HTTP headers in the response.
**Learning:** Similar to `Content-Disposition`, legacy HTTP header fields that accept unencoded strings, like `Content-Type`, are a common vector for HTTP response splitting/CRLF injection when user input is used to construct them.
**Prevention:** Always sanitize untrusted input when constructing HTTP headers, especially when not using a built-in framework that automatically sanitizes them. For `Content-Type`, strip out `\r` and `\n` from the unencoded parameter.
## 2026-07-07 - Global CRLF injection in HTTP headers
**Vulnerability:** A CRLF injection vulnerability was identified where unvalidated inputs could be reflected directly into HTTP response headers, leading to HTTP response splitting.
**Learning:** Instead of sanitizing individual headers independently, the overarching wrapper that interacts with the HTTP response output should inherently validate and sanitize the input to prevent injection across all headers.
**Prevention:** `TusServletResponse.java` was modified to include a `sanitizeHeaderValue` method, replacing any instances of `\r` and `\n` characters before interacting with the core `HttpServletResponse`, ensuring consistent CRLF prevention application-wide.
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,11 @@ public void process(
HttpHeader.CONTENT_DISPOSITION,
String.format(
CONTENT_DISPOSITION_FORMAT,
info.getFileName().replaceAll("[\r\n\"]", ""),
info.getFileName().replace("\"", ""),
URLEncoder.encode(info.getFileName(), StandardCharsets.UTF_8.toString())
.replace("+", "%20")));

servletResponse.setHeader(
HttpHeader.CONTENT_TYPE, info.getFileMimeType().replaceAll("[\r\n]", ""));
servletResponse.setHeader(HttpHeader.CONTENT_TYPE, info.getFileMimeType());

if (info.hasMetadata()) {
servletResponse.setHeader(HttpHeader.UPLOAD_METADATA, info.getEncodedMetadata());
Expand Down
17 changes: 13 additions & 4 deletions src/main/java/me/desair/tus/server/util/TusServletResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,16 @@ public void addDateHeader(String name, long date) {

@Override
public void setHeader(String name, String value) {
super.setHeader(name, value);
overwriteHeader(name, value);
String sanitizedValue = sanitizeHeaderValue(value);
super.setHeader(name, sanitizedValue);
overwriteHeader(name, sanitizedValue);
}

@Override
public void addHeader(String name, String value) {
super.addHeader(name, value);
recordHeader(name, value);
String sanitizedValue = sanitizeHeaderValue(value);
super.addHeader(name, sanitizedValue);
recordHeader(name, sanitizedValue);
}

@Override
Expand Down Expand Up @@ -84,4 +86,11 @@ private void overwriteHeader(String name, String value) {
values.add(value);
headers.put(name, values);
}

private String sanitizeHeaderValue(String value) {
if (value == null) {
return null;
}
return value.replaceAll("[\r\n]", "");
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package me.desair.tus.server.util;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletResponse;

public class TusServletResponseCRLFTest {
private TusServletResponse tusServletResponse;
private MockHttpServletResponse servletResponse;

@Before
public void setUp() {
servletResponse = new MockHttpServletResponse();
tusServletResponse = new TusServletResponse(servletResponse);
}

@Test
public void testCRLFInjectionInSetHeader() {
tusServletResponse.setHeader("TEST", "value\r\nInjected-Header: true");
assertThat(tusServletResponse.getHeader("TEST"), is("valueInjected-Header: true"));
assertThat(servletResponse.getHeader("TEST"), is("valueInjected-Header: true"));
}

@Test
public void testCRLFInjectionInAddHeader() {
tusServletResponse.addHeader("TEST", "value\r\nInjected-Header: true");
assertThat(tusServletResponse.getHeader("TEST"), is("valueInjected-Header: true"));
assertThat(servletResponse.getHeader("TEST"), is("valueInjected-Header: true"));
}
}
Loading