Skip to content

Commit 88debcd

Browse files
Copilotslachiewicz
andcommitted
Fix directory traversal vulnerability in Expand.extractFile
Co-authored-by: slachiewicz <6705942+slachiewicz@users.noreply.github.com>
1 parent 17b901f commit 88debcd

File tree

2 files changed

+218
-2
lines changed

2 files changed

+218
-2
lines changed

src/main/java/org/codehaus/plexus/util/Expand.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,18 @@ protected void extractFile(
111111
throws Exception {
112112
File f = FileUtils.resolveFile(dir, entryName);
113113

114-
if (!f.getAbsolutePath().startsWith(dir.getAbsolutePath())) {
115-
throw new IOException("Entry '" + entryName + "' outside the target directory.");
114+
// Use canonical paths to prevent bypasses via symlinks, and add separator check
115+
// to prevent partial prefix matches (e.g., /tmp/app matching /tmp/app-data)
116+
try {
117+
String canonicalDirPath = dir.getCanonicalPath();
118+
String canonicalFilePath = f.getCanonicalPath();
119+
120+
if (!canonicalFilePath.equals(canonicalDirPath)
121+
&& !canonicalFilePath.startsWith(canonicalDirPath + File.separator)) {
122+
throw new IOException("Entry '" + entryName + "' outside the target directory.");
123+
}
124+
} catch (IOException e) {
125+
throw new IOException("Failed to validate path for entry '" + entryName + "'", e);
116126
}
117127

118128
try {
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package org.codehaus.plexus.util;
2+
3+
/*
4+
* Copyright The Codehaus Foundation.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
import java.io.File;
20+
import java.io.FileOutputStream;
21+
import java.util.zip.ZipEntry;
22+
import java.util.zip.ZipOutputStream;
23+
24+
import org.junit.jupiter.api.Test;
25+
26+
import static org.junit.jupiter.api.Assertions.assertThrows;
27+
import static org.junit.jupiter.api.Assertions.assertTrue;
28+
29+
/**
30+
* Tests for the Expand class to ensure it properly prevents directory traversal attacks.
31+
*/
32+
public class ExpandTest extends FileBasedTestCase {
33+
34+
/**
35+
* Test that path traversal using ../ is blocked
36+
*/
37+
@Test
38+
void testPathTraversalWithParentDirectory() throws Exception {
39+
File testDir = new File(getTestDirectory(), "expandTest");
40+
testDir.mkdirs();
41+
42+
File zipFile = new File(testDir, "malicious.zip");
43+
File extractDir = new File(testDir, "extract");
44+
extractDir.mkdirs();
45+
46+
// Create a malicious zip with path traversal
47+
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
48+
ZipEntry entry = new ZipEntry("../evil.txt");
49+
zos.putNextEntry(entry);
50+
zos.write("malicious content".getBytes());
51+
zos.closeEntry();
52+
}
53+
54+
Expand expand = new Expand();
55+
expand.setSrc(zipFile);
56+
expand.setDest(extractDir);
57+
58+
// This should throw an IOException due to path traversal detection
59+
assertThrows(
60+
Exception.class, () -> expand.execute(), "Should have thrown exception for path traversal attempt");
61+
62+
// Verify the file was not created outside the target directory
63+
File evilFile = new File(testDir, "evil.txt");
64+
assertTrue(!evilFile.exists(), "File should not have been created outside target directory");
65+
}
66+
67+
/**
68+
* Test that absolute paths are blocked
69+
*/
70+
@Test
71+
void testPathTraversalWithAbsolutePath() throws Exception {
72+
File testDir = new File(getTestDirectory(), "expandTest2");
73+
testDir.mkdirs();
74+
75+
File zipFile = new File(testDir, "malicious.zip");
76+
File extractDir = new File(testDir, "extract");
77+
extractDir.mkdirs();
78+
79+
// Create a malicious zip with absolute path
80+
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
81+
ZipEntry entry = new ZipEntry("/tmp/evil.txt");
82+
zos.putNextEntry(entry);
83+
zos.write("malicious content".getBytes());
84+
zos.closeEntry();
85+
}
86+
87+
Expand expand = new Expand();
88+
expand.setSrc(zipFile);
89+
expand.setDest(extractDir);
90+
91+
// This should throw an IOException due to absolute path
92+
assertThrows(Exception.class, () -> expand.execute(), "Should have thrown exception for absolute path");
93+
}
94+
95+
/**
96+
* Test that valid files are extracted correctly
97+
*/
98+
@Test
99+
void testValidFileExtraction() throws Exception {
100+
File testDir = new File(getTestDirectory(), "expandTest3");
101+
testDir.mkdirs();
102+
103+
File zipFile = new File(testDir, "valid.zip");
104+
File extractDir = new File(testDir, "extract");
105+
extractDir.mkdirs();
106+
107+
String testContent = "valid content";
108+
109+
// Create a valid zip file
110+
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
111+
ZipEntry entry = new ZipEntry("subdir/valid.txt");
112+
zos.putNextEntry(entry);
113+
zos.write(testContent.getBytes());
114+
zos.closeEntry();
115+
}
116+
117+
Expand expand = new Expand();
118+
expand.setSrc(zipFile);
119+
expand.setDest(extractDir);
120+
expand.execute();
121+
122+
// Verify the file was created in the correct location
123+
File extractedFile = new File(extractDir, "subdir/valid.txt");
124+
assertTrue(extractedFile.exists(), "Valid file should have been extracted");
125+
assertTrue(extractedFile.isFile(), "Extracted path should be a file");
126+
}
127+
128+
/**
129+
* Test complex path traversal attempts
130+
*/
131+
@Test
132+
void testComplexPathTraversal() throws Exception {
133+
File testDir = new File(getTestDirectory(), "expandTest4");
134+
testDir.mkdirs();
135+
136+
File zipFile = new File(testDir, "malicious.zip");
137+
File extractDir = new File(testDir, "extract");
138+
extractDir.mkdirs();
139+
140+
// Create a malicious zip with complex path traversal
141+
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
142+
// Try various path traversal techniques
143+
String[] maliciousPaths = {
144+
"../../evil.txt", "../../../evil.txt", "subdir/../../evil.txt", "subdir/../../../evil.txt"
145+
};
146+
147+
for (String path : maliciousPaths) {
148+
ZipEntry entry = new ZipEntry(path);
149+
zos.putNextEntry(entry);
150+
zos.write("malicious content".getBytes());
151+
zos.closeEntry();
152+
}
153+
}
154+
155+
Expand expand = new Expand();
156+
expand.setSrc(zipFile);
157+
expand.setDest(extractDir);
158+
159+
// This should throw an IOException due to path traversal detection
160+
assertThrows(
161+
Exception.class,
162+
() -> expand.execute(),
163+
"Should have thrown exception for complex path traversal attempt");
164+
}
165+
166+
/**
167+
* Test partial path prefix match vulnerability
168+
* This tests the case where the target directory is /tmp/app and an attacker
169+
* tries to write to /tmp/app-data which would pass a naive startsWith check
170+
*/
171+
@Test
172+
void testPartialPrefixMatchVulnerability() throws Exception {
173+
File testDir = new File(getTestDirectory(), "expandTest5");
174+
testDir.mkdirs();
175+
176+
// Create directories with similar names
177+
File extractDir = new File(testDir, "app");
178+
extractDir.mkdirs();
179+
File siblingDir = new File(testDir, "app-data");
180+
siblingDir.mkdirs();
181+
182+
File zipFile = new File(testDir, "malicious.zip");
183+
184+
// Create a malicious zip that tries to escape to the sibling directory
185+
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
186+
ZipEntry entry = new ZipEntry("../app-data/evil.txt");
187+
zos.putNextEntry(entry);
188+
zos.write("malicious content".getBytes());
189+
zos.closeEntry();
190+
}
191+
192+
Expand expand = new Expand();
193+
expand.setSrc(zipFile);
194+
expand.setDest(extractDir);
195+
196+
// This should throw an IOException due to path traversal detection
197+
assertThrows(
198+
Exception.class,
199+
() -> expand.execute(),
200+
"Should have thrown exception for partial prefix match attack");
201+
202+
// Verify the file was not created in the sibling directory
203+
File evilFile = new File(siblingDir, "evil.txt");
204+
assertTrue(!evilFile.exists(), "File should not have been created in sibling directory");
205+
}
206+
}

0 commit comments

Comments
 (0)