This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch branch_3x
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/branch_3x by this push:
new a365b1d465 TIKA-4760 for 3x
a365b1d465 is described below
commit a365b1d465b6aa33b4bc6e428294ea2be04b28fc
Author: tallison <[email protected]>
AuthorDate: Wed Jun 24 20:59:11 2026 -0400
TIKA-4760 for 3x
---
CHANGES.txt | 6 +++
.../java/org/apache/tika/config/ConfigBase.java | 40 ++++++++++++++++++-
.../apache/tika/pipes/fetcher/FetcherManager.java | 16 ++++++++
.../apache/tika/server/core/TikaServerConfig.java | 24 ++++++++++++
.../apache/tika/server/core/TikaServerProcess.java | 4 +-
.../tika/server/core/TikaServerConfigTest.java | 45 ++++++++++++++++++++++
.../resources/configs/tika-config-server-basic.xml | 1 +
.../configs/tika-config-server-emitter.xml | 2 +-
...ml => tika-config-server-pipes-no-unsecure.xml} | 10 +----
9 files changed, 136 insertions(+), 12 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 0bdf66fa55..17c5276bab 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,9 @@
+Release 3.3.2 - (unreleased)
+
+ * tika-server now requires enableUnsecureFeatures=true when the /pipes,
/async, or
+ /status endpoints are selected; the server refuses to start otherwise.
Previously
+ listing the endpoint was treated as sufficient consent (TIKA-4760).
+
Release 3.3.1 - 5/20/2026
* Dependency upgrades (TIKA-4695).
diff --git a/tika-core/src/main/java/org/apache/tika/config/ConfigBase.java
b/tika-core/src/main/java/org/apache/tika/config/ConfigBase.java
index 4afe30ac36..bee7fcd0a0 100644
--- a/tika-core/src/main/java/org/apache/tika/config/ConfigBase.java
+++ b/tika-core/src/main/java/org/apache/tika/config/ConfigBase.java
@@ -124,6 +124,24 @@ public abstract class ConfigBase {
protected static <P, T> P buildComposite(String compositeElementName,
Class<P> compositeClass,
String itemName, Class<T>
itemClass, InputStream is)
throws TikaConfigException, IOException {
+ P composite = buildCompositeOrNull(compositeElementName,
compositeClass, itemName,
+ itemClass, is);
+ if (composite == null) {
+ throw new TikaConfigException("could not find " +
compositeElementName);
+ }
+ return composite;
+ }
+
+ /**
+ * As {@link #buildComposite(String, Class, String, Class, InputStream)},
but returns
+ * {@code null} instead of throwing when the composite element (e.g.
{@code <fetchers/>})
+ * is absent from the config. If the element is present but a child item
is misconfigured,
+ * this still throws so genuine configuration errors are not silently
swallowed.
+ */
+ protected static <P, T> P buildCompositeOrNull(String compositeElementName,
+ Class<P> compositeClass,
String itemName,
+ Class<T> itemClass,
InputStream is)
+ throws TikaConfigException, IOException {
Element properties = null;
try {
properties = XMLReaderUtils.buildDOM(is).getDocumentElement();
@@ -132,7 +150,7 @@ public abstract class ConfigBase {
} catch (TikaException e) {
throw new TikaConfigException("problem loading xml to dom", e);
}
- return buildComposite(compositeElementName, compositeClass, itemName,
itemClass,
+ return buildCompositeOrNull(compositeElementName, compositeClass,
itemName, itemClass,
properties);
}
@@ -140,6 +158,24 @@ public abstract class ConfigBase {
String itemName, Class<T>
itemClass,
Element properties)
throws TikaConfigException, IOException {
+ P composite = buildCompositeOrNull(compositeElementName,
compositeClass, itemName,
+ itemClass, properties);
+ if (composite == null) {
+ throw new TikaConfigException("could not find " +
compositeElementName);
+ }
+ return composite;
+ }
+
+ /**
+ * As {@link #buildComposite(String, Class, String, Class, Element)}, but
returns
+ * {@code null} instead of throwing when the composite element (e.g.
{@code <fetchers/>})
+ * is absent from the config. If the element is present but a child item
is misconfigured,
+ * this still throws so genuine configuration errors are not silently
swallowed.
+ */
+ protected static <P, T> P buildCompositeOrNull(String compositeElementName,
+ Class<P> compositeClass,
String itemName,
+ Class<T> itemClass, Element
properties)
+ throws TikaConfigException, IOException {
if (!("properties".equals(properties.getNodeName()) ||
"properties".equals(properties.getLocalName()))) {
@@ -167,7 +203,7 @@ public abstract class ConfigBase {
}
}
}
- throw new TikaConfigException("could not find " +
compositeElementName);
+ return null;
}
private static <T> List<T> loadComposite(Node composite, String itemName,
diff --git
a/tika-core/src/main/java/org/apache/tika/pipes/fetcher/FetcherManager.java
b/tika-core/src/main/java/org/apache/tika/pipes/fetcher/FetcherManager.java
index f99806a90f..23fa876eee 100644
--- a/tika-core/src/main/java/org/apache/tika/pipes/fetcher/FetcherManager.java
+++ b/tika-core/src/main/java/org/apache/tika/pipes/fetcher/FetcherManager.java
@@ -43,6 +43,22 @@ public class FetcherManager extends ConfigBase {
"fetcher", Fetcher.class, is);
}
}
+
+ /**
+ * As {@link #load(Path)}, but returns an empty manager (no fetchers) when
the config
+ * contains no {@code <fetchers/>} element, instead of throwing. This
allows a server to
+ * enable unsecure features (e.g. for the status endpoint) without being
forced to
+ * configure a fetcher. If the {@code <fetchers/>} element is present but
a fetcher is
+ * misconfigured, this still throws.
+ */
+ public static FetcherManager loadOrEmpty(Path p) throws IOException,
TikaConfigException {
+ try (InputStream is =
+ Files.newInputStream(p)) {
+ FetcherManager fetcherManager =
FetcherManager.buildCompositeOrNull("fetchers",
+ FetcherManager.class, "fetcher", Fetcher.class, is);
+ return fetcherManager == null ? new FetcherManager(List.of()) :
fetcherManager;
+ }
+ }
private final Map<String, Fetcher> fetcherMap = new ConcurrentHashMap<>();
public FetcherManager(List<Fetcher> fetchers) throws TikaConfigException {
diff --git
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
index 663e95bebf..9c08dd3c4a 100644
---
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
@@ -78,6 +78,13 @@ public class TikaServerConfig extends ConfigBase {
"as you've given your fetchers and the same write
permissions " + "as your emitters.\n" + "Users could request and receive a
sensitive file from your\n" +
"drive or a webpage from your intranet and/or send
malicious content to\n" + " your emitter endpoints. See CVE-2015-3271.\n" +
"Please make sure you know what you are doing.";
+ /**
+ * Endpoints that expose unsecure features (process-isolated pipes
parsing, async
+ * batch processing, and server status). Selecting any of these requires
+ * {@code enableUnsecureFeatures=true} as an explicit, deliberate opt-in.
+ */
+ private static final Set<String> ENDPOINTS_REQUIRING_UNSECURE_FEATURES =
+ new HashSet<>(Arrays.asList("pipes", "async", "status"));
private static final List<String> ONLY_IN_FORK_MODE = Arrays.asList(
new String[]{"taskTimeoutMillis", "taskPulseMillis", "maxFiles",
"javaPath", "maxRestarts", "numRestarts", "forkedStatusFile",
"maxForkedStartupMillis",
"tmpFilePrefix"});
@@ -445,6 +452,23 @@ private long forkedProcessShutdownMillis =
DEFAULT_FORKED_PROCESS_SHUTDOWN_MILLI
if (host == null) {
throw new TikaConfigException("Must specify 'host'");
}
+ if (!enableUnsecureFeatures) {
+ List<String> requireUnsecure = new ArrayList<>();
+ for (String endpoint : endpoints) {
+ if (ENDPOINTS_REQUIRING_UNSECURE_FEATURES.contains(endpoint)
+ && !requireUnsecure.contains(endpoint)) {
+ requireUnsecure.add(endpoint);
+ }
+ }
+ if (!requireUnsecure.isEmpty()) {
+ throw new TikaConfigException(
+ "The following selected endpoint(s) require unsecure
features to be " +
+ "enabled: " + requireUnsecure + ". Set
'enableUnsecureFeatures' to true " +
+ "in the 'server' section of your tika-config and
confirm you understand " +
+ "the security implications. These endpoints were
previously enabled " +
+ "simply by selecting them; the explicit opt-in is now
required.");
+ }
+ }
if (!StringUtils.isBlank(port)) {
setPort(port);
diff --git
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
index 0b1f7ab209..010348955e 100644
---
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
+++
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
@@ -205,7 +205,9 @@ public class TikaServerProcess {
FetcherManager fetcherManager = null;
InputStreamFactory inputStreamFactory = null;
if (tikaServerConfig.isEnableUnsecureFeatures()) {
- fetcherManager =
FetcherManager.load(tikaServerConfig.getConfigPath());
+ //tolerate configs that enable unsecure features (e.g. for the
status endpoint)
+ //without declaring any fetchers
+ fetcherManager =
FetcherManager.loadOrEmpty(tikaServerConfig.getConfigPath());
inputStreamFactory = new FetcherStreamFactory(fetcherManager);
} else {
inputStreamFactory = new DefaultInputStreamFactory();
diff --git
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerConfigTest.java
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerConfigTest.java
index fb5c570c7c..ba7c2aafff 100644
---
a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerConfigTest.java
+++
b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerConfigTest.java
@@ -18,6 +18,7 @@ package org.apache.tika.server.core;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Path;
@@ -35,6 +36,7 @@ import org.apache.commons.cli.Options;
import org.junit.jupiter.api.Test;
import org.apache.tika.config.TikaConfigTest;
+import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.utils.ProcessUtils;
public class TikaServerConfigTest {
@@ -108,6 +110,49 @@ public class TikaServerConfigTest {
assertEquals(9999, ports[5]);
}
+ @Test
+ public void testUnsecureEndpointRequiresEnableUnsecureFeatures() throws
Exception {
+ // Selecting pipes (or async, status) without enableUnsecureFeatures
must fail
+ // at config load (validateConsistency runs in load(CommandLine)),
forcing an
+ // explicit opt-in.
+ CommandLineParser parser = new DefaultParser();
+ Path path = Paths.get(TikaConfigTest.class
+
.getResource("/configs/tika-config-server-pipes-no-unsecure.xml")
+ .toURI());
+ CommandLine commandLine = parser.parse(new Options()
+ .addOption(Option
+ .builder("c")
+ .longOpt("config")
+ .hasArg()
+ .get()), new String[]{"-c",
ProcessUtils.escapeCommandLine(path
+ .toAbsolutePath()
+ .toString())});
+ TikaConfigException ex = assertThrows(TikaConfigException.class,
+ () -> TikaServerConfig.load(commandLine));
+ assertTrue(ex.getMessage().contains("enableUnsecureFeatures"),
ex.getMessage());
+ assertTrue(ex.getMessage().contains("pipes"), ex.getMessage());
+ }
+
+ @Test
+ public void testUnsecureEndpointAllowedWithEnableUnsecureFeatures() throws
Exception {
+ // tika-config-server-basic.xml selects the 'status' endpoint together
with
+ // enableUnsecureFeatures=true, so it must load without error.
+ CommandLineParser parser = new DefaultParser();
+ Path path = Paths.get(TikaConfigTest.class
+ .getResource("/configs/tika-config-server-basic.xml")
+ .toURI());
+ CommandLine commandLine = parser.parse(new Options()
+ .addOption(Option
+ .builder("c")
+ .longOpt("config")
+ .hasArg()
+ .get()), new String[]{"-c",
ProcessUtils.escapeCommandLine(path
+ .toAbsolutePath()
+ .toString())});
+ TikaServerConfig config = TikaServerConfig.load(commandLine);
+ assertTrue(config.isEnableUnsecureFeatures());
+ }
+
@Test
public void testTlsConfig() throws Exception {
Set<String> settings = new HashSet<>();
diff --git
a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-basic.xml
b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-basic.xml
index 0825612781..3bbe269504 100644
---
a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-basic.xml
+++
b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-basic.xml
@@ -24,6 +24,7 @@
<forkedJvmArgs>
<arg>-Xmx512m</arg>
</forkedJvmArgs>
+ <enableUnsecureFeatures>true</enableUnsecureFeatures>
<endpoints>
<endpoint>rmeta</endpoint>
<endpoint>status</endpoint>
diff --git
a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-emitter.xml
b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-emitter.xml
index 201f552f13..e158cc7a69 100644
---
a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-emitter.xml
+++
b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-emitter.xml
@@ -23,7 +23,7 @@
<forkedJvmArgs>
<arg>-Xmx512m</arg>
</forkedJvmArgs>
- <enableUnsecure>true</enableUnsecure>
+ <enableUnsecureFeatures>true</enableUnsecureFeatures>
<endpoints>
<endpoint>emit</endpoint>
<endpoint>async</endpoint>
diff --git
a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-basic.xml
b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-pipes-no-unsecure.xml
similarity index 77%
copy from
tika-server/tika-server-core/src/test/resources/configs/tika-config-server-basic.xml
copy to
tika-server/tika-server-core/src/test/resources/configs/tika-config-server-pipes-no-unsecure.xml
index 0825612781..631e78a92a 100644
---
a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-basic.xml
+++
b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-pipes-no-unsecure.xml
@@ -17,16 +17,10 @@
-->
<properties>
<server>
- <taskTimeoutMillis>120000</taskTimeoutMillis>
- <minimumTimeoutMillis>10</minimumTimeoutMillis>
<port>9999</port>
- <maxFiles>1000</maxFiles>
- <forkedJvmArgs>
- <arg>-Xmx512m</arg>
- </forkedJvmArgs>
<endpoints>
- <endpoint>rmeta</endpoint>
- <endpoint>status</endpoint>
+ <endpoint>tika</endpoint>
+ <endpoint>pipes</endpoint>
</endpoints>
</server>
</properties>