Skip to content

Commit aaea543

Browse files
authored
TIKA-4809 stage 7
2 parents 9753c8a + 117a166 commit aaea543

8 files changed

Lines changed: 124 additions & 40 deletions

File tree

tika-core/src/main/java/org/apache/tika/config/ServiceLoader.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
import java.util.regex.Pattern;
3636

3737
import aQute.bnd.annotation.spi.ServiceConsumer;
38+
import org.slf4j.Logger;
39+
import org.slf4j.LoggerFactory;
3840

3941
import org.apache.tika.detect.Detector;
4042
import org.apache.tika.detect.EncodingDetector;
@@ -59,6 +61,8 @@
5961
@ServiceConsumer(MetadataFilter.class)
6062
public class ServiceLoader {
6163

64+
private static final Logger LOG = LoggerFactory.getLogger(ServiceLoader.class);
65+
6266
/**
6367
* The dynamic set of services available in an OSGi environment.
6468
* Managed by the {@link TikaActivator} class and used as an additional
@@ -339,7 +343,10 @@ public <T> List<T> loadStaticServiceProviders(Class<T> iface,
339343
"Class " + name + " is not of type: " + iface);
340344
}
341345
} catch (Throwable t) {
342-
//TODO: swallow, log, throw?
346+
// Swallowed so one bad provider cannot break loading of the rest,
347+
// but logged: an SPI class that silently fails to load looks exactly
348+
// like one that was never registered.
349+
LOG.warn("failed to load service provider {} for {}", name, iface, t);
343350
}
344351
}
345352
}

tika-langdetect/tika-langdetect-charsoup/src/main/java/org/apache/tika/langdetect/charsoup/CharSoupMetadataFilter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
* limitations under the License.
1616
*/
1717
package org.apache.tika.langdetect.charsoup;
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
1821
import org.apache.tika.annotation.TikaComponent;
1922
import org.apache.tika.langdetect.charsoup.core.CharSoupFeatureExtractor;
2023
import org.apache.tika.language.detect.LanguageResult;
@@ -39,6 +42,8 @@
3942
@TikaComponent(name = "charsoup-metadata-filter")
4043
public class CharSoupMetadataFilter extends MetadataFilterBase {
4144

45+
private static final Logger LOG = LoggerFactory.getLogger(CharSoupMetadataFilter.class);
46+
4247
private int maxLength = CharSoupFeatureExtractor.MAX_TEXT_LENGTH;
4348

4449
public void setMaxLength(int maxLength) {
@@ -49,6 +54,9 @@ public void setMaxLength(int maxLength) {
4954
public void filter(Metadata metadata) {
5055
String content = metadata.get(TikaCoreProperties.TIKA_CONTENT);
5156
if (content == null || content.isEmpty()) {
57+
// No tk:content to detect from -- e.g. the ignore handler, which /meta pins.
58+
// Silence here reads as "no language found" rather than "never ran".
59+
LOG.debug("no content to detect language from; filter is a no-op for this document");
5260
return;
5361
}
5462
CharSoupLanguageDetector detector = new CharSoupLanguageDetector();

tika-langdetect/tika-langdetect-opennlp/src/main/java/org/apache/tika/langdetect/opennlp/metadatafilter/OpenNLPMetadataFilter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
*/
1717
package org.apache.tika.langdetect.opennlp.metadatafilter;
1818

19+
import org.slf4j.Logger;
20+
import org.slf4j.LoggerFactory;
21+
1922
import org.apache.tika.annotation.TikaComponent;
2023
import org.apache.tika.langdetect.opennlp.OpenNLPDetector;
2124
import org.apache.tika.language.detect.LanguageResult;
@@ -26,6 +29,8 @@
2629
@TikaComponent
2730
public class OpenNLPMetadataFilter extends MetadataFilterBase {
2831

32+
private static final Logger LOG = LoggerFactory.getLogger(OpenNLPMetadataFilter.class);
33+
2934
private int maxCharsForDetection = 10000;
3035

3136
public void setMaxCharsForDetection(int maxCharsForDetection) {
@@ -38,6 +43,9 @@ public void filter(Metadata metadata) {
3843
detector.setMaxLength(maxCharsForDetection);
3944
String content = metadata.get(TikaCoreProperties.TIKA_CONTENT);
4045
if (content == null) {
46+
// No tk:content to detect from -- e.g. the ignore handler, which /meta pins.
47+
// Silence here reads as "no language found" rather than "never ran".
48+
LOG.debug("no content to detect language from; filter is a no-op for this document");
4149
return;
4250
}
4351
LanguageResult r = detector.detect(content);

tika-langdetect/tika-langdetect-optimaize/src/main/java/org/apache/tika/langdetect/optimaize/metadatafilter/OptimaizeMetadataFilter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
*/
1717
package org.apache.tika.langdetect.optimaize.metadatafilter;
1818

19+
import org.slf4j.Logger;
20+
import org.slf4j.LoggerFactory;
21+
1922
import org.apache.tika.annotation.TikaComponent;
2023
import org.apache.tika.langdetect.optimaize.OptimaizeLangDetector;
2124
import org.apache.tika.language.detect.LanguageResult;
@@ -26,6 +29,8 @@
2629
@TikaComponent
2730
public class OptimaizeMetadataFilter extends MetadataFilterBase {
2831

32+
private static final Logger LOG = LoggerFactory.getLogger(OptimaizeMetadataFilter.class);
33+
2934
private int maxCharsForDetection = OptimaizeLangDetector.DEFAULT_MAX_CHARS_FOR_DETECTION;
3035

3136
public void setMaxCharsForDetection(int maxCharsForDetection) {
@@ -38,6 +43,9 @@ public void filter(Metadata metadata) {
3843
detector.loadModels();
3944
String content = metadata.get(TikaCoreProperties.TIKA_CONTENT);
4045
if (content == null) {
46+
// No tk:content to detect from -- e.g. the ignore handler, which /meta pins.
47+
// Silence here reads as "no language found" rather than "never ran".
48+
LOG.debug("no content to detect language from; filter is a no-op for this document");
4149
return;
4250
}
4351
LanguageResult r = detector.detect(content);

tika-serialization/src/main/java/org/apache/tika/serialization/ParseContextUtils.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,11 @@ public static void resolveAll(ParseContext context, ClassLoader classLoader)
126126
// Try to find this friendly name in any registered component registry
127127
var optionalInfo = ComponentNameResolver.getComponentInfo(friendlyName);
128128
if (optionalInfo.isEmpty()) {
129-
// Not a registered component -- ignored (not applied). WARN so a typo'd config key
130-
// is visible rather than silently dropped.
131-
LOG.warn("Ignoring unrecognized parse-context entry '{}' (not a registered "
132-
+ "component); check for a typo", friendlyName);
133-
continue;
129+
// Fail rather than warn: parse-context is where the DoS limits live
130+
// (timeout-limits, embedded-limits, output-limits), so a typo'd name
131+
// silently reverts to defaults an operator believes they overrode.
132+
throw new TikaConfigException("Unrecognized parse-context entry '" + friendlyName
133+
+ "'. Check for a typo; it does not match any registered component.");
134134
}
135135

136136
ComponentInfo info = optionalInfo.get();

tika-serialization/src/test/java/org/apache/tika/serialization/TestParseContextSerialization.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
import org.apache.tika.config.TimeoutLimits;
3636
import org.apache.tika.config.loader.TikaObjectMapperFactory;
37+
import org.apache.tika.exception.TikaConfigException;
3738
import org.apache.tika.extractor.DocumentSelector;
3839
import org.apache.tika.extractor.SkipEmbeddedDocumentSelector;
3940
import org.apache.tika.metadata.filter.AttachmentCountingListFilter;
@@ -527,4 +528,19 @@ public void testNoDuplicateWhenDifferentContextKeys() throws Exception {
527528
assertNotNull(deserialized.get(ContentHandlerFactory.class));
528529
assertNotNull(deserialized.get(DocumentSelector.class));
529530
}
531+
@Test
532+
public void testUnrecognizedComponentNameFails() throws Exception {
533+
// parse-context carries the DoS limits, so a typo must not silently fall back
534+
// to defaults the operator believes they overrode.
535+
ObjectMapper mapper = createMapper();
536+
ParseContext ctx = mapper.readValue(
537+
"{\"timeout-limitz\": {\"totalTaskTimeoutMillis\": 5000}}", ParseContext.class);
538+
539+
TikaConfigException e = assertThrows(TikaConfigException.class,
540+
() -> ParseContextUtils.resolveAll(ctx,
541+
Thread.currentThread().getContextClassLoader()));
542+
assertTrue(e.getMessage().contains("timeout-limitz"),
543+
"the message must name the offending key: " + e.getMessage());
544+
}
545+
530546
}

tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java

Lines changed: 67 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ public class TikaServerProcess {
101101
private static final Logger LOG = LoggerFactory.getLogger(TikaServerProcess.class);
102102
public static int DO_NOT_RESTART_EXIT_VALUE = -100;
103103

104+
private static final List<String> VALID_ENDPOINTS = List.of("tika", "rmeta", "meta",
105+
"unpack", "detect", "language", "translate", "mime", "mime-types", "detectors",
106+
"parsers", "version", "status", "pipes", "async");
107+
104108
private static Options getOptions() {
105109
Options options = new Options();
106110
options.addOption("h", "host", true, "host name, use * for all)");
@@ -158,6 +162,35 @@ private static void startServer(ServerDetails serverDetails) {
158162
LOG.info("Started Apache Tika server {} at {}", serverDetails.serverId, serverDetails.url);
159163
}
160164

165+
/**
166+
* Warns for settings that widen what a caller can reach. The security prose already
167+
* exists, but only in the message shown when a flag is *not* set -- i.e. only to the
168+
* operators who did not enable the risky thing.
169+
*/
170+
private static void warnOnPermissiveConfig(TikaServerConfig config) {
171+
if (config.isAllowPipes()) {
172+
LOG.warn("allowPipes is enabled: /pipes and /async can fetch and emit through "
173+
+ "configured fetchers/emitters, reaching files and network resources at "
174+
+ "the server's privilege level. Expose only to trusted callers.");
175+
}
176+
if (config.isAllowPerRequestConfig()) {
177+
LOG.warn("allowPerRequestConfig is enabled: callers may set any parser option, "
178+
+ "including options that spawn external processes such as OCR.");
179+
}
180+
if (config.isReturnStackTrace()) {
181+
LOG.warn("returnStackTrace is enabled: error responses include server-side stack "
182+
+ "traces. Intended for development, not production.");
183+
}
184+
if ("*".equals(config.getCors())) {
185+
LOG.warn("cors is '*': any origin may call this server from a browser.");
186+
}
187+
if ("0.0.0.0".equals(config.getHost()) || "*".equals(config.getHost())) {
188+
LOG.warn("host is {}: the server is reachable on every interface. tika-server "
189+
+ "performs no authentication; restrict access at the network layer.",
190+
config.getHost());
191+
}
192+
}
193+
161194
//This returns the server, configured and ready to be started.
162195
private static ServerDetails initServer(TikaServerConfig tikaServerConfig) throws Exception {
163196
String host = tikaServerConfig.getHost();
@@ -199,6 +232,8 @@ private static ServerDetails initServer(TikaServerConfig tikaServerConfig) throw
199232
sf.setOutInterceptors(Collections.singletonList(new GZIPOutInterceptor()));
200233
sf.setInInterceptors(Collections.singletonList(new GZIPInInterceptor()));
201234

235+
warnOnPermissiveConfig(tikaServerConfig);
236+
202237
String protocol = tikaServerConfig
203238
.getTlsConfig()
204239
.isActive() ? "https" : "http";
@@ -362,34 +397,38 @@ static List<ResourceProvider> loadCoreProviders(TikaServerConfig tikaServerConfi
362397
// this default set.
363398
} else {
364399
for (String endPoint : tikaServerConfig.getEndpoints()) {
365-
if ("meta".equals(endPoint)) {
366-
resourceProviders.add(new SingletonResourceProvider(new MetadataResource(tikaResource)));
367-
} else if ("rmeta".equals(endPoint)) {
368-
resourceProviders.add(new SingletonResourceProvider(new RecursiveMetadataResource(tikaResource)));
369-
} else if ("detect".equals(endPoint)) {
370-
resourceProviders.add(new SingletonResourceProvider(new DetectorResource(serverStatus, tikaResource)));
371-
} else if ("language".equals(endPoint)) {
372-
resourceProviders.add(new SingletonResourceProvider(new LanguageResource()));
373-
} else if ("translate".equals(endPoint)) {
374-
resourceProviders.add(new SingletonResourceProvider(new TranslateResource(serverStatus)));
375-
} else if ("tika".equals(endPoint)) {
376-
resourceProviders.add(new SingletonResourceProvider(tikaResource));
377-
} else if ("unpack".equals(endPoint)) {
378-
resourceProviders.add(new SingletonResourceProvider(new UnpackerResource(tikaResource)));
379-
} else if ("mime".equals(endPoint)) {
380-
resourceProviders.add(new SingletonResourceProvider(new TikaMimeTypes(tikaResource)));
381-
} else if ("detectors".equals(endPoint)) {
382-
resourceProviders.add(new SingletonResourceProvider(new TikaDetectors(tikaResource)));
383-
} else if ("parsers".equals(endPoint)) {
384-
resourceProviders.add(new SingletonResourceProvider(new TikaParsers(tikaResource)));
385-
} else if ("version".equals(endPoint)) {
386-
resourceProviders.add(new SingletonResourceProvider(new TikaVersion()));
387-
} else if ("pipes".equals(endPoint)) {
388-
addPipesResource = true;
389-
} else if ("async".equals(endPoint)) {
390-
addAsyncResource = true;
391-
} else if ("status".equals(endPoint)) {
392-
resourceProviders.add(new SingletonResourceProvider(new TikaServerStatus(serverStatus)));
400+
switch (endPoint) {
401+
case "meta" -> resourceProviders.add(
402+
new SingletonResourceProvider(new MetadataResource(tikaResource)));
403+
case "rmeta" -> resourceProviders.add(
404+
new SingletonResourceProvider(new RecursiveMetadataResource(tikaResource)));
405+
case "detect" -> resourceProviders.add(
406+
new SingletonResourceProvider(new DetectorResource(serverStatus, tikaResource)));
407+
case "language" -> resourceProviders.add(
408+
new SingletonResourceProvider(new LanguageResource()));
409+
case "translate" -> resourceProviders.add(
410+
new SingletonResourceProvider(new TranslateResource(serverStatus)));
411+
case "tika" -> resourceProviders.add(
412+
new SingletonResourceProvider(tikaResource));
413+
case "unpack" -> resourceProviders.add(
414+
new SingletonResourceProvider(new UnpackerResource(tikaResource)));
415+
// "mime-types" accepted too: that is the path, and it is the natural guess.
416+
case "mime", "mime-types" -> resourceProviders.add(
417+
new SingletonResourceProvider(new TikaMimeTypes(tikaResource)));
418+
case "detectors" -> resourceProviders.add(
419+
new SingletonResourceProvider(new TikaDetectors(tikaResource)));
420+
case "parsers" -> resourceProviders.add(
421+
new SingletonResourceProvider(new TikaParsers(tikaResource)));
422+
case "version" -> resourceProviders.add(
423+
new SingletonResourceProvider(new TikaVersion()));
424+
case "status" -> resourceProviders.add(
425+
new SingletonResourceProvider(new TikaServerStatus(serverStatus)));
426+
case "pipes" -> addPipesResource = true;
427+
case "async" -> addAsyncResource = true;
428+
// Refuse rather than skip: a dropped name is a missing endpoint at
429+
// runtime with nothing in the log pointing at the config.
430+
default -> throw new TikaConfigException("Unrecognized endpoint '" + endPoint
431+
+ "'. Valid endpoints: " + VALID_ENDPOINTS);
393432
}
394433
}
395434
}

tika-server/tika-server-core/src/test/resources/configs/cxf-unpack-test-template.json

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,10 @@
2525
"throwOnZeroBytes": false
2626
},
2727
"parse-context": {
28-
"digester-factory": {
29-
"commons-digester-factory": {
30-
"digests": [
31-
{ "algorithm": "MD5" }
32-
]
33-
}
28+
"commons-digester-factory": {
29+
"digests": [
30+
{ "algorithm": "MD5" }
31+
]
3432
},
3533
"timeout-limits": {
3634
"progressTimeoutMillis": "TIMEOUT_MILLIS"

0 commit comments

Comments
 (0)