WireRestrictedFetchEmitTupleTest.java
/*
* 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.pipes.core.serialization;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.StringReader;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
import org.junit.jupiter.api.Test;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.pipes.api.FetchEmitTuple;
import org.apache.tika.pipes.api.emitter.EmitKey;
import org.apache.tika.pipes.api.fetcher.FetchKey;
/**
* End-to-end checks at the actual wire entry points. A FetchEmitTuple is always untrusted request
* input, so its parseContext must not introduce a wire-blocked component. All three entry points
* (/pipes, /async, fork IPC) share {@code FetchEmitTupleDeserializer}, which enforces this.
*/
public class WireRestrictedFetchEmitTupleTest {
private static final String WIRE_BLOCKED_PARSE_CONTEXT =
"\"parse-context\":{\"typed\":{\"external-parser\":{\"config\":{" +
"\"commandLine\":[\"/bin/sh\",\"-c\",\"echo x\"]," +
"\"supportedTypes\":[\"text/plain\"]}}}}";
private static String tuple(String parseContextField) {
return "{\"id\":\"t\",\"fetcher\":\"f\",\"fetchKey\":\"k\"," +
"\"emitter\":\"e\",\"emitKey\":\"ek\",\"onParseException\":\"skip\"" +
(parseContextField.isEmpty() ? "" : "," + parseContextField) + "}";
}
@Test
public void pipesEndpointRejectsParserInjection() {
Exception e = assertThrows(Exception.class,
() -> JsonFetchEmitTuple.fromJson(new StringReader(tuple(WIRE_BLOCKED_PARSE_CONTEXT))));
assertTrue(root(e).contains("may not be supplied via a request parseContext"),
"expected wire-blocked rejection, got: " + root(e));
}
@Test
public void asyncEndpointRejectsParserInjection() {
Exception e = assertThrows(Exception.class,
() -> JsonFetchEmitTupleList.fromJson(new StringReader("[" + tuple(WIRE_BLOCKED_PARSE_CONTEXT) + "]")));
assertTrue(root(e).contains("may not be supplied via a request parseContext"),
"expected wire-blocked rejection, got: " + root(e));
}
@Test
public void pipesEndpointAllowsSafeParseContext() throws Exception {
String safe = "\"parse-context\":{" +
"\"basic-content-handler-factory\":{\"type\":\"XML\",\"writeLimit\":1000}," +
"\"timeout-limits\":{\"progressTimeoutMillis\":5000,\"totalTaskTimeoutMillis\":60000}}";
FetchEmitTuple t = JsonFetchEmitTuple.fromJson(new StringReader(tuple(safe)));
assertNotNull(t);
assertTrue(t.getParseContext().hasJsonConfig("timeout-limits"));
assertTrue(t.getParseContext().hasJsonConfig("basic-content-handler-factory"));
}
@Test
public void ipcRoundTripsSafeTuple() throws Exception {
FetchEmitTuple t = new FetchEmitTuple("t", new FetchKey("f", "k"),
new EmitKey("e", "ek"), new Metadata(), new ParseContext(),
FetchEmitTuple.ON_PARSE_EXCEPTION.SKIP);
byte[] bytes = JsonPipesIpc.toBytes(t);
FetchEmitTuple back = JsonPipesIpc.fromBytes(bytes, FetchEmitTuple.class);
assertEquals(t, back);
}
@Test
public void forkIpcRejectsParserInjection() throws Exception {
// The fork-IPC path uses Smile but shares the same restricted FetchEmitTupleDeserializer.
byte[] smile = new ObjectMapper(new SmileFactory())
.writeValueAsBytes(new ObjectMapper().readTree(tuple(WIRE_BLOCKED_PARSE_CONTEXT)));
Exception e = assertThrows(Exception.class,
() -> JsonPipesIpc.fromBytes(smile, FetchEmitTuple.class));
assertTrue(root(e).contains("may not be supplied via a request parseContext"),
"expected wire-blocked rejection at fork IPC, got: " + root(e));
}
private static String root(Throwable t) {
Throwable r = t;
while (r.getCause() != null && r.getCause() != r) {
r = r.getCause();
}
return String.valueOf(r.getMessage());
}
}