|
24 | 24 |
|
25 | 25 | package org.jenkinsci.plugins.workflow.multibranch;
|
26 | 26 |
|
| 27 | +import hudson.Functions; |
27 | 28 | import hudson.model.Result;
|
| 29 | +import hudson.scm.SubversionSCM; |
| 30 | +import java.io.File; |
| 31 | +import java.nio.charset.StandardCharsets; |
28 | 32 | import jenkins.branch.BranchSource;
|
29 | 33 | import jenkins.plugins.git.GitSampleRepoRule;
|
30 | 34 | import jenkins.plugins.git.GitStep;
|
|
40 | 44 | import org.jvnet.hudson.test.Issue;
|
41 | 45 | import org.jvnet.hudson.test.JenkinsRule;
|
42 | 46 |
|
| 47 | +import java.nio.file.Files; |
| 48 | +import java.nio.file.Path; |
| 49 | +import java.nio.file.Paths; |
| 50 | +import jenkins.plugins.git.GitSCMSource; |
| 51 | +import jenkins.scm.impl.subversion.SubversionSCMSource; |
| 52 | +import jenkins.scm.impl.subversion.SubversionSampleRepoRule; |
| 53 | +import org.apache.commons.io.FileUtils; |
| 54 | +import org.junit.Ignore; |
| 55 | +import org.jvnet.hudson.test.FlagRule; |
| 56 | + |
| 57 | +import static org.hamcrest.MatcherAssert.assertThat; |
| 58 | +import static org.hamcrest.Matchers.equalTo; |
| 59 | +import static org.hamcrest.Matchers.not; |
| 60 | +import static org.hamcrest.io.FileMatchers.anExistingFile; |
| 61 | +import static org.junit.Assume.assumeFalse; |
| 62 | + |
43 | 63 | public class ReadTrustedStepTest {
|
44 | 64 |
|
45 | 65 | @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher();
|
46 | 66 | @Rule public JenkinsRule r = new JenkinsRule();
|
47 | 67 | @Rule public GitSampleRepoRule sampleRepo = new GitSampleRepoRule();
|
| 68 | + @Rule public SubversionSampleRepoRule sampleRepoSvn = new SubversionSampleRepoRule(); |
| 69 | + @Rule public FlagRule<Boolean> heavyweightCheckoutFlag = new FlagRule<>(() -> SCMBinder.USE_HEAVYWEIGHT_CHECKOUT, v -> { SCMBinder.USE_HEAVYWEIGHT_CHECKOUT = v; }); |
48 | 70 |
|
49 | 71 | @Test public void smokes() throws Exception {
|
50 | 72 | sampleRepo.init();
|
@@ -193,4 +215,157 @@ public class ReadTrustedStepTest {
|
193 | 215 | }
|
194 | 216 | }
|
195 | 217 |
|
| 218 | + @Test |
| 219 | + public void pathTraversalRejected() throws Exception { |
| 220 | + SCMBinder.USE_HEAVYWEIGHT_CHECKOUT = true; |
| 221 | + sampleRepo.init(); |
| 222 | + sampleRepo.write("Jenkinsfile", "node { checkout scm; echo \"${readTrusted '../../secrets/master.key'}\"}"); |
| 223 | + Path secrets = Paths.get(sampleRepo.getRoot().getPath(), "secrets"); |
| 224 | + Files.createSymbolicLink(secrets, Paths.get(r.jenkins.getRootDir() + "/secrets")); |
| 225 | + sampleRepo.git("add", "."); |
| 226 | + sampleRepo.git("commit", "-m", "init"); |
| 227 | + |
| 228 | + WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); |
| 229 | + mp.getSourcesList().add(new BranchSource(new SCMBinderTest.WarySource(null, sampleRepo.toString(), "", "*", "", false))); |
| 230 | + WorkflowJob p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "master"); |
| 231 | + r.waitUntilNoActivity(); |
| 232 | + |
| 233 | + WorkflowRun b = p.getLastBuild(); |
| 234 | + assertEquals(1, b.getNumber()); |
| 235 | + r.assertLogContains("secrets/master.key references a file that is not inside " + r.jenkins.getWorkspaceFor(p).getRemote(), b); |
| 236 | + } |
| 237 | + |
| 238 | + @Issue("SECURITY-2491") |
| 239 | + @Test |
| 240 | + public void symlinksInReadTrustedCannotEscapeWorkspaceContext() throws Exception { |
| 241 | + SCMBinder.USE_HEAVYWEIGHT_CHECKOUT = true; |
| 242 | + sampleRepo.init(); |
| 243 | + sampleRepo.write("Jenkinsfile", "node { checkout scm; echo \"${readTrusted 'secrets/master.key'}\"}"); |
| 244 | + Path secrets = Paths.get(sampleRepo.getRoot().getPath(), "secrets"); |
| 245 | + Files.createSymbolicLink(secrets, Paths.get(r.jenkins.getRootDir() + "/secrets")); |
| 246 | + sampleRepo.git("add", "."); |
| 247 | + sampleRepo.git("commit", "-m", "init"); |
| 248 | + |
| 249 | + WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); |
| 250 | + mp.getSourcesList().add(new BranchSource(new SCMBinderTest.WarySource(null, sampleRepo.toString(), "", "*", "", false))); |
| 251 | + WorkflowJob p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "master"); |
| 252 | + r.waitUntilNoActivity(); |
| 253 | + |
| 254 | + WorkflowRun run = p.getLastBuild(); |
| 255 | + assertEquals(1, run.getNumber()); |
| 256 | + r.assertLogContains("secrets/master.key references a file that is not inside " + r.jenkins.getWorkspaceFor(p).getRemote(), run); |
| 257 | + } |
| 258 | + |
| 259 | + @Issue("SECURITY-2491") |
| 260 | + @Test |
| 261 | + public void symlinksInUntrustedRevisionCannotEscapeWorkspace() throws Exception { |
| 262 | + SCMBinder.USE_HEAVYWEIGHT_CHECKOUT = true; |
| 263 | + sampleRepo.init(); |
| 264 | + sampleRepo.write("Jenkinsfile", "node { checkout scm; echo \"${readTrusted 'secrets/master.key'}\"}"); |
| 265 | + sampleRepo.write("secrets/master.key", "secret info"); |
| 266 | + sampleRepo.git("add", "."); |
| 267 | + sampleRepo.git("commit", "-m", "init"); |
| 268 | + sampleRepo.git("checkout", "-b", "feature"); |
| 269 | + Path secrets = Paths.get(sampleRepo.getRoot().getPath(), "secrets"); |
| 270 | + Files.delete(Paths.get(secrets.toString(), "master.key")); |
| 271 | + Files.delete(secrets); |
| 272 | + Files.createSymbolicLink(secrets, Paths.get(r.jenkins.getRootDir() + "/secrets")); |
| 273 | + sampleRepo.git("add", "."); |
| 274 | + sampleRepo.git("commit", "-m", "now with unsafe symlink"); |
| 275 | + |
| 276 | + WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); |
| 277 | + mp.getSourcesList().add(new BranchSource(new SCMBinderTest.WarySource(null, sampleRepo.toString(), "", "*", "", false))); |
| 278 | + WorkflowJob p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "feature"); |
| 279 | + r.waitUntilNoActivity(); |
| 280 | + |
| 281 | + WorkflowRun run = p.getLastBuild(); |
| 282 | + assertEquals(1, run.getNumber()); |
| 283 | + r.assertLogContains("secrets/master.key references a file that is not inside ", run); |
| 284 | + } |
| 285 | + |
| 286 | + @Issue("SECURITY-2491") |
| 287 | + @Test |
| 288 | + public void symlinksInNonMultibranchCannotEscapeWorkspaceContextViaReadTrusted() throws Exception { |
| 289 | + SCMBinder.USE_HEAVYWEIGHT_CHECKOUT = true; |
| 290 | + sampleRepo.init(); |
| 291 | + sampleRepo.write("Jenkinsfile", "echo \"${readTrusted 'master.key'}\""); |
| 292 | + Path secrets = Paths.get(sampleRepo.getRoot().getPath(), "master.key"); |
| 293 | + Files.createSymbolicLink(secrets, Paths.get(r.jenkins.getRootDir() + "/secrets/master.key")); |
| 294 | + sampleRepo.git("add", "."); |
| 295 | + sampleRepo.git("commit", "-m", "init"); |
| 296 | + |
| 297 | + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p"); |
| 298 | + GitStep step = new GitStep(sampleRepo.toString()); |
| 299 | + p.setDefinition(new CpsScmFlowDefinition(step.createSCM(), "Jenkinsfile")); |
| 300 | + WorkflowRun run = r.buildAndAssertStatus(Result.FAILURE, p); |
| 301 | + |
| 302 | + r.assertLogContains("master.key references a file that is not inside " + r.jenkins.getWorkspaceFor(p), run); |
| 303 | + } |
| 304 | + |
| 305 | + @Ignore("There are two checkouts, one from CpsScmFlowDefinition via SCMBinder and one from ReadTrustedStep. Fixing the former requires an updated version of workflow-cps.") |
| 306 | + @Issue("SECURITY-2463") |
| 307 | + @Test public void multibranchCheckoutDirectoriesAreNotReusedByDifferentScms() throws Exception { |
| 308 | + SCMBinder.USE_HEAVYWEIGHT_CHECKOUT = true; |
| 309 | + assumeFalse(Functions.isWindows()); // Checkout hook is not cross-platform. |
| 310 | + sampleRepo.init(); |
| 311 | + sampleRepo.git("checkout", "-b", "trunk"); // So we end up using the same project for both SCMs. |
| 312 | + sampleRepo.write("Jenkinsfile", "echo('git library'); readTrusted('Jenkinsfile')"); |
| 313 | + sampleRepo.git("add", "Jenkinsfile"); |
| 314 | + sampleRepo.git("commit", "--message=init"); |
| 315 | + sampleRepoSvn.init(); |
| 316 | + sampleRepoSvn.write("Jenkinsfile", "echo('svn library'); readTrusted('Jenkinsfile')"); |
| 317 | + // Copy .git folder from the Git repo into the SVN repo as data. |
| 318 | + File gitDirInSvnRepo = new File(sampleRepoSvn.wc(), ".git"); |
| 319 | + FileUtils.copyDirectory(new File(sampleRepo.getRoot(), ".git"), gitDirInSvnRepo); |
| 320 | + String jenkinsRootDir = r.jenkins.getRootDir().toString(); |
| 321 | + // Add a Git post-checkout hook to the .git folder in the SVN repo. |
| 322 | + Files.write(gitDirInSvnRepo.toPath().resolve("hooks/post-checkout"), ("#!/bin/sh\ntouch '" + jenkinsRootDir + "/hook-executed'\n").getBytes(StandardCharsets.UTF_8)); |
| 323 | + sampleRepoSvn.svnkit("add", sampleRepoSvn.wc() + "/Jenkinsfile"); |
| 324 | + sampleRepoSvn.svnkit("add", sampleRepoSvn.wc() + "/.git"); |
| 325 | + sampleRepoSvn.svnkit("propset", "svn:executable", "ON", sampleRepoSvn.wc() + "/.git/hooks/post-checkout"); |
| 326 | + sampleRepoSvn.svnkit("commit", "--message=init", sampleRepoSvn.wc()); |
| 327 | + // Run a build using the SVN repo. |
| 328 | + WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "p"); |
| 329 | + mp.getSourcesList().add(new BranchSource(new SubversionSCMSource("", sampleRepoSvn.prjUrl()))); |
| 330 | + WorkflowJob p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "trunk"); |
| 331 | + r.waitUntilNoActivity(); |
| 332 | + // Run a build using the Git repo. It should be checked out to a different directory than the SVN repo. |
| 333 | + mp.getSourcesList().clear(); |
| 334 | + mp.getSourcesList().add(new BranchSource(new GitSCMSource("", sampleRepo.toString(), "", "*", "", false))); |
| 335 | + WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "trunk"); |
| 336 | + r.waitUntilNoActivity(); |
| 337 | + assertThat(p.getLastBuild().getNumber(), equalTo(2)); |
| 338 | + assertThat(new File(r.jenkins.getRootDir(), "hook-executed"), not(anExistingFile())); |
| 339 | + } |
| 340 | + |
| 341 | + @Ignore("There are two checkouts, one from CpsScmFlowDefinition and one from ReadTrustedStep. Fixing the former requires an updated version of workflow-cps.") |
| 342 | + @Issue("SECURITY-2463") |
| 343 | + @Test public void checkoutDirectoriesAreNotReusedByDifferentScms() throws Exception { |
| 344 | + SCMBinder.USE_HEAVYWEIGHT_CHECKOUT = true; |
| 345 | + assumeFalse(Functions.isWindows()); // Checkout hook is not cross-platform. |
| 346 | + sampleRepo.init(); |
| 347 | + sampleRepo.write("Jenkinsfile", "echo('git library'); readTrusted('Jenkinsfile')"); |
| 348 | + sampleRepo.git("add", "Jenkinsfile"); |
| 349 | + sampleRepo.git("commit", "--message=init"); |
| 350 | + sampleRepoSvn.init(); |
| 351 | + sampleRepoSvn.write("Jenkinsfile", "echo('subversion library'); readTrusted('Jenkinsfile')"); |
| 352 | + // Copy .git folder from the Git repo into the SVN repo as data. |
| 353 | + File gitDirInSvnRepo = new File(sampleRepoSvn.wc(), ".git"); |
| 354 | + FileUtils.copyDirectory(new File(sampleRepo.getRoot(), ".git"), gitDirInSvnRepo); |
| 355 | + String jenkinsRootDir = r.jenkins.getRootDir().toString(); |
| 356 | + // Add a Git post-checkout hook to the .git folder in the SVN repo. |
| 357 | + Files.write(gitDirInSvnRepo.toPath().resolve("hooks/post-checkout"), ("#!/bin/sh\ntouch '" + jenkinsRootDir + "/hook-executed'\n").getBytes(StandardCharsets.UTF_8)); |
| 358 | + sampleRepoSvn.svnkit("add", sampleRepoSvn.wc() + "/Jenkinsfile"); |
| 359 | + sampleRepoSvn.svnkit("add", sampleRepoSvn.wc() + "/.git"); |
| 360 | + sampleRepoSvn.svnkit("propset", "svn:executable", "ON", sampleRepoSvn.wc() + "/.git/hooks/post-checkout"); |
| 361 | + sampleRepoSvn.svnkit("commit", "--message=init", sampleRepoSvn.wc()); |
| 362 | + // Run a build using the SVN repo. |
| 363 | + WorkflowJob p = r.createProject(WorkflowJob.class); |
| 364 | + p.setDefinition(new CpsScmFlowDefinition(new SubversionSCM(sampleRepoSvn.trunkUrl()), "Jenkinsfile")); |
| 365 | + r.buildAndAssertSuccess(p); |
| 366 | + // Run a build using the Git repo. It should be checked out to a different directory than the SVN repo. |
| 367 | + p.setDefinition(new CpsScmFlowDefinition(new GitStep(sampleRepo.toString()).createSCM(), "Jenkinsfile")); |
| 368 | + WorkflowRun b2 = r.buildAndAssertSuccess(p); |
| 369 | + assertThat(new File(r.jenkins.getRootDir(), "hook-executed"), not(anExistingFile())); |
| 370 | + } |
196 | 371 | }
|
0 commit comments