Skip to content

Commit 8045266

Browse files
jeffret-bKevin-CB
authored andcommitted
[SECURITY-1807]
Co-authored-by: Kevin-CB <[email protected]>
1 parent a3f3114 commit 8045266

File tree

10 files changed

+652
-177
lines changed

10 files changed

+652
-177
lines changed

core/src/main/java/hudson/FilePath.java

Lines changed: 175 additions & 58 deletions
Large diffs are not rendered by default.

core/src/main/java/hudson/model/DirectoryBrowserSupport.java

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import java.io.Serializable;
3535
import java.net.URL;
3636
import java.nio.charset.StandardCharsets;
37+
import java.nio.file.LinkOption;
38+
import java.nio.file.OpenOption;
3739
import java.text.Collator;
3840
import java.util.ArrayList;
3941
import java.util.Arrays;
@@ -49,6 +51,7 @@
4951
import java.util.StringTokenizer;
5052
import java.util.logging.Level;
5153
import java.util.logging.Logger;
54+
import java.util.regex.Pattern;
5255
import java.util.stream.Collectors;
5356
import java.util.stream.Stream;
5457
import javax.servlet.ServletException;
@@ -83,6 +86,11 @@ public final class DirectoryBrowserSupport implements HttpResponse {
8386
@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Accessible via System Groovy Scripts")
8487
public static boolean ALLOW_SYMLINK_ESCAPE = SystemProperties.getBoolean(DirectoryBrowserSupport.class.getName() + ".allowSymlinkEscape");
8588

89+
@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Accessible via System Groovy Scripts")
90+
public static boolean ALLOW_TMP_DISPLAY = SystemProperties.getBoolean(DirectoryBrowserSupport.class.getName() + ".allowTmpEscape");
91+
92+
private static final Pattern TMPDIR_PATTERN = Pattern.compile(".+@tmp/.*");
93+
8694
/**
8795
* Escape hatch for the protection against SECURITY-2481. If enabled, the absolute paths on Windows will be allowed.
8896
*/
@@ -264,7 +272,7 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root
264272
baseFile = root.child(base);
265273
}
266274

267-
if (baseFile.hasSymlink(getNoFollowLinks())) {
275+
if (baseFile.hasSymlink(getOpenOptions()) || hasTmpDir(baseFile, base, getOpenOptions())) {
268276
rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
269277
return;
270278
}
@@ -280,13 +288,13 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root
280288
includes = rest;
281289
prefix = "";
282290
}
283-
baseFile.zip(rsp.getOutputStream(), includes, null, true, getNoFollowLinks(), prefix);
291+
baseFile.zip(rsp.getOutputStream(), includes, null, true, prefix, getOpenOptions());
284292
return;
285293
}
286294
if (plain) {
287295
rsp.setContentType("text/plain;charset=UTF-8");
288296
try (OutputStream os = rsp.getOutputStream()) {
289-
for (VirtualFile kid : baseFile.list(getNoFollowLinks())) {
297+
for (VirtualFile kid : baseFile.list(getOpenOptions())) {
290298
os.write(kid.getName().getBytes(StandardCharsets.UTF_8));
291299
if (kid.isDirectory()) {
292300
os.write('/');
@@ -310,14 +318,16 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root
310318
List<List<Path>> glob = null;
311319
boolean patternUsed = rest.length() > 0;
312320
boolean containsSymlink = false;
313-
if (patternUsed) {
321+
boolean containsTmpDir = false;
322+
if (patternUsed) {
314323
// the rest is Ant glob pattern
315324
glob = patternScan(baseFile, rest, createBackRef(restSize));
316325
} else
317326
if (serveDirIndex) {
318327
// serve directory index
319328
glob = baseFile.run(new BuildChildPaths(root, baseFile, req.getLocale()));
320-
containsSymlink = baseFile.containsSymLinkChild(getNoFollowLinks());
329+
containsSymlink = baseFile.containsSymLinkChild(getOpenOptions());
330+
containsTmpDir = baseFile.containsTmpDirChild(getOpenOptions());
321331
}
322332

323333
if (glob != null) {
@@ -333,6 +343,7 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root
333343
req.setAttribute("pattern", rest);
334344
req.setAttribute("dir", baseFile);
335345
req.setAttribute("showSymlinkWarning", containsSymlink);
346+
req.setAttribute("showTmpDirWarning", containsTmpDir);
336347
if (ResourceDomainConfiguration.isResourceRequest(req)) {
337348
req.getView(this, "plaindir.jelly").forward(req, rsp);
338349
} else {
@@ -378,7 +389,7 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root
378389
if (view) {
379390
InputStream in;
380391
try {
381-
in = baseFile.open(getNoFollowLinks());
392+
in = baseFile.open(getOpenOptions());
382393
} catch (IOException ioe) {
383394
rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
384395
return;
@@ -406,7 +417,7 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root
406417
}
407418
InputStream in;
408419
try {
409-
in = baseFile.open(getNoFollowLinks());
420+
in = baseFile.open(getOpenOptions());
410421
} catch (IOException ioe) {
411422
rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
412423
return;
@@ -429,6 +440,13 @@ public Boolean call() throws IOException {
429440
}
430441
}
431442

443+
private boolean hasTmpDir(VirtualFile baseFile, String base, OpenOption[] openOptions) {
444+
if (FilePath.isTmpDir(baseFile.getName(), openOptions)) {
445+
return true;
446+
}
447+
return FilePath.isIgnoreTmpDirs(openOptions) && TMPDIR_PATTERN.matcher(base).matches();
448+
}
449+
432450
private List<List<Path>> keepReadabilityOnlyOnDescendants(VirtualFile root, boolean patternUsed, List<List<Path>> pathFragmentsList) {
433451
Stream<List<Path>> pathFragmentsStream = pathFragmentsList.stream().map((List<Path> pathFragments) -> {
434452
List<Path> mappedFragments = new ArrayList<>(pathFragments.size());
@@ -754,7 +772,7 @@ private static final class BuildChildPaths extends MasterToSlaveCallable<List<Li
754772
private static List<List<Path>> buildChildPaths(VirtualFile cur, Locale locale) throws IOException {
755773
List<List<Path>> r = new ArrayList<>();
756774

757-
VirtualFile[] files = cur.list(getNoFollowLinks());
775+
VirtualFile[] files = cur.list(getOpenOptions());
758776
Arrays.sort(files, new FileComparator(locale));
759777

760778
for (VirtualFile f : files) {
@@ -769,7 +787,7 @@ private static List<List<Path>> buildChildPaths(VirtualFile cur, Locale locale)
769787
while (true) {
770788
// files that don't start with '.' qualify for 'meaningful files', nor SCM related files
771789
List<VirtualFile> sub = new ArrayList<>();
772-
for (VirtualFile vf : f.list(getNoFollowLinks())) {
790+
for (VirtualFile vf : f.list(getOpenOptions())) {
773791
String name = vf.getName();
774792
if (!name.startsWith(".") && !name.equals("CVS") && !name.equals(".svn")) {
775793
sub.add(vf);
@@ -794,7 +812,7 @@ private static List<List<Path>> buildChildPaths(VirtualFile cur, Locale locale)
794812
* @param baseRef String like "../../../" that cancels the 'rest' portion. Can be "./"
795813
*/
796814
private static List<List<Path>> patternScan(VirtualFile baseDir, String pattern, String baseRef) throws IOException {
797-
Collection<String> files = baseDir.list(pattern, null, /* TODO what is the user expectation? */true, getNoFollowLinks());
815+
Collection<String> files = baseDir.list(pattern, null, /* TODO what is the user expectation? */true, getOpenOptions());
798816

799817
if (!files.isEmpty()) {
800818
List<List<Path>> r = new ArrayList<>(files.size());
@@ -837,8 +855,15 @@ private static void buildPathList(VirtualFile baseDir, VirtualFile filePath, Lis
837855
pathList.add(path);
838856
}
839857

840-
private static boolean getNoFollowLinks() {
841-
return !ALLOW_SYMLINK_ESCAPE;
858+
private static OpenOption[] getOpenOptions() {
859+
List<OpenOption> options = new ArrayList<>();
860+
if (!ALLOW_SYMLINK_ESCAPE) {
861+
options.add(LinkOption.NOFOLLOW_LINKS);
862+
}
863+
if (!ALLOW_TMP_DISPLAY) {
864+
options.add(FilePath.DisplayOption.IGNORE_TMP_DIRS);
865+
}
866+
return options.toArray(new OpenOption[0]);
842867
}
843868

844869
private static final Logger LOGGER = Logger.getLogger(DirectoryBrowserSupport.class.getName());

core/src/main/java/hudson/slaves/WorkspaceList.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ public void release() {
310310
*/
311311
@CheckForNull
312312
public static FilePath tempDir(FilePath ws) {
313-
return ws.sibling(ws.getName() + COMBINATOR + "tmp");
313+
return ws.sibling(ws.getName() + TMP_DIR_SUFFIX);
314314
}
315315

316316
private static final Logger LOGGER = Logger.getLogger(WorkspaceList.class.getName());
@@ -320,4 +320,9 @@ public static FilePath tempDir(FilePath ws) {
320320
* @since 2.244
321321
*/
322322
public static final String COMBINATOR = SystemProperties.getString(WorkspaceList.class.getName(), "@");
323+
324+
/**
325+
* Suffix for temporary folders.
326+
*/
327+
public static final String TMP_DIR_SUFFIX = COMBINATOR + "tmp";
323328
}

core/src/main/java/hudson/util/DirScanner.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.io.FileFilter;
99
import java.io.IOException;
1010
import java.io.Serializable;
11+
import java.nio.file.OpenOption;
1112
import java.util.HashSet;
1213
import java.util.Set;
1314
import org.apache.tools.ant.BuildException;
@@ -106,25 +107,25 @@ public static class Glob extends DirScanner {
106107
private final String includes, excludes;
107108

108109
private boolean useDefaultExcludes = true;
109-
private final boolean followSymlinks;
110+
private OpenOption[] openOptions;
110111

111112
public Glob(String includes, String excludes) {
112-
this(includes, excludes, true, true);
113+
this(includes, excludes, true, new OpenOption[0]);
113114
}
114115

115116
public Glob(String includes, String excludes, boolean useDefaultExcludes) {
116-
this(includes, excludes, useDefaultExcludes, true);
117+
this(includes, excludes, useDefaultExcludes, new OpenOption[0]);
117118
}
118119

119120
/**
120121
* @since 2.275 and 2.263.2
121122
*/
122123
@Restricted(NoExternalUse.class)
123-
public Glob(String includes, String excludes, boolean useDefaultExcludes, boolean followSymlinks) {
124+
public Glob(String includes, String excludes, boolean useDefaultExcludes, OpenOption... openOptions) {
124125
this.includes = includes;
125126
this.excludes = excludes;
126127
this.useDefaultExcludes = useDefaultExcludes;
127-
this.followSymlinks = followSymlinks;
128+
this.openOptions = openOptions;
128129
}
129130

130131
@Override
@@ -136,7 +137,7 @@ public void scan(File dir, FileVisitor visitor) throws IOException {
136137
}
137138

138139
FileSet fs = Util.createFileSet(dir, includes, excludes);
139-
fs.setFollowSymlinks(followSymlinks);
140+
fs.setFollowSymlinks(!FilePath.isNoFollowLink(openOptions));
140141
fs.setDefaultexcludes(useDefaultExcludes);
141142

142143
if (dir.exists()) {

core/src/main/java/hudson/util/io/ArchiverFactory.java

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.io.IOException;
3131
import java.io.OutputStream;
3232
import java.io.Serializable;
33+
import java.nio.file.OpenOption;
3334
import org.kohsuke.accmod.Restricted;
3435
import org.kohsuke.accmod.restrictions.NoExternalUse;
3536

@@ -65,13 +66,14 @@ public abstract class ArchiverFactory implements Serializable {
6566
public static ArchiverFactory ZIP = new ZipArchiverFactory();
6667

6768
/**
68-
* Zip format, without following symlinks.
69+
* Zip format, with prefix and optional OpenOptions.
6970
* @param prefix The portion of file path that will be added at the beginning of the relative path inside the archive.
7071
* If non-empty, a trailing forward slash will be enforced.
72+
* @param openOptions the options to apply when opening files.
7173
*/
7274
@Restricted(NoExternalUse.class)
73-
public static ArchiverFactory createZipWithoutSymlink(String prefix) {
74-
return new ZipWithoutSymLinksArchiverFactory(prefix);
75+
public static ArchiverFactory createZipWithPrefix(String prefix, OpenOption... openOptions) {
76+
return new ZipArchiverFactory(prefix, openOptions);
7577
}
7678

7779
private static final class TarArchiverFactory extends ArchiverFactory {
@@ -91,26 +93,23 @@ public Archiver create(OutputStream out) throws IOException {
9193
}
9294

9395
private static final class ZipArchiverFactory extends ArchiverFactory {
94-
@NonNull
95-
@Override
96-
public Archiver create(OutputStream out) {
97-
return new ZipArchiver(out);
98-
}
9996

100-
private static final long serialVersionUID = 1L;
101-
}
102-
103-
private static final class ZipWithoutSymLinksArchiverFactory extends ArchiverFactory {
10497
private final String prefix;
98+
private final OpenOption[] openOptions;
99+
100+
ZipArchiverFactory() {
101+
this(null);
102+
}
105103

106-
ZipWithoutSymLinksArchiverFactory(String prefix) {
104+
ZipArchiverFactory(String prefix, OpenOption... openOptions) {
107105
this.prefix = prefix;
106+
this.openOptions = openOptions;
108107
}
109108

110109
@NonNull
111110
@Override
112111
public Archiver create(OutputStream out) {
113-
return new ZipArchiver(out, true, prefix);
112+
return new ZipArchiver(out, prefix, openOptions);
114113
}
115114

116115
private static final long serialVersionUID = 1L;

core/src/main/java/hudson/util/io/ZipArchiver.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
package hudson.util.io;
2626

27+
import hudson.FilePath;
2728
import hudson.Util;
2829
import hudson.util.FileVisitor;
2930
import hudson.util.IOUtils;
@@ -33,7 +34,6 @@
3334
import java.io.OutputStream;
3435
import java.nio.file.Files;
3536
import java.nio.file.InvalidPathException;
36-
import java.nio.file.LinkOption;
3737
import java.nio.file.OpenOption;
3838
import java.nio.file.attribute.BasicFileAttributes;
3939
import org.apache.commons.lang.StringUtils;
@@ -55,20 +55,20 @@ final class ZipArchiver extends Archiver {
5555
private final String prefix;
5656

5757
ZipArchiver(OutputStream out) {
58-
this(out, false, "");
58+
this(out, "");
5959
}
6060

6161
// Restriction added for clarity, it's a package class, you should not use it outside of Jenkins core
6262
@Restricted(NoExternalUse.class)
63-
ZipArchiver(OutputStream out, boolean failOnSymLink, String prefix) {
63+
ZipArchiver(OutputStream out, String prefix, OpenOption... openOptions) {
64+
this.openOptions = openOptions;
6465
if (StringUtils.isBlank(prefix)) {
6566
this.prefix = "";
6667
} else {
6768
this.prefix = Util.ensureEndsWith(prefix, "/");
6869
}
6970

7071
zip = new ZipOutputStream(out);
71-
openOptions = failOnSymLink ? new LinkOption[]{LinkOption.NOFOLLOW_LINKS} : new OpenOption[0];
7272
zip.setEncoding(System.getProperty("file.encoding"));
7373
zip.setUseZip64(Zip64Mode.AsNeeded);
7474
}
@@ -97,7 +97,7 @@ public void visit(final File f, final String _relativePath) throws IOException {
9797
fileZipEntry.setTime(basicFileAttributes.lastModifiedTime().toMillis());
9898
fileZipEntry.setSize(basicFileAttributes.size());
9999
zip.putNextEntry(fileZipEntry);
100-
try (InputStream in = Files.newInputStream(f.toPath(), openOptions)) {
100+
try (InputStream in = FilePath.openInputStream(f, openOptions)) {
101101
int len;
102102
while ((len = in.read(buf)) >= 0)
103103
zip.write(buf, 0, len);

0 commit comments

Comments
 (0)