This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch TIKA-4809-stage-9
in repository https://gitbox.apache.org/repos/asf/tika.git

commit 0e2a05244412695731f6f3410b8cffe39f242c9f
Merge: 279315924c f1f7532303
Author: tallison <[email protected]>
AuthorDate: Tue Aug 11 12:41:08 2026 -0400

    Merge remote-tracking branch 'origin/main' into TIKA-4809-stage-9

 docs/modules/ROOT/pages/pipes/cpu-sizing.adoc      |  29 +++++
 .../ROOT/pages/using-tika/server/index.adoc        |   4 +-
 .../tika/pipes/core/PerClientServerManager.java    |  40 ++++++-
 .../org/apache/tika/pipes/core/PipesConfig.java    |  20 +++-
 .../apache/tika/pipes/core/server/PipesServer.java |  19 ++++
 .../tika/server/core/MaxRequestSizeFilter.java     | 112 +++++++++++++++++++
 .../apache/tika/server/core/TikaServerConfig.java  |  12 ++
 .../apache/tika/server/core/TikaServerProcess.java |   3 +-
 .../server/core/resource/PipesParsingHelper.java   |  31 +++++-
 .../tika/server/core/resource/TikaResource.java    |  96 +++++++++-------
 .../tika/server/core/MaxRequestSizeFilterTest.java | 124 +++++++++++++++++++++
 11 files changed, 439 insertions(+), 51 deletions(-)

diff --cc docs/modules/ROOT/pages/using-tika/server/index.adoc
index 639a3971a9,0ab7393434..926488ea94
--- a/docs/modules/ROOT/pages/using-tika/server/index.adoc
+++ b/docs/modules/ROOT/pages/using-tika/server/index.adoc
@@@ -330,7 -304,15 +330,9 @@@ Server behavior beyond host/port is con
  |`""` (off)
  |`*` to allow any origin, or an explicit origin string. Empty disables CORS.
  
- |`logLevel`
 -|`returnStackTrace`
 -|`false`
 -|Include parser stack traces in error responses. Useful in dev, dangerous in 
production (leaks internals).
 -
+ |`maxRequestSizeBytes`
+ |`-1` (no limit)
 -|Maximum request body in bytes; larger requests are rejected with `413`. 
Enforced for chunked uploads too, not just those declaring a `Content-Length`. 
Uploads are spooled to disk, so leaving this unset lets a caller fill the temp 
directory.
 -
 -|`logLevel`
++|Maximum request body in bytes; larger requests are rejected with `413`. 
Enforced for chunked uploads too, not just those declaring a `Content-Length`. 
Uploads are spooled to disk, so leaving this unset lets a caller fill the temp 
directory.|`logLevel`
  |_inherited_
  |`debug` or `info` to override the runtime log level.
  
diff --cc 
tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerConfig.java
index 57fe4cef23,f56f5e7c3c..f7ef888049
--- 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerConfig.java
+++ 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerConfig.java
@@@ -68,6 -68,8 +68,7 @@@ private long forkedProcessShutdownMilli
      private boolean allowPipes = false;
      private boolean allowPerRequestConfig = false;
      private String cors = "";
 -    private boolean returnStackTrace = false;
+     private long maxRequestSizeBytes = -1;
      private String id = UUID
              .randomUUID()
              .toString();
@@@ -239,7 -241,26 +240,18 @@@
          this.configPath = Paths.get(path);
      }
  
+     /**
+      * Maximum request body in bytes. Negative (the default) means no limit; 
tika-server
+      * spools uploads to disk, so an unbounded value lets a caller fill the 
temp directory.
+      */
+     public long getMaxRequestSizeBytes() {
+         return maxRequestSizeBytes;
+     }
+ 
+     public void setMaxRequestSizeBytes(long maxRequestSizeBytes) {
+         this.maxRequestSizeBytes = maxRequestSizeBytes;
+     }
  
 -    public boolean isReturnStackTrace() {
 -        return returnStackTrace;
 -    }
 -
 -    public void setReturnStackTrace(boolean returnStackTrace) {
 -        this.returnStackTrace = returnStackTrace;
 -    }
 -
      public TlsConfig getTlsConfig() {
          return tlsConfig;
      }
diff --cc 
tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/PipesParsingHelper.java
index 0b0a14bc16,75fb2e8989..c0c463768d
--- 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/PipesParsingHelper.java
+++ 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/PipesParsingHelper.java
@@@ -187,33 -190,18 +187,43 @@@ public class PipesParsingHelper 
          }
      }
  
+     /** Longest suffix carried over from a client filename; keeps well clear 
of NAME_MAX. */
+     private static final int MAX_SUFFIX_LENGTH = 20;
+ 
 +    /**
 +     * Removes the server's spool filename from the returned metadata.
 +     * <p>
 +     * The document is fetched from a temp file, so the fetcher records that 
path as
 +     * {@code tk:source-path} and, when the caller supplied no filename, it 
also becomes
 +     * {@code tk:resource-name} -- the field downstream consumers key 
document identity on.
 +     * Neither describes the caller's document: they name a file that has 
already been
 +     * deleted, and they expose the server's spooling scheme.
 +     */
 +    private static void stripSpoolIdentity(List<Metadata> metadataList, 
String spoolName,
 +                                           String callerSuppliedName) {
 +        if (metadataList == null) {
 +            return;
 +        }
 +        for (Metadata m : metadataList) {
 +            if (spoolName.equals(m.get(TikaCoreProperties.SOURCE_PATH))) {
 +                m.remove(TikaCoreProperties.SOURCE_PATH.getName());
 +            }
 +            if (callerSuppliedName == null
 +                    && 
spoolName.equals(m.get(TikaCoreProperties.RESOURCE_NAME_KEY))) {
 +                m.remove(TikaCoreProperties.RESOURCE_NAME_KEY.getName());
 +            }
 +        }
 +    }
 +
      /**
-      * Extracts file suffix from metadata (resource name or content-type).
+      * Extracts a file suffix from the resource name for the spool file.
+      * <p>
+      * The resource name is client-supplied ({@code Content-Disposition} / 
{@code File-Name}),
+      * so the suffix is sanitized here rather than left for {@code 
Files.createTempFile} to
+      * reject: a suffix containing a path separator makes it throw {@code 
IllegalArgumentException}
+      * — not a traversal, since the JDK refuses it, but an uncaught 500 
driven by a request
+      * header. An over-long suffix likewise fails at the filesystem. The 
suffix is a parser
+      * hint, so anything unusable is simply dropped in favour of {@code .tmp}.
       */
      private String getSuffix(Metadata metadata) {
          String resourceName = 
metadata.get(TikaCoreProperties.RESOURCE_NAME_KEY);
diff --cc 
tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
index a6045dd299,1115b1a5e7..00c492e305
--- 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
+++ 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/resource/TikaResource.java
@@@ -463,12 -460,13 +466,13 @@@ public class TikaResource 
      @Path("text")
      public Response getText(final InputStream is, @Context HttpHeaders 
httpHeaders)
              throws IOException {
-         TikaInputStream tis = TikaInputStream.get(is);
-         tis.getPath(); // Spool to temp file for pipes-based parsing
-         ParseContext context = createParseContext();
-         // "body", not "text": 3.x served this from BodyContentHandler, so 
TEXT (whole XHTML
-         // document, title included as characters) was a 4.x regression.
-         return produceRawOutput(tis, Metadata.newInstance(context), 
httpHeaders.getRequestHeaders(), "body");
+         // try-with-resources: the spooled temp file must be deleted even if
+         // context setup or metadata filling throws before the parse begins.
+         try (TikaInputStream tis = TikaInputStream.get(is)) {
+             tis.getPath(); // Spool to temp file for pipes-based parsing
+             ParseContext context = createParseContext();
 -            return produceRawOutput(tis, Metadata.newInstance(context), 
httpHeaders.getRequestHeaders(), "text");
++            return produceRawOutput(tis, Metadata.newInstance(context), 
httpHeaders.getRequestHeaders(), "body");
+         }
      }
  
      /**
@@@ -525,11 -532,13 +538,13 @@@
      @Path("json")
      public Metadata getJsonDefault(final InputStream is, @Context HttpHeaders 
httpHeaders)
              throws IOException {
-         TikaInputStream tis = TikaInputStream.get(is);
-         tis.getPath(); // Spool to temp file for pipes-based parsing
-         ParseContext context = createParseContext();
-         // null, not "text": no handler was named, so this takes 
DEFAULT_HANDLER_TYPE.
-         return produceJson(tis, Metadata.newInstance(context), 
httpHeaders.getRequestHeaders(), null);
+         // try-with-resources: the spooled temp file must be deleted even if
+         // context setup or metadata filling throws before the parse begins.
+         try (TikaInputStream tis = TikaInputStream.get(is)) {
+             tis.getPath(); // Spool to temp file for pipes-based parsing
+             ParseContext context = createParseContext();
 -            return produceJson(tis, Metadata.newInstance(context), 
httpHeaders.getRequestHeaders(), "text");
++            return produceJson(tis, Metadata.newInstance(context), 
httpHeaders.getRequestHeaders(), null);
+         }
      }
  
      /**
diff --cc 
tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/MaxRequestSizeFilterTest.java
index 0000000000,a6be0374ef..c0388a951c
mode 000000,100644..100644
--- 
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/MaxRequestSizeFilterTest.java
+++ 
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/MaxRequestSizeFilterTest.java
@@@ -1,0 -1,124 +1,124 @@@
+ /*
+  * Licensed to the Apache Software Foundation (ASF) under one or more
+  * contributor license agreements.  See the NOTICE file distributed with
+  * this work for additional information regarding copyright ownership.
+  * The ASF licenses this file to You under the Apache License, Version 2.0
+  * (the "License"); you may not use this file except in compliance with
+  * the License.  You may obtain a copy of the License at
+  *
+  *     http://www.apache.org/licenses/LICENSE-2.0
+  *
+  * Unless required by applicable law or agreed to in writing, software
+  * distributed under the License is distributed on an "AS IS" BASIS,
+  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  * See the License for the specific language governing permissions and
+  * limitations under the License.
+  */
+ package org.apache.tika.server.core;
+ 
+ import static java.nio.charset.StandardCharsets.UTF_8;
+ import static org.junit.jupiter.api.Assertions.assertEquals;
+ import static org.junit.jupiter.api.Assertions.assertNotEquals;
+ 
+ import java.io.ByteArrayInputStream;
+ import java.util.ArrayList;
+ import java.util.List;
+ 
+ import jakarta.ws.rs.core.Response;
+ import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
+ import org.apache.cxf.jaxrs.client.WebClient;
+ import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
+ import org.junit.jupiter.api.Test;
+ 
+ import org.apache.tika.server.core.resource.TikaResource;
+ import org.apache.tika.server.core.writer.JSONMessageBodyWriter;
+ 
+ public class MaxRequestSizeFilterTest extends CXFTestBase {
+ 
+     private static final String TIKA_PATH = "/tika";
+     private static final long MAX_BYTES = 500;
+ 
+     @Override
+     protected void setUpResources(JAXRSServerFactoryBean sf) {
+         sf.setResourceClasses(TikaResource.class);
+         sf.setResourceProvider(TikaResource.class, new 
SingletonResourceProvider(tikaResource));
+     }
+ 
+     @Override
+     protected void setUpProviders(JAXRSServerFactoryBean sf) {
+         List<Object> providers = new ArrayList<>();
 -        providers.add(new TikaServerParseExceptionMapper(false));
++        providers.add(new TikaServerParseExceptionMapper());
+         providers.add(new JSONMessageBodyWriter());
+         providers.add(new MaxRequestSizeFilter(MAX_BYTES));
+         sf.setProviders(providers);
+     }
+ 
+     @Test
+     public void testOverLimitRejected() throws Exception {
+         Response response = WebClient
+                 .create(endPoint + TIKA_PATH + "/text")
+                 .put(new ByteArrayInputStream(body((int) MAX_BYTES * 4)));
+ 
+         assertEquals(413, response.getStatus());
+     }
+ 
+     @Test
+     public void testUnderLimitAccepted() throws Exception {
+         Response response = WebClient
+                 .create(endPoint + TIKA_PATH + "/text")
+                 .put(new ByteArrayInputStream(body(50)));
+ 
+         assertNotEquals(413, response.getStatus(),
+                 "a body well under the limit must not be rejected");
+     }
+ 
+     /**
+      * A filename whose extension contains a path separator previously reached
+      * Files.createTempFile and threw IllegalArgumentException, surfacing as 
a 500
+      * driven entirely by a request header.
+      */
+     @Test
+     public void testHostileFilenameDoesNotError() throws Exception {
+         Response response = WebClient
+                 .create(endPoint + TIKA_PATH + "/text")
+                 .header("Content-Disposition", "attachment; 
filename=\"a.b/../../c\"")
+                 .put(new ByteArrayInputStream(body(50)));
+ 
+         assertNotEquals(500, response.getStatus(),
+                 "a hostile filename suffix must not produce a server error");
+     }
+ 
+     /**
+      * Chunked uploads carry no usable Content-Length, so the declared-length 
check cannot
+      * fire and the counting stream is the only thing enforcing the limit.
+      */
+     @Test
+     public void testOverLimitRejectedWhenChunked() throws Exception {
+         WebClient client = WebClient.create(endPoint + TIKA_PATH + "/text");
+         WebClient
+                 .getConfig(client)
+                 .getRequestContext()
+                 .put("use.async.http.conduit", Boolean.FALSE);
+         WebClient
+                 .getConfig(client)
+                 .getHttpConduit()
+                 .getClient()
+                 .setAllowChunking(true);
+ 
+         Response response = client.put(new ByteArrayInputStream(body((int) 
MAX_BYTES * 4)));
+ 
+         assertNotEquals(200, response.getStatus(),
+                 "an over-limit chunked body must not parse successfully");
+     }
+ 
+     private static byte[] body(int approxBytes) {
+         StringBuilder sb = new StringBuilder("<html><body>");
+         while (sb.length() < approxBytes) {
+             sb.append("aaaaaaaaaa");
+         }
+         return sb
+                 .append("</body></html>")
+                 .toString()
+                 .getBytes(UTF_8);
+     }
+ }

Reply via email to