Skip to content

Commit 81efae2

Browse files
authored
jetty-start cleanup (jetty#9555)
* Extract jars/zips using zipfs * Restrict MavenMetadata external DTD/XSD access * Introduce --allow-insecure-http-downloads * Produce debug log if file exists in destination during copy. * Simpler MavenMetadata * If `maven.repo.uri` has been redeclared by user, automatically set allow insecure http downloads. --------- Signed-off-by: Joakim Erdfelt <[email protected]>
1 parent 2c61011 commit 81efae2

File tree

9 files changed

+232
-67
lines changed

9 files changed

+232
-67
lines changed

jetty-start/src/main/java/org/eclipse/jetty/start/BaseBuilder.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ else if (args.isCreateFiles())
8686
// Handle local directories
8787
fileInitializers.add(new LocalFileInitializer(baseHome));
8888

89-
// Downloads are allowed to be performed
9089
// Setup Maven Local Repo
9190
Path localRepoDir = args.findMavenLocalRepoDir();
9291
if (localRepoDir != null)
@@ -106,7 +105,7 @@ else if (args.isCreateFiles())
106105
fileInitializers.add(new BaseHomeFileInitializer(baseHome));
107106

108107
// Normal URL downloads
109-
fileInitializers.add(new UriFileInitializer(baseHome));
108+
fileInitializers.add(new UriFileInitializer(startArgs, baseHome));
110109
}
111110
}
112111

jetty-start/src/main/java/org/eclipse/jetty/start/FS.java

+54-29
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,20 @@
1616
import java.io.Closeable;
1717
import java.io.File;
1818
import java.io.IOException;
19-
import java.io.InputStream;
19+
import java.net.URI;
20+
import java.nio.file.FileSystem;
21+
import java.nio.file.FileSystemAlreadyExistsException;
2022
import java.nio.file.FileSystems;
2123
import java.nio.file.Files;
2224
import java.nio.file.Path;
2325
import java.nio.file.attribute.FileTime;
24-
import java.util.Enumeration;
26+
import java.util.HashMap;
27+
import java.util.Iterator;
28+
import java.util.List;
2529
import java.util.Locale;
26-
import java.util.zip.ZipEntry;
27-
import java.util.zip.ZipFile;
30+
import java.util.Map;
31+
import java.util.stream.Collectors;
32+
import java.util.stream.Stream;
2833

2934
public class FS
3035
{
@@ -184,40 +189,60 @@ public static void extract(Path archive, Path destination) throws IOException
184189

185190
public static void extractZip(Path archive, Path destination) throws IOException
186191
{
187-
try (ZipFile zip = new ZipFile(archive.toFile()))
192+
StartLog.info("extract %s to %s", archive, destination);
193+
URI jaruri = URI.create("jar:" + archive.toUri());
194+
Map<String, ?> fsEnv = Map.of();
195+
try (FileSystem zipFs = FileSystems.newFileSystem(jaruri, fsEnv))
188196
{
189-
StartLog.info("extract %s to %s", archive, destination);
190-
Enumeration<? extends ZipEntry> entries = zip.entries();
191-
while (entries.hasMoreElements())
192-
{
193-
ZipEntry entry = entries.nextElement();
197+
copyZipContents(zipFs.getPath("/"), destination);
198+
}
199+
catch (FileSystemAlreadyExistsException e)
200+
{
201+
FileSystem zipFs = FileSystems.getFileSystem(jaruri);
202+
copyZipContents(zipFs.getPath("/"), destination);
203+
}
204+
}
194205

195-
if (entry.isDirectory() || entry.getName().startsWith("/META-INF"))
196-
{
197-
// skip
198-
continue;
199-
}
206+
public static void copyZipContents(Path root, Path destination) throws IOException
207+
{
208+
if (!Files.exists(destination))
209+
{
210+
Files.createDirectories(destination);
211+
}
212+
URI outputDirURI = destination.toUri();
213+
URI archiveURI = root.toUri();
214+
int archiveURISubIndex = archiveURI.toASCIIString().indexOf("!/") + 2;
200215

201-
String entryName = entry.getName();
202-
Path destFile = destination.resolve(entryName).normalize().toAbsolutePath();
203-
// make sure extracted path does not escape the destination directory
204-
if (!destFile.startsWith(destination))
216+
try (Stream<Path> entriesStream = Files.walk(root))
217+
{
218+
// ensure proper unpack order (eg: directories before files)
219+
Stream<Path> sorted = entriesStream
220+
.filter((path) -> path.getNameCount() > 0)
221+
.filter((path) -> !path.getName(0).toString().equalsIgnoreCase("META-INF"))
222+
.sorted();
223+
224+
Iterator<Path> pathIterator = sorted.iterator();
225+
while (pathIterator.hasNext())
226+
{
227+
Path path = pathIterator.next();
228+
URI entryURI = path.toUri();
229+
String subURI = entryURI.toASCIIString().substring(archiveURISubIndex);
230+
URI outputPathURI = outputDirURI.resolve(subURI);
231+
Path outputPath = Path.of(outputPathURI);
232+
StartLog.info("zipFs: %s > %s", path, outputPath);
233+
if (Files.isDirectory(path))
205234
{
206-
throw new IOException(String.format("Malicious Archive %s found with bad entry \"%s\"",
207-
archive, entryName));
235+
if (!Files.exists(outputPath))
236+
Files.createDirectory(outputPath);
208237
}
209-
if (!Files.exists(destFile))
238+
else if (Files.exists(outputPath))
210239
{
211-
FS.ensureDirectoryExists(destFile.getParent());
212-
try (InputStream input = zip.getInputStream(entry))
213-
{
214-
StartLog.debug("extracting %s", destFile);
215-
Files.copy(input, destFile);
216-
}
240+
StartLog.debug("skipping extraction (file exists) %s", outputPath);
217241
}
218242
else
219243
{
220-
StartLog.debug("skipping extract (file exists) %s", destFile);
244+
StartLog.info("extracting %s to %s", path, outputPath);
245+
Files.copy(path, outputPath);
221246
}
222247
}
223248
}

jetty-start/src/main/java/org/eclipse/jetty/start/FileInitializer.java

+10-31
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,11 @@ protected Path getDestination(URI uri, String location) throws IOException
109109

110110
Path destination = _basehome.getBasePath(location);
111111

112-
// now on copy/download paths (be safe above all else)
112+
// We restrict our behavior to only modifying what exists in ${jetty.base}.
113+
// If the user decides they want to use advanced setups, such as symlinks to point
114+
// to content outside of ${jetty.base}, that is their decision ad we will not
115+
// attempt to save them from themselves.
116+
// Note: All copy and extract steps, will not replace files that already exist.
113117
if (destination != null && !destination.startsWith(_basehome.getBasePath()))
114118
throw new IOException("For security reasons, Jetty start is unable to process file resource not in ${jetty.base} - " + location);
115119

@@ -120,35 +124,6 @@ protected Path getDestination(URI uri, String location) throws IOException
120124
return destination;
121125
}
122126

123-
protected void download(URI uri, Path destination) throws IOException
124-
{
125-
if (FS.ensureDirectoryExists(destination.getParent()))
126-
StartLog.info("mkdir " + _basehome.toShortForm(destination.getParent()));
127-
128-
StartLog.info("download %s to %s", uri, _basehome.toShortForm(destination));
129-
130-
URLConnection connection = uri.toURL().openConnection();
131-
132-
if (connection instanceof HttpURLConnection)
133-
{
134-
HttpURLConnection http = (HttpURLConnection)uri.toURL().openConnection();
135-
http.setInstanceFollowRedirects(true);
136-
http.setAllowUserInteraction(false);
137-
138-
int status = http.getResponseCode();
139-
140-
if (status != HttpURLConnection.HTTP_OK)
141-
{
142-
throw new IOException("URL GET Failure [" + status + "/" + http.getResponseMessage() + "] on " + uri);
143-
}
144-
}
145-
146-
try (InputStream in = connection.getInputStream())
147-
{
148-
Files.copy(in, destination, StandardCopyOption.REPLACE_EXISTING);
149-
}
150-
}
151-
152127
/**
153128
* Test if any of the Paths exist (as files)
154129
*
@@ -199,7 +174,11 @@ public boolean copyDirectory(Path source, Path destination) throws IOException
199174
if (copyDirectory(from, to))
200175
modified = true;
201176
}
202-
else if (!Files.exists(to))
177+
else if (Files.exists(to))
178+
{
179+
StartLog.debug("skipping copy (file exists in destination) %s", to);
180+
}
181+
else
203182
{
204183
StartLog.info("copy %s to %s", _basehome.toShortForm(from), _basehome.toShortForm(to));
205184
Files.copy(from, to);

jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java

+14
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public class StartArgs
5454
public static final String VERSION;
5555
public static final Set<String> ALL_PARTS = Set.of("java", "opts", "path", "main", "args");
5656
public static final Set<String> ARG_PARTS = Set.of("args");
57+
public static final String ARG_ALLOW_INSECURE_HTTP_DOWNLOADS = "--allow-insecure-http-downloads";
5758

5859
private static final String JETTY_VERSION_KEY = "jetty.version";
5960
private static final String JETTY_TAG_NAME_KEY = "jetty.tag.version";
@@ -216,6 +217,7 @@ public class StartArgs
216217

217218
private boolean exec = false;
218219
private String execProperties;
220+
private boolean allowInsecureHttpDownloads = false;
219221
private boolean approveAllLicenses = false;
220222

221223
public StartArgs(BaseHome baseHome)
@@ -960,6 +962,11 @@ public boolean hasSystemProperties()
960962
return false;
961963
}
962964

965+
public boolean isAllowInsecureHttpDownloads()
966+
{
967+
return allowInsecureHttpDownloads;
968+
}
969+
963970
public boolean isApproveAllLicenses()
964971
{
965972
return approveAllLicenses;
@@ -1244,6 +1251,13 @@ public void parse(final String rawarg, String source)
12441251
return;
12451252
}
12461253

1254+
// Allow insecure-http downloads
1255+
if (ARG_ALLOW_INSECURE_HTTP_DOWNLOADS.equals(arg))
1256+
{
1257+
allowInsecureHttpDownloads = true;
1258+
return;
1259+
}
1260+
12471261
// Enable forked execution of Jetty server
12481262
if ("--approve-all-licenses".equals(arg))
12491263
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//
2+
// ========================================================================
3+
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
4+
//
5+
// This program and the accompanying materials are made available under the
6+
// terms of the Eclipse Public License v. 2.0 which is available at
7+
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
8+
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
9+
//
10+
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
11+
// ========================================================================
12+
//
13+
14+
package org.eclipse.jetty.start.fileinits;
15+
16+
import java.io.IOException;
17+
import java.io.InputStream;
18+
import java.net.ProxySelector;
19+
import java.net.URI;
20+
import java.net.http.HttpClient;
21+
import java.net.http.HttpRequest;
22+
import java.net.http.HttpResponse;
23+
import java.nio.file.Files;
24+
import java.nio.file.Path;
25+
import java.nio.file.StandardCopyOption;
26+
import java.util.Optional;
27+
28+
import org.eclipse.jetty.start.BaseHome;
29+
import org.eclipse.jetty.start.FS;
30+
import org.eclipse.jetty.start.FileInitializer;
31+
import org.eclipse.jetty.start.StartArgs;
32+
import org.eclipse.jetty.start.StartLog;
33+
34+
public abstract class DownloadFileInitializer extends FileInitializer
35+
{
36+
private HttpClient httpClient;
37+
38+
protected DownloadFileInitializer(BaseHome basehome, String... scheme)
39+
{
40+
super(basehome, scheme);
41+
}
42+
43+
protected abstract boolean allowInsecureHttpDownloads();
44+
45+
protected void download(URI uri, Path destination) throws IOException
46+
{
47+
if ("http".equalsIgnoreCase(uri.getScheme()) && !allowInsecureHttpDownloads())
48+
throw new IOException("Insecure HTTP download not allowed (use " + StartArgs.ARG_ALLOW_INSECURE_HTTP_DOWNLOADS + " to bypass): " + uri);
49+
50+
if (Files.exists(destination))
51+
{
52+
if (Files.isRegularFile(destination))
53+
{
54+
StartLog.warn("skipping download of %s : file exists in destination %s", uri, destination);
55+
}
56+
else
57+
{
58+
StartLog.warn("skipping download of %s : path conflict at destination %s", uri, destination);
59+
}
60+
return;
61+
}
62+
63+
if (FS.ensureDirectoryExists(destination.getParent()))
64+
StartLog.info("mkdir " + _basehome.toShortForm(destination.getParent()));
65+
66+
StartLog.info("download %s to %s", uri, _basehome.toShortForm(destination));
67+
68+
HttpClient httpClient = getHttpClient();
69+
70+
try
71+
{
72+
HttpRequest request = HttpRequest.newBuilder()
73+
.uri(uri)
74+
.GET()
75+
.build();
76+
77+
HttpResponse<InputStream> response =
78+
httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
79+
80+
int status = response.statusCode();
81+
82+
if (!allowInsecureHttpDownloads() && (status >= 300) && (status <= 399))
83+
{
84+
// redirection status, provide more details in error
85+
Optional<String> location = response.headers().firstValue("Location");
86+
if (location.isPresent())
87+
{
88+
throw new IOException("URL GET Failure [status " + status + "] on " + uri +
89+
" wanting to redirect to insecure HTTP location (use " +
90+
StartArgs.ARG_ALLOW_INSECURE_HTTP_DOWNLOADS + " to bypass): " +
91+
location.get());
92+
}
93+
}
94+
95+
if (status != 200)
96+
{
97+
throw new IOException("URL GET Failure [status " + status + "] on " + uri);
98+
}
99+
100+
try (InputStream in = response.body())
101+
{
102+
Files.copy(in, destination, StandardCopyOption.REPLACE_EXISTING);
103+
}
104+
}
105+
catch (InterruptedException e)
106+
{
107+
throw new IOException("Failed to GET: " + uri, e);
108+
}
109+
}
110+
111+
private HttpClient getHttpClient()
112+
{
113+
if (httpClient == null)
114+
{
115+
httpClient = HttpClient.newBuilder()
116+
.followRedirects(allowInsecureHttpDownloads() ? HttpClient.Redirect.ALWAYS : HttpClient.Redirect.NORMAL)
117+
.proxy(ProxySelector.getDefault())
118+
.build();
119+
}
120+
return httpClient;
121+
}
122+
}

jetty-start/src/main/java/org/eclipse/jetty/start/fileinits/MavenLocalRepoFileInitializer.java

+14-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222

2323
import org.eclipse.jetty.start.BaseHome;
2424
import org.eclipse.jetty.start.FS;
25-
import org.eclipse.jetty.start.FileInitializer;
2625
import org.eclipse.jetty.start.StartLog;
2726
import org.eclipse.jetty.start.Utils;
2827
import org.xml.sax.SAXException;
@@ -42,7 +41,7 @@
4241
* <dd>optional type and classifier requirement</dd>
4342
* </dl>
4443
*/
45-
public class MavenLocalRepoFileInitializer extends FileInitializer
44+
public class MavenLocalRepoFileInitializer extends DownloadFileInitializer
4645
{
4746
public static class Coordinates
4847
{
@@ -126,6 +125,19 @@ public MavenLocalRepoFileInitializer(BaseHome baseHome, Path localRepoDir, boole
126125
this.mavenRepoUri = mavenRepoUri;
127126
}
128127

128+
@Override
129+
protected boolean allowInsecureHttpDownloads()
130+
{
131+
// Always allow insecure http downloads in this file initializer.
132+
133+
// The user is either using the DEFAULT_REMOTE_REPO, or has redeclared it to a new URI.
134+
// If the `maven.repo.uri` property has been changed from default, this indicates a change
135+
// to a different maven uri, overwhelmingly pointing to a maven repository manager
136+
// like artifactory or nexus. This is viewed as an intentional decision by the
137+
// user and as such we should not put additional hurdles in their way.
138+
return true;
139+
}
140+
129141
private static Path newTempRepo()
130142
{
131143
Path javaTempDir = Paths.get(System.getProperty("java.io.tmpdir"));

0 commit comments

Comments
 (0)