|
46 | 46 | import java.io.File;
|
47 | 47 | import java.io.FileNotFoundException;
|
48 | 48 | import java.io.IOException;
|
| 49 | +import java.io.PrintWriter; |
| 50 | +import java.io.StringWriter; |
49 | 51 | import java.nio.charset.StandardCharsets;
|
50 | 52 | import java.nio.file.NoSuchFileException;
|
51 | 53 | import java.util.Collections;
|
52 | 54 | import java.util.Set;
|
53 | 55 | import java.util.TreeSet;
|
| 56 | +import java.util.stream.Collectors; |
| 57 | + |
54 | 58 | import jenkins.security.QueueItemAuthenticator;
|
55 | 59 | import jenkins.security.QueueItemAuthenticatorConfiguration;
|
56 | 60 | import org.apache.commons.io.FileUtils;
|
57 | 61 | import static org.hamcrest.MatcherAssert.assertThat;
|
| 62 | +import static org.hamcrest.Matchers.containsString; |
58 | 63 | import static org.hamcrest.Matchers.equalTo;
|
59 | 64 | import static org.hamcrest.Matchers.hasItem;
|
60 | 65 | import static org.hamcrest.Matchers.is;
|
| 66 | +import static org.hamcrest.Matchers.not; |
61 | 67 | import static org.hamcrest.Matchers.notNullValue;
|
62 | 68 | import static org.hamcrest.Matchers.nullValue;
|
63 | 69 | import org.jenkinsci.plugins.plaincredentials.StringCredentials;
|
64 | 70 | import org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImpl;
|
65 | 71 | import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl;
|
66 | 72 | import org.jenkinsci.plugins.workflow.actions.ArgumentsAction;
|
| 73 | +import org.jenkinsci.plugins.workflow.actions.ErrorAction; |
67 | 74 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
|
68 | 75 | import org.jenkinsci.plugins.workflow.cps.SnippetizerTester;
|
69 | 76 | import org.jenkinsci.plugins.workflow.flow.FlowExecution;
|
@@ -460,6 +467,103 @@ public void usernameUnmaskedInStepArguments() throws Throwable {
|
460 | 467 | r.assertLogContains("got: P#1", r.assertBuildStatusSuccess(p.scheduleBuild2(0).get()));
|
461 | 468 | });
|
462 | 469 | }
|
| 470 | + |
| 471 | + @Issue("SECURITY-3499") |
| 472 | + @Test public void maskingExceptionInError() throws Throwable { |
| 473 | + rr.then(r -> { |
| 474 | + CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), new StringCredentialsImpl(CredentialsScope.GLOBAL, "creds", "sample", Secret.fromString("s3cr3t"))); |
| 475 | + var p = r.jenkins.createProject(WorkflowJob.class, "p"); |
| 476 | + p.setDefinition(new CpsFlowDefinition( |
| 477 | + """ |
| 478 | + withCredentials([string(credentialsId: 'creds', variable: 'SECRET')]) { |
| 479 | + error(/should not mention $SECRET/) |
| 480 | + } |
| 481 | + """, true)); |
| 482 | + var b = r.buildAndAssertStatus(Result.FAILURE, p); |
| 483 | + r.assertLogNotContains("s3cr3t", b); |
| 484 | + r.assertLogContains("should not mention ****", b); |
| 485 | + assertErrorActionsDoNotContainString(b, "s3cr3t"); |
| 486 | + }); |
| 487 | + } |
| 488 | + |
| 489 | + @Issue("SECURITY-3499") |
| 490 | + @Test public void maskingExceptionInNestedError() throws Throwable { |
| 491 | + rr.then(r -> { |
| 492 | + CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), new StringCredentialsImpl(CredentialsScope.GLOBAL, "creds", "sample", Secret.fromString("s3cr3t"))); |
| 493 | + var p = r.jenkins.createProject(WorkflowJob.class, "p"); |
| 494 | + p.setDefinition(new CpsFlowDefinition( |
| 495 | + """ |
| 496 | + withCredentials([string(credentialsId: 'creds', variable: 'SECRET')]) { |
| 497 | + try { |
| 498 | + error(/nested exception should not mention $SECRET/) |
| 499 | + } |
| 500 | + catch (err) { |
| 501 | + throw new Exception("normal exception should not mention $SECRET", err) |
| 502 | + } |
| 503 | + } |
| 504 | + """, false)); |
| 505 | + var b = r.buildAndAssertStatus(Result.FAILURE, p); |
| 506 | + r.assertLogNotContains("s3cr3t", b); |
| 507 | + r.assertLogContains("nested exception should not mention ****", b); |
| 508 | + r.assertLogContains("normal exception should not mention ****", b); |
| 509 | + assertErrorActionsDoNotContainString(b, "s3cr3t"); |
| 510 | + }); |
| 511 | + } |
| 512 | + |
| 513 | + @Issue("SECURITY-3499") |
| 514 | + @Test public void maskingExceptionInSuppressedError() throws Throwable { |
| 515 | + rr.then(r -> { |
| 516 | + CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), new StringCredentialsImpl(CredentialsScope.GLOBAL, "creds", "sample", Secret.fromString("s3cr3t"))); |
| 517 | + var p = r.jenkins.createProject(WorkflowJob.class, "p"); |
| 518 | + p.setDefinition(new CpsFlowDefinition( |
| 519 | + """ |
| 520 | + withCredentials([string(credentialsId: 'creds', variable: 'SECRET')]) { |
| 521 | + def main = new Exception("normal exception should not mention $SECRET") |
| 522 | + main.addSuppressed(new Exception("suppressed exception should not mention $SECRET")) |
| 523 | + throw main |
| 524 | + } |
| 525 | + """, false)); |
| 526 | + var b = r.buildAndAssertStatus(Result.FAILURE, p); |
| 527 | + r.assertLogNotContains("s3cr3t", b); |
| 528 | + r.assertLogContains("suppressed exception should not mention ****", b); |
| 529 | + r.assertLogContains("normal exception should not mention ****", b); |
| 530 | + assertErrorActionsDoNotContainString(b, "s3cr3t"); |
| 531 | + }); |
| 532 | + } |
| 533 | + |
| 534 | + @Issue("SECURITY-3499") |
| 535 | + @Test public void maskingExceptionForIncorrectArgs() throws Throwable { |
| 536 | + rr.then(r -> { |
| 537 | + CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), new StringCredentialsImpl(CredentialsScope.GLOBAL, "creds", "sample", Secret.fromString("s3cr3t"))); |
| 538 | + var p = r.jenkins.createProject(WorkflowJob.class, "p"); |
| 539 | + p.setDefinition(new CpsFlowDefinition( |
| 540 | + """ |
| 541 | + withCredentials([string(credentialsId: 'creds', variable: 'SECRET')]) { |
| 542 | + writeFile([file: "creds.txt", text: "${SECRET}", encoding: 'Base64']) { |
| 543 | + } |
| 544 | + } |
| 545 | + """, false)); |
| 546 | + var b = r.buildAndAssertStatus(Result.FAILURE, p); |
| 547 | + r.assertLogNotContains("s3cr3t", b); |
| 548 | + r.assertLogContains("text=****", b); |
| 549 | + assertErrorActionsDoNotContainString(b, "s3cr3t"); |
| 550 | + }); |
| 551 | + } |
| 552 | + |
| 553 | + private void assertErrorActionsDoNotContainString(WorkflowRun b, String needle) { |
| 554 | + var errorActionStackTraces = new DepthFirstScanner().allNodes(b.getExecution()).stream() |
| 555 | + .map(n -> n.getPersistentAction(ErrorAction.class)) |
| 556 | + .filter(ea -> ea != null) |
| 557 | + .map(ea -> ea.getError()) |
| 558 | + .map(t -> { |
| 559 | + var writer = new StringWriter(); |
| 560 | + t.printStackTrace(new PrintWriter(writer)); |
| 561 | + return writer.toString(); |
| 562 | + }) |
| 563 | + .collect(Collectors.toList()); |
| 564 | + assertThat(errorActionStackTraces, not(hasItem(containsString(needle)))); |
| 565 | + } |
| 566 | + |
463 | 567 | private static final class SpecialCredentials extends BaseStandardCredentials implements StringCredentials {
|
464 | 568 | private Run<?, ?> build;
|
465 | 569 | SpecialCredentials(CredentialsScope scope, String id, String description) {
|
|
0 commit comments