Skip to content

Commit 349d4fc

Browse files
authored
[JENKINS-73161] Ensure file parameters are retained across Jenkins restarts (#11081)
2 parents a68faf9 + ab342c0 commit 349d4fc

File tree

4 files changed

+190
-90
lines changed

4 files changed

+190
-90
lines changed

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

Lines changed: 98 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626

2727
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
2828
import hudson.EnvVars;
29+
import hudson.Extension;
2930
import hudson.FilePath;
3031
import hudson.Launcher;
3132
import hudson.Util;
33+
import hudson.model.queue.QueueListener;
3234
import hudson.tasks.BuildWrapper;
3335
import hudson.util.VariableResolver;
3436
import java.io.File;
@@ -39,12 +41,17 @@
3941
import java.nio.charset.Charset;
4042
import java.nio.file.Files;
4143
import java.nio.file.Path;
44+
import java.util.List;
45+
import java.util.logging.Level;
46+
import java.util.logging.Logger;
4247
import java.util.regex.Pattern;
48+
import jenkins.model.Jenkins;
4349
import jenkins.util.SystemProperties;
4450
import org.apache.commons.fileupload2.core.FileItem;
4551
import org.apache.commons.fileupload2.core.FileItemFactory;
4652
import org.apache.commons.fileupload2.core.FileItemHeaders;
4753
import org.apache.commons.fileupload2.core.FileItemHeadersProvider;
54+
import org.apache.commons.io.FileUtils;
4855
import org.apache.commons.io.FilenameUtils;
4956
import org.kohsuke.accmod.Restricted;
5057
import org.kohsuke.accmod.restrictions.NoExternalUse;
@@ -58,6 +65,9 @@
5865
* @author Kohsuke Kawaguchi
5966
*/
6067
public class FileParameterValue extends ParameterValue {
68+
69+
private static final Logger LOGGER = Logger.getLogger(FileParameterValue.class.getName());
70+
6171
private static final String FOLDER_NAME = "fileParameters";
6272
private static final Pattern PROHIBITED_DOUBLE_DOT = Pattern.compile(".*[\\\\/]\\.\\.[\\\\/].*");
6373
private static final long serialVersionUID = -143427023159076073L;
@@ -71,13 +81,16 @@ public class FileParameterValue extends ParameterValue {
7181
public static /* Script Console modifiable */ boolean ALLOW_FOLDER_TRAVERSAL_OUTSIDE_WORKSPACE =
7282
SystemProperties.getBoolean(FileParameterValue.class.getName() + ".allowFolderTraversalOutsideWorkspace");
7383

74-
private final transient FileItem file;
84+
private transient FileItem file;
7585

7686
/**
7787
* The name of the originally uploaded file.
7888
*/
7989
private final String originalFileName;
8090

91+
92+
private String tmpFileName;
93+
8194
/**
8295
* Overrides the location in the build to place this file. Initially set to {@link #getName()}
8396
* The location could be directly the filename or also a hierarchical path.
@@ -106,6 +119,16 @@ public FileParameterValue(String name, File file, String originalFileName) {
106119

107120
protected FileParameterValue(String name, FileItem file, String originalFileName) {
108121
super(name);
122+
try {
123+
File dir = new File(Jenkins.get().getRootDir(), "fileParameterValueFiles");
124+
Files.createDirectories(dir.toPath());
125+
File tmp = Files.createTempFile(dir.toPath(), "jenkins", ".tmp").toFile();
126+
FileUtils.copyInputStreamToFile(file.getInputStream(), tmp);
127+
tmpFileName = tmp.getAbsolutePath();
128+
} catch (IOException e) {
129+
throw new RuntimeException(e);
130+
}
131+
109132
this.file = file;
110133
this.originalFileName = originalFileName;
111134
setLocation(name);
@@ -149,7 +172,17 @@ public String getOriginalFileName() {
149172
return originalFileName;
150173
}
151174

175+
private void createFile() {
176+
if (file == null && tmpFileName != null) {
177+
File tmp = new File(tmpFileName);
178+
if (tmp.exists()) {
179+
file = new FileItemImpl2(tmp);
180+
}
181+
}
182+
}
183+
152184
public FileItem getFile2() {
185+
createFile();
153186
return file;
154187
}
155188

@@ -163,31 +196,44 @@ public org.apache.commons.fileupload.FileItem getFile() {
163196

164197
@Override
165198
public BuildWrapper createBuildWrapper(AbstractBuild<?, ?> build) {
199+
createFile();
166200
return new BuildWrapper() {
167-
@SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "TODO needs triage")
201+
@SuppressFBWarnings(value = {"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", "PATH_TRAVERSAL_IN"}, justification = "TODO needs triage, False positive, the path is a temporary file")
168202
@Override
169203
public Environment setUp(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
170-
if (location != null && !location.isEmpty() && file.getName() != null && !file.getName().isEmpty()) {
171-
listener.getLogger().println("Copying file to " + location);
172-
FilePath ws = build.getWorkspace();
173-
if (ws == null) {
174-
throw new IllegalStateException("The workspace should be created when setUp method is called");
175-
}
176-
if (!ALLOW_FOLDER_TRAVERSAL_OUTSIDE_WORKSPACE && (PROHIBITED_DOUBLE_DOT.matcher(location).matches() || !ws.isDescendant(location))) {
177-
listener.error("Rejecting file path escaping base directory with relative path: " + location);
178-
// force the build to fail
179-
return null;
180-
}
181-
FilePath locationFilePath = ws.child(location);
182-
locationFilePath.getParent().mkdirs();
183-
184-
// TODO Remove this workaround after FILEUPLOAD-293 is resolved.
185-
if (locationFilePath.exists() && !locationFilePath.isDirectory()) {
186-
locationFilePath.delete();
204+
if (location != null && !location.isBlank() && file != null && file.getName() != null && !file.getName().isBlank()) {
205+
try {
206+
listener.getLogger().println("Copying file to " + location);
207+
FilePath ws = build.getWorkspace();
208+
if (ws == null) {
209+
throw new IllegalStateException("The workspace should be created when setUp method is called");
210+
}
211+
if (!ALLOW_FOLDER_TRAVERSAL_OUTSIDE_WORKSPACE && (PROHIBITED_DOUBLE_DOT.matcher(location).matches() || !ws.isDescendant(location))) {
212+
listener.error("Rejecting file path escaping base directory with relative path: " + location);
213+
// force the build to fail
214+
return null;
215+
}
216+
FilePath locationFilePath = ws.child(location);
217+
locationFilePath.getParent().mkdirs();
218+
219+
// TODO Remove this workaround after FILEUPLOAD-293 is resolved.
220+
if (locationFilePath.exists() && !locationFilePath.isDirectory()) {
221+
locationFilePath.delete();
222+
}
223+
locationFilePath.copyFrom(file);
224+
locationFilePath.copyTo(new FilePath(getLocationUnderBuild(build)));
225+
} finally {
226+
if (tmpFileName != null) {
227+
File tmp = new File(tmpFileName);
228+
try {
229+
Files.deleteIfExists(tmp.toPath());
230+
} catch (IOException e) {
231+
LOGGER.log(Level.WARNING, "Unable to delete temporary file {0} for parameter {1} of job {2}",
232+
new Object[]{tmp.getAbsolutePath(), getName(), build.getParent().getName()});
233+
}
234+
}
235+
tmpFileName = null;
187236
}
188-
189-
locationFilePath.copyFrom(file);
190-
locationFilePath.copyTo(new FilePath(getLocationUnderBuild(build)));
191237
}
192238
return new Environment() {};
193239
}
@@ -257,6 +303,36 @@ private File getFileParameterFolderUnderBuild(AbstractBuild<?, ?> build) {
257303
return new File(build.getRootDir(), FOLDER_NAME);
258304
}
259305

306+
@Extension
307+
public static class CancelledQueueListener extends QueueListener {
308+
309+
@Override
310+
public void onLeft(Queue.LeftItem li) {
311+
if (li.isCancelled()) {
312+
List<ParametersAction> actions = li.getActions(ParametersAction.class);
313+
actions.forEach(a -> {
314+
a.getAllParameters().stream()
315+
.filter(p -> p instanceof FileParameterValue)
316+
.map(p -> (FileParameterValue) p)
317+
.forEach(this::deleteTmpFile);
318+
});
319+
}
320+
}
321+
322+
@SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "False positive, the path is a temporary file")
323+
private void deleteTmpFile(FileParameterValue p) {
324+
if (p.tmpFileName != null) {
325+
File tmp = new File(p.tmpFileName);
326+
try {
327+
Files.deleteIfExists(tmp.toPath());
328+
} catch (IOException e) {
329+
LOGGER.log(Level.WARNING, "Unable to delete temporary file {0} for parameter {1}",
330+
new Object[]{tmp.getAbsolutePath(), p.getName()});
331+
}
332+
}
333+
}
334+
}
335+
260336
/**
261337
* Default implementation from {@link File}.
262338
*

core/src/test/java/hudson/model/FileParameterValueTest.java

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

test/src/test/java/hudson/model/FileParameterValuePersistenceTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,34 @@
55
import static org.junit.jupiter.api.Assertions.assertEquals;
66
import static org.junit.jupiter.api.Assertions.assertTrue;
77

8+
import hudson.ExtensionList;
89
import hudson.Functions;
10+
import hudson.model.queue.CauseOfBlockage;
11+
import hudson.model.queue.QueueTaskDispatcher;
912
import hudson.tasks.BatchFile;
1013
import hudson.tasks.Shell;
1114
import java.io.File;
15+
import java.net.URL;
1216
import java.nio.charset.StandardCharsets;
1317
import java.nio.file.Files;
1418
import java.nio.file.Path;
19+
import java.util.Collections;
20+
import jenkins.model.Jenkins;
21+
import org.apache.commons.io.FileUtils;
22+
import org.htmlunit.FormEncodingType;
23+
import org.htmlunit.HttpMethod;
24+
import org.htmlunit.WebRequest;
1525
import org.htmlunit.html.HtmlForm;
1626
import org.htmlunit.html.HtmlInput;
1727
import org.htmlunit.html.HtmlPage;
28+
import org.htmlunit.util.KeyDataPair;
1829
import org.junit.jupiter.api.Test;
1930
import org.junit.jupiter.api.extension.RegisterExtension;
2031
import org.junit.jupiter.api.io.TempDir;
2132
import org.jvnet.hudson.test.Issue;
2233
import org.jvnet.hudson.test.JenkinsRule;
34+
import org.jvnet.hudson.test.MockAuthorizationStrategy;
35+
import org.jvnet.hudson.test.TestExtension;
2336
import org.jvnet.hudson.test.junit.jupiter.JenkinsSessionExtension;
2437

2538
class FileParameterValuePersistenceTest {
@@ -78,4 +91,39 @@ private static void verifyPersistence(JenkinsRule j) throws Throwable {
7891
assertThat(page.getWebResponse().getContentAsString(), containsString(FILENAME));
7992
}
8093
}
94+
95+
@Issue("JENKINS-73161")
96+
@Test
97+
void fileParameterValueIsRetained() throws Throwable {
98+
sessions.then(r -> {
99+
r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
100+
r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("admin"));
101+
FreeStyleProject p = r.createFreeStyleProject("p");
102+
p.addProperty(new ParametersDefinitionProperty(new FileParameterDefinition("FILE")));
103+
WebRequest req = new WebRequest(new URL(r.getURL() + "job/p/buildWithParameters"), HttpMethod.POST);
104+
File f = File.createTempFile("junit", null, tmp);
105+
FileUtils.write(f, "uploaded content here", "UTF-8");
106+
req.setEncodingType(FormEncodingType.MULTIPART);
107+
req.setRequestParameters(Collections.singletonList(new KeyDataPair("FILE", f, "myfile.txt", "text/plain", "UTF-8")));
108+
r.createWebClient().withBasicApiToken("admin").getPage(req);
109+
});
110+
sessions.then(r -> {
111+
ExtensionList.lookupSingleton(Block.class).ready = true;
112+
FreeStyleProject p = r.jenkins.getItemByFullName("p", FreeStyleProject.class);
113+
r.waitUntilNoActivity();
114+
FreeStyleBuild b = p.getBuildByNumber(1);
115+
r.assertBuildStatusSuccess(b);
116+
});
117+
}
118+
119+
@TestExtension("fileParameterValueIsRetained")
120+
public static final class Block extends QueueTaskDispatcher {
121+
private boolean ready;
122+
123+
@Override
124+
public CauseOfBlockage canTake(Node node, Queue.BuildableItem item) {
125+
return ready ? null : new CauseOfBlockage.BecauseNodeIsBusy(node);
126+
}
127+
}
128+
81129
}

0 commit comments

Comments
 (0)