Skip to content

Commit d2d8ad2

Browse files
committed
improve isatab parsing
1 parent 88a6fc5 commit d2d8ad2

4 files changed

Lines changed: 113 additions & 3 deletions

File tree

tika-core/src/main/java/org/apache/tika/io/FilenameUtils.java

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

19+
import java.io.IOException;
20+
import java.nio.file.Path;
1921
import java.util.HashSet;
2022
import java.util.Locale;
2123
import java.util.regex.Matcher;
@@ -268,6 +270,39 @@ public static String getSanitizedEmbeddedFilePath(Metadata metadata,
268270
return retPath;
269271
}
270272

273+
/**
274+
* Resolves {@code name} against {@code dir} and returns the result only if it stays
275+
* within {@code dir}.
276+
* <p>
277+
* The result is {@link Path#normalize() normalized} before it is checked. This is the
278+
* load-bearing step: without it, resolving a name such as {@code ../x} yields the literal
279+
* path {@code dir/../x}, whose path elements still begin with {@code dir}, so the
280+
* {@link Path#startsWith(Path)} check below would wrongly accept it. Normalizing first
281+
* collapses the {@code ..} so the check sees the real location.
282+
* <p>
283+
* Containment is tested with {@link Path#startsWith(Path)} (element by element) rather
284+
* than by comparing path strings, so a sibling whose name merely shares a textual prefix
285+
* with {@code dir} (for example {@code /a/bc} against {@code /a/b}) is correctly treated
286+
* as being outside {@code dir}.
287+
*
288+
* @param dir the directory the resolved path must stay within
289+
* @param name the child name to resolve against {@code dir}; may contain relative
290+
* segments such as {@code ..}
291+
* @return the resolved, normalized path, guaranteed to start with the normalized
292+
* {@code dir}
293+
* @throws IOException if {@code name} resolves to a location outside {@code dir}
294+
*/
295+
public static Path resolveWithin(Path dir, String name) throws IOException {
296+
Path normalizedDir = dir.normalize();
297+
Path resolved = normalizedDir.resolve(name).normalize();
298+
if (!resolved.startsWith(normalizedDir)) {
299+
throw new IOException(
300+
"'" + name + "' resolves to '" + resolved + "', which is outside of '" +
301+
normalizedDir + "'");
302+
}
303+
return resolved;
304+
}
305+
271306
private static int getPrefixLength(String path) {
272307
int prefixLength = org.apache.commons.io.FilenameUtils.getPrefixLength(path);
273308
if (prefixLength > 0) {

tika-core/src/test/java/org/apache/tika/io/FilenameUtilsTest.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@
1818

1919
import static org.junit.jupiter.api.Assertions.assertEquals;
2020
import static org.junit.jupiter.api.Assertions.assertNull;
21+
import static org.junit.jupiter.api.Assertions.assertThrows;
2122
import static org.junit.jupiter.api.Assertions.assertTrue;
2223
import static org.junit.jupiter.api.Assertions.fail;
2324

25+
import java.io.IOException;
26+
import java.nio.file.Path;
27+
import java.nio.file.Paths;
28+
2429
import org.junit.jupiter.api.Test;
2530

2631
import org.apache.tika.metadata.Metadata;
@@ -105,6 +110,32 @@ public void testGetName() throws Exception {
105110
testFilenameEquality("HW.txt", "_1457338542/HW.txt");
106111
}
107112

113+
@Test
114+
public void testResolveWithin() throws Exception {
115+
Path dir = Paths.get("base", "isa");
116+
117+
// plain and nested children resolve to a path under dir
118+
assertEquals(Paths.get("base", "isa", "a_assay.txt"),
119+
FilenameUtils.resolveWithin(dir, "a_assay.txt"));
120+
assertEquals(Paths.get("base", "isa", "sub", "a_assay.txt"),
121+
FilenameUtils.resolveWithin(dir, "sub/a_assay.txt"));
122+
123+
// resolving the directory itself stays within it
124+
assertEquals(dir.normalize(), FilenameUtils.resolveWithin(dir, "."));
125+
126+
// names that resolve out of dir are rejected. These also pin the normalize() call:
127+
// without it the resolved path would still textually begin with dir and be accepted.
128+
assertThrows(IOException.class, () -> FilenameUtils.resolveWithin(dir, "../outside.txt"));
129+
assertThrows(IOException.class, () -> FilenameUtils.resolveWithin(dir, "../../outside.txt"));
130+
assertThrows(IOException.class,
131+
() -> FilenameUtils.resolveWithin(dir, "sub/../../outside.txt"));
132+
133+
// element-wise (not string-prefix) containment: a/bc is not within a/b even though
134+
// the string "a/bc" starts with the string "a/b".
135+
assertThrows(IOException.class,
136+
() -> FilenameUtils.resolveWithin(Paths.get("a", "b"), "../bc"));
137+
}
138+
108139
@Test
109140
public void testExtension() throws Exception {
110141
assertEquals(".pdf", FilenameUtils.getSuffixFromPath("blah/blah/or/something.pdf"));

tika-parsers/tika-parsers-extended/tika-parser-scientific-module/src/main/java/org/apache/tika/parser/isatab/ISArchiveParser.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.File;
2020
import java.io.IOException;
2121
import java.io.InputStream;
22+
import java.nio.file.Path;
2223
import java.util.Collections;
2324
import java.util.Set;
2425

@@ -27,6 +28,7 @@
2728

2829
import org.apache.tika.config.TikaComponent;
2930
import org.apache.tika.exception.TikaException;
31+
import org.apache.tika.io.FilenameUtils;
3032
import org.apache.tika.io.TemporaryResources;
3133
import org.apache.tika.io.TikaInputStream;
3234
import org.apache.tika.metadata.Metadata;
@@ -134,12 +136,15 @@ private void parseStudy(InputStream tis, XHTMLContentHandler xhtml, Metadata met
134136

135137
private void parseAssay(XHTMLContentHandler xhtml, Metadata metadata, ParseContext context)
136138
throws IOException, SAXException, TikaException {
139+
// location starts with "/C:" on windows, so build the directory Path from a File
140+
// rather than Paths.get(). The assay file names come from the investigation file and
141+
// are resolved within this directory.
142+
Path locationDir = new File(this.location).toPath();
137143
for (String assayFileName : metadata.getValues(studyAssayFileNameField)) {
138144
xhtml.startElement("div");
139145
xhtml.element("h3", "ASSAY " + assayFileName);
140-
// location starts with "/C:" on windows, can't use Paths.get()
141-
try (InputStream stream = TikaInputStream.get(new File(this.location + assayFileName).toPath()))
142-
{
146+
Path assayFile = FilenameUtils.resolveWithin(locationDir, assayFileName);
147+
try (InputStream stream = TikaInputStream.get(assayFile)) {
143148
ISATabUtils.parseAssay(stream, xhtml, metadata, context);
144149
}
145150
xhtml.endElement("div");

tika-parsers/tika-parsers-extended/tika-parser-scientific-module/src/test/java/org/apache/tika/parser/isatab/ISArchiveParserTest.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,16 @@
1717
package org.apache.tika.parser.isatab;
1818

1919
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertFalse;
21+
import static org.junit.jupiter.api.Assertions.assertThrows;
22+
23+
import java.io.IOException;
24+
import java.nio.charset.StandardCharsets;
25+
import java.nio.file.Files;
26+
import java.nio.file.Path;
2027

2128
import org.junit.jupiter.api.Test;
29+
import org.junit.jupiter.api.io.TempDir;
2230
import org.xml.sax.ContentHandler;
2331

2432
import org.apache.tika.TikaTest;
@@ -68,4 +76,35 @@ public void testParseArchive() throws Exception {
6876
assertEquals("Stephen", metadata.get("Investigation Person First Name"),
6977
"Invalid Investigation Person First Name");
7078
}
79+
80+
@Test
81+
public void testAssayFileNameResolvedWithinDirectory(@TempDir Path root) throws Exception {
82+
// A file sitting next to (but outside of) the ISA-Tab directory.
83+
Path outside = root.resolve("outside.txt");
84+
Files.write(outside, "OUTSIDE_DIRECTORY_CONTENT".getBytes(StandardCharsets.UTF_8));
85+
86+
// An ISA-Tab directory whose investigation file points an assay at the sibling file
87+
// using a relative name.
88+
Path isaDir = Files.createDirectory(root.resolve("isa"));
89+
Files.write(isaDir.resolve("i_test.txt"),
90+
("STUDY\n"
91+
+ "Study File Name\t\"s_test.txt\"\n"
92+
+ "Study Assay File Name\t\"../outside.txt\"\n")
93+
.getBytes(StandardCharsets.UTF_8));
94+
Path study = isaDir.resolve("s_test.txt");
95+
Files.write(study, "\"Source Name\"\n\"culture1\"\n".getBytes(StandardCharsets.UTF_8));
96+
97+
Parser parser = new ISArchiveParser(isaDir.toString());
98+
ContentHandler handler = new BodyContentHandler();
99+
Metadata metadata = new Metadata();
100+
metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, "s_test.txt");
101+
ParseContext context = new ParseContext();
102+
103+
try (TikaInputStream tis = TikaInputStream.get(study)) {
104+
assertThrows(IOException.class,
105+
() -> parser.parse(tis, handler, metadata, context));
106+
}
107+
assertFalse(handler.toString().contains("OUTSIDE_DIRECTORY_CONTENT"),
108+
"assay reader read a file outside the ISA-Tab directory");
109+
}
71110
}

0 commit comments

Comments
 (0)