Skip to content

Commit 30669a0

Browse files
committed
combined path checks for manifest and fetch files and added tests for malicious urls
1 parent 9b0db29 commit 30669a0

12 files changed

Lines changed: 98 additions & 109 deletions

File tree

src/main/java/gov/loc/repository/bagit/domain/FetchItem.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package gov.loc.repository.bagit.domain;
22

33
import java.net.URL;
4+
import java.nio.file.Path;
45
import java.util.Objects;
56

67
/**
@@ -19,13 +20,13 @@ public final class FetchItem {
1920
public final Long length;
2021

2122
/**
22-
* The path relative to the /data directory
23+
* The path where the fetched item should be put
2324
*/
24-
public final String path;
25+
public final Path path;
2526

2627
private transient String cachedString;
2728

28-
public FetchItem(final URL url, final Long length, final String path){
29+
public FetchItem(final URL url, final Long length, final Path path){
2930
this.url = url;
3031
this.length = length;
3132
this.path = path;
@@ -64,7 +65,7 @@ public Long getLength() {
6465
return length;
6566
}
6667

67-
public String getPath() {
68+
public Path getPath() {
6869
return path;
6970
}
7071

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package gov.loc.repository.bagit.exceptions;
2+
3+
/**
4+
* Class to represent an error when the bag manifest file does not conform to the bagit specfication format
5+
*/
6+
public class InvalidBagitFileFormatException extends Exception {
7+
private static final long serialVersionUID = 1L;
8+
9+
public InvalidBagitFileFormatException(final String message){
10+
super(message);
11+
}
12+
}

src/main/java/gov/loc/repository/bagit/exceptions/InvalidFetchFormatException.java

Lines changed: 0 additions & 15 deletions
This file was deleted.

src/main/java/gov/loc/repository/bagit/exceptions/InvalidManifestFormatException.java

Lines changed: 0 additions & 15 deletions
This file was deleted.

src/main/java/gov/loc/repository/bagit/reader/BagReader.java

Lines changed: 26 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
import java.io.BufferedReader;
44
import java.io.IOException;
5+
import java.net.URI;
6+
import java.net.URISyntaxException;
57
import java.net.URL;
68
import java.nio.charset.Charset;
79
import java.nio.charset.StandardCharsets;
810
import java.nio.file.DirectoryStream;
911
import java.nio.file.Files;
1012
import java.nio.file.Path;
13+
import java.nio.file.Paths;
1114
import java.util.AbstractMap.SimpleImmutableEntry;
1215
import java.util.ArrayList;
1316
import java.util.HashMap;
@@ -22,8 +25,7 @@
2225
import gov.loc.repository.bagit.domain.Manifest;
2326
import gov.loc.repository.bagit.domain.Version;
2427
import gov.loc.repository.bagit.exceptions.InvalidBagMetadataException;
25-
import gov.loc.repository.bagit.exceptions.InvalidFetchFormatException;
26-
import gov.loc.repository.bagit.exceptions.InvalidManifestFormatException;
28+
import gov.loc.repository.bagit.exceptions.InvalidBagitFileFormatException;
2729
import gov.loc.repository.bagit.exceptions.MaliciousPathException;
2830
import gov.loc.repository.bagit.exceptions.UnparsableVersionException;
2931
import gov.loc.repository.bagit.exceptions.UnsupportedAlgorithmException;
@@ -61,10 +63,9 @@ public BagReader(final BagitAlgorithmNameToSupportedAlgorithmMapping nameMapping
6163
* @throws MaliciousPathException if there is path that is referenced in the manifest or fetch file that is outside the bag root directory
6264
* @throws InvalidBagMetadataException if the metadata or bagit.txt file does not conform to the bagit spec
6365
* @throws UnsupportedAlgorithmException if the manifest uses a algorithm that isn't supported
64-
* @throws InvalidManifestFormatException if the manifest is not formatted properly
65-
* @throws InvalidFetchFormatException if the fetch format does not follow the bagit specification
66+
* @throws InvalidBagitFileFormatException if the manifest or fetch file is not formatted properly
6667
*/
67-
public Bag read(final Path rootDir) throws IOException, UnparsableVersionException, MaliciousPathException, InvalidBagMetadataException, UnsupportedAlgorithmException, InvalidManifestFormatException, InvalidFetchFormatException{
68+
public Bag read(final Path rootDir) throws IOException, UnparsableVersionException, MaliciousPathException, InvalidBagMetadataException, UnsupportedAlgorithmException, InvalidBagitFileFormatException{
6869
final Bag bag = new Bag();
6970

7071
//@Incubating
@@ -139,7 +140,7 @@ Version parseVersion(final String version) throws UnparsableVersionException{
139140
/*
140141
* Finds and reads all manifest files in the rootDir and adds them to the given bag.
141142
*/
142-
void readAllManifests(final Path rootDir, final Bag bag) throws IOException, MaliciousPathException, UnsupportedAlgorithmException, InvalidManifestFormatException{
143+
void readAllManifests(final Path rootDir, final Bag bag) throws IOException, MaliciousPathException, UnsupportedAlgorithmException, InvalidBagitFileFormatException{
143144
logger.info("Attempting to find and read manifests");
144145
final DirectoryStream<Path> manifests = getAllManifestFiles(rootDir);
145146

@@ -183,9 +184,9 @@ public boolean accept(final Path file) throws IOException {
183184
* @throws IOException if there is a problem reading a file
184185
* @throws MaliciousPathException if there is path that is referenced in the manifest that is outside the bag root directory
185186
* @throws UnsupportedAlgorithmException if the manifest uses a algorithm that isn't supported
186-
* @throws InvalidManifestFormatException if the manifest is not formatted properly
187+
* @throws InvalidBagitFileFormatException if the manifest is not formatted properly
187188
*/
188-
public Manifest readManifest(final Path manifestFile, final Path bagRootDir, final Charset charset) throws IOException, MaliciousPathException, UnsupportedAlgorithmException, InvalidManifestFormatException{
189+
public Manifest readManifest(final Path manifestFile, final Path bagRootDir, final Charset charset) throws IOException, MaliciousPathException, UnsupportedAlgorithmException, InvalidBagitFileFormatException{
189190
logger.debug("Reading manifest [{}]", manifestFile);
190191
final String alg = PathUtils.getFilename(manifestFile).split("[-\\.]")[1];
191192
final SupportedAlgorithm algorithm = nameMapping.getSupportedAlgorithm(alg);
@@ -201,7 +202,7 @@ public Manifest readManifest(final Path manifestFile, final Path bagRootDir, fin
201202
/*
202203
* read the manifest file into a map of files and checksums
203204
*/
204-
Map<Path, String> readChecksumFileMap(final Path manifestFile, final Path bagRootDir, final Charset charset) throws IOException, MaliciousPathException, InvalidManifestFormatException{
205+
Map<Path, String> readChecksumFileMap(final Path manifestFile, final Path bagRootDir, final Charset charset) throws IOException, MaliciousPathException, InvalidBagitFileFormatException{
205206
final HashMap<Path, String> map = new HashMap<>();
206207
final BufferedReader br = Files.newBufferedReader(manifestFile, charset);
207208

@@ -222,22 +223,31 @@ Map<Path, String> readChecksumFileMap(final Path manifestFile, final Path bagRoo
222223
/*
223224
* Create the file and check it for various things, like starting with a *
224225
*/
225-
private Path createFileFromManifest(final Path bagRootDir, final String path) throws MaliciousPathException, InvalidManifestFormatException{
226+
private Path createFileFromManifest(final Path bagRootDir, final String path) throws MaliciousPathException, InvalidBagitFileFormatException{
226227
String fixedPath = path;
227228
if(path.charAt(0) == '*'){
228229
logger.warn("Encountered path that was created by non-bagit tool. Removing * from path. Please remove all * from manifest files!");
229230
fixedPath = path.substring(1); //remove the * from the path
230231
}
231232

232233
if(path.contains("\\")){
233-
throw new InvalidManifestFormatException(ERROR_PREFIX + path + "] is invalid due to the use of the path separactor [\\]");
234+
throw new InvalidBagitFileFormatException(ERROR_PREFIX + path + "] is invalid due to the use of the path separactor [\\]");
234235
}
235236

236237
if(path.contains("~/")){
237238
throw new MaliciousPathException(ERROR_PREFIX + path + "] is trying to be malicious and access a file outside the bag");
238239
}
239240

240-
final Path file = bagRootDir.resolve(PathUtils.decodeFilname(fixedPath)).normalize();
241+
fixedPath = PathUtils.decodeFilname(fixedPath);
242+
Path file = bagRootDir.resolve(fixedPath).normalize();
243+
if(fixedPath.startsWith("file://")){
244+
try {
245+
file = Paths.get(new URI(fixedPath));
246+
} catch (URISyntaxException e) {
247+
// TODO Auto-generated catch block
248+
e.printStackTrace();
249+
}
250+
}
241251

242252
if(!file.normalize().startsWith(bagRootDir)){
243253
throw new MaliciousPathException(ERROR_PREFIX + file + "] is outside the bag root directory of " + bagRootDir +
@@ -284,11 +294,11 @@ public List<SimpleImmutableEntry<String, String>> readBagMetadata(final Path roo
284294
* @return a list of items to fetch
285295
*
286296
* @throws IOException if there is a problem reading a file
287-
* @throws InvalidFetchFormatException if the fetch format does not follow the bagit specification
288297
* @throws MaliciousPathException if the path was crafted to point outside the bag directory
298+
* @throws InvalidBagitFileFormatException if the fetch format does not follow the bagit specification
289299
*/
290300
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
291-
public List<FetchItem> readFetch(final Path fetchFile, final Charset encoding, final Path bagRootDir) throws IOException, MaliciousPathException, InvalidFetchFormatException{
301+
public List<FetchItem> readFetch(final Path fetchFile, final Charset encoding, final Path bagRootDir) throws IOException, MaliciousPathException, InvalidBagitFileFormatException{
292302
logger.info("Attempting to read [{}]", fetchFile);
293303
final BufferedReader br = Files.newBufferedReader(fetchFile, encoding);
294304
final List<FetchItem> itemsToFetch = new ArrayList<>();
@@ -299,12 +309,12 @@ public List<FetchItem> readFetch(final Path fetchFile, final Charset encoding, f
299309
URL url = null;
300310
while(line != null){
301311
parts = line.split("\\s+", 3);
302-
checkPath(parts[2], bagRootDir);
312+
final Path path = createFileFromManifest(bagRootDir, parts[2]);
303313
length = parts[1].equals("-") ? -1 : Long.decode(parts[1]);
304314
url = new URL(parts[0]);
305315

306316
logger.debug("Read URL [{}] length [{}] path [{}] from fetch file [{}]", url, length, parts[2], fetchFile);
307-
final FetchItem itemToFetch = new FetchItem(url, length, parts[2]);
317+
final FetchItem itemToFetch = new FetchItem(url, length, path);
308318
itemsToFetch.add(itemToFetch);
309319

310320
line = br.readLine();
@@ -313,26 +323,6 @@ public List<FetchItem> readFetch(final Path fetchFile, final Charset encoding, f
313323
return itemsToFetch;
314324
}
315325

316-
/*
317-
* Check for malicious paths as well as invalid format
318-
*/
319-
private void checkPath(final String path, final Path bagRoot) throws MaliciousPathException, InvalidFetchFormatException{
320-
if(path.contains("\\")){
321-
logger.warn("Path [{}] contains [\\] which is not allowed by the bagit specification. Use [/] instead.", path);
322-
throw new InvalidFetchFormatException(ERROR_PREFIX + path + "] uses invalid path separator [\\]. Use [/] instead.");
323-
}
324-
325-
if(path.contains("~/")){
326-
throw new MaliciousPathException(ERROR_PREFIX + path + "] is trying to be malicious by accessing a file outside the bag");
327-
}
328-
329-
final Path finalPath = bagRoot.resolve(path).normalize();
330-
if(!finalPath.startsWith(bagRoot)){
331-
throw new MaliciousPathException(ERROR_PREFIX + path + "] is outside the bag root directory of " + bagRoot +
332-
"! This is not allowed according to the bagit specification!");
333-
}
334-
}
335-
336326
/*
337327
* Generic method to read key value pairs from the bagit files, like bagit.txt or bag-info.txt
338328
*/

src/main/java/gov/loc/repository/bagit/verify/BagVerifier.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import gov.loc.repository.bagit.domain.Version;
2727
import gov.loc.repository.bagit.exceptions.CorruptChecksumException;
2828
import gov.loc.repository.bagit.exceptions.FileNotInPayloadDirectoryException;
29-
import gov.loc.repository.bagit.exceptions.InvalidManifestFormatException;
29+
import gov.loc.repository.bagit.exceptions.InvalidBagitFileFormatException;
3030
import gov.loc.repository.bagit.exceptions.InvalidPayloadOxumException;
3131
import gov.loc.repository.bagit.exceptions.MaliciousPathException;
3232
import gov.loc.repository.bagit.exceptions.MissingBagitFileException;
@@ -154,9 +154,9 @@ public void quicklyVerify(final Bag bag, final boolean ignoreHiddenFiles) throws
154154
* @throws MaliciousPathException if there is path that is referenced in the manifest that is outside the bag root directory
155155
* @throws VerificationException some other exception happened during processing so capture it here.
156156
* @throws UnsupportedAlgorithmException if the manifest uses a algorithm that isn't supported
157-
* @throws InvalidManifestFormatException if the manifest is not formatted properly
157+
* @throws InvalidBagitFileFormatException if the manifest is not formatted properly
158158
*/
159-
public void isValid(final Bag bag, final boolean ignoreHiddenFiles) throws IOException, NoSuchAlgorithmException, MissingPayloadManifestException, MissingBagitFileException, MissingPayloadDirectoryException, FileNotInPayloadDirectoryException, InterruptedException, MaliciousPathException, CorruptChecksumException, VerificationException, UnsupportedAlgorithmException, InvalidManifestFormatException{
159+
public void isValid(final Bag bag, final boolean ignoreHiddenFiles) throws IOException, NoSuchAlgorithmException, MissingPayloadManifestException, MissingBagitFileException, MissingPayloadDirectoryException, FileNotInPayloadDirectoryException, InterruptedException, MaliciousPathException, CorruptChecksumException, VerificationException, UnsupportedAlgorithmException, InvalidBagitFileFormatException{
160160
logger.info("Checking if the bag with root directory [{}] is valid.", bag.getRootDir());
161161
isComplete(bag, ignoreHiddenFiles);
162162

@@ -218,11 +218,11 @@ void checkHashes(final Manifest manifest) throws CorruptChecksumException, Inter
218218
* @throws InterruptedException if the threads are interrupted when checking if all files are listed in manifest(s)
219219
* @throws MaliciousPathException if there is path that is referenced in the manifest that is outside the bag root directory
220220
* @throws UnsupportedAlgorithmException if the manifest uses a algorithm that isn't supported
221-
* @throws InvalidManifestFormatException if the manifest is not formatted properly
221+
* @throws InvalidBagitFileFormatException if the manifest is not formatted properly
222222
*/
223223
public void isComplete(final Bag bag, final boolean ignoreHiddenFiles) throws
224224
IOException, MissingPayloadManifestException, MissingBagitFileException, MissingPayloadDirectoryException,
225-
FileNotInPayloadDirectoryException, InterruptedException, MaliciousPathException, UnsupportedAlgorithmException, InvalidManifestFormatException{
225+
FileNotInPayloadDirectoryException, InterruptedException, MaliciousPathException, UnsupportedAlgorithmException, InvalidBagitFileFormatException{
226226
logger.info("Checking if the bag with root directory [{}] is complete.", bag.getRootDir());
227227

228228
final Path dataDir = getDataDir(bag);
@@ -321,7 +321,7 @@ private void checkIfAtLeastOnePayloadManifestsExist(final Path rootDir, final Ve
321321
/*
322322
* get all the files listed in all the manifests
323323
*/
324-
private Set<Path> getAllFilesListedInManifests(final Bag bag) throws IOException, MaliciousPathException, UnsupportedAlgorithmException, InvalidManifestFormatException{
324+
private Set<Path> getAllFilesListedInManifests(final Bag bag) throws IOException, MaliciousPathException, UnsupportedAlgorithmException, InvalidBagitFileFormatException{
325325
logger.debug("Getting all files listed in the manifest(s)");
326326
final Set<Path> filesListedInManifests = new HashSet<>();
327327

src/test/java/gov/loc/repository/bagit/domain/FetchItemTest.java

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.net.MalformedURLException;
44
import java.net.URL;
5+
import java.nio.file.Paths;
56

67
import org.junit.Assert;
78
import org.junit.Before;
@@ -18,61 +19,61 @@ public void setup() throws MalformedURLException{
1819

1920
@Test
2021
public void testToString() throws MalformedURLException{
21-
FetchItem item = new FetchItem(url, 1l, "/foo");
22+
FetchItem item = new FetchItem(url, 1l, Paths.get("/foo"));
2223
String expected = "https://github.com/LibraryOfCongress/bagit-java 1 /foo";
2324

2425
assertEquals(expected, item.toString());
2526
}
2627

2728
@Test
2829
public void testHashCodeReturnsSameValueForEqualObjects(){
29-
FetchItem item1 = new FetchItem(url, 1l, "/foo");
30-
FetchItem item2 = new FetchItem(url, 1l, "/foo");
30+
FetchItem item1 = new FetchItem(url, 1l, Paths.get("/foo"));
31+
FetchItem item2 = new FetchItem(url, 1l, Paths.get("/foo"));
3132

3233
assertEquals(item1.hashCode(), item2.hashCode());
3334
}
3435

3536
@Test
3637
public void testHashCodeReturnsDifferentValueForDifferentObjects(){
37-
FetchItem item1 = new FetchItem(url, 1l, "/foo");
38-
FetchItem item2 = new FetchItem(url, 1l, "/bar");
38+
FetchItem item1 = new FetchItem(url, 1l, Paths.get("/foo"));
39+
FetchItem item2 = new FetchItem(url, 1l, Paths.get("/bar"));
3940

4041
assertNotEquals(item1.hashCode(), item2.hashCode());
4142
}
4243

4344
@Test
4445
public void testEqualsReturnsTrueWhenSameObject(){
45-
FetchItem item = new FetchItem(url, 1l, "/foo");
46+
FetchItem item = new FetchItem(url, 1l, Paths.get("/foo"));
4647

4748
assertTrue(item.equals(item));
4849
}
4950

5051
@Test
5152
public void testEqualsReturnsFalseWhenNull(){
52-
FetchItem item = new FetchItem(url, 1l, "/foo");
53+
FetchItem item = new FetchItem(url, 1l, Paths.get("/foo"));
5354

5455
assertFalse(item.equals(null));
5556
}
5657

5758
@Test
5859
public void testEqualsReturnsFalseWhenDifferentTypes(){
59-
FetchItem item = new FetchItem(url, 1l, "/foo");
60+
FetchItem item = new FetchItem(url, 1l, Paths.get("/foo"));
6061

6162
assertFalse(item.equals("foo"));
6263
}
6364

6465
@Test
6566
public void testEqualsReturnsTrueWhenSameValues(){
66-
FetchItem item1 = new FetchItem(url, 1l, "/foo");
67-
FetchItem item2 = new FetchItem(url, 1l, "/foo");
67+
FetchItem item1 = new FetchItem(url, 1l, Paths.get("/foo"));
68+
FetchItem item2 = new FetchItem(url, 1l, Paths.get("/foo"));
6869

6970
assertTrue(item1.equals(item2));
7071
}
7172

7273
@Test
7374
public void testEqualsReturnsFalseWhenDifferentValues(){
74-
FetchItem item1 = new FetchItem(url, 1l, "/foo");
75-
FetchItem item2 = new FetchItem(url, 1l, "/bar");
75+
FetchItem item1 = new FetchItem(url, 1l, Paths.get("/foo"));
76+
FetchItem item2 = new FetchItem(url, 1l, Paths.get("/bar"));
7677

7778
assertFalse(item1.equals(item2));
7879
}

src/test/java/gov/loc/repository/bagit/examples/fetching/FetchHttpFileExample.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import java.io.IOException;
44
import java.net.URL;
55
import java.nio.file.Files;
6-
import java.nio.file.Path;
7-
import java.nio.file.Paths;
86
import java.nio.file.StandardCopyOption;
97

108
import org.junit.Assert;
@@ -26,12 +24,10 @@ public class FetchHttpFileExample extends Assert {
2624
@Test
2725
public void fetchFileUsingJavaStandardLibrary() throws IOException{
2826
//in actual usage you would iterate over the list of FetchItem in the Bag
29-
FetchItem item = new FetchItem(new URL("https://en.wikipedia.org/wiki/Main_Page"), 0l, folder.newFile("Main_page.html").toString());
27+
FetchItem item = new FetchItem(new URL("https://en.wikipedia.org/wiki/Main_Page"), 0l, folder.newFile("Main_page.html").toPath());
3028
try{
31-
Path path = Paths.get(item.path);
32-
33-
Files.copy(item.url.openStream(), path, StandardCopyOption.REPLACE_EXISTING);
34-
assertTrue(Files.exists(path));
29+
Files.copy(item.url.openStream(), item.path, StandardCopyOption.REPLACE_EXISTING);
30+
assertTrue(Files.exists(item.path));
3531
}
3632
catch(Exception e){
3733
e.printStackTrace();

0 commit comments

Comments
 (0)