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); + } + }
