Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-63254][JENKINS-47101] Insecure Groovy String Interpolation Warnings #370

Merged
merged 81 commits into from
Nov 6, 2020

Conversation

car-roll
Copy link
Contributor

@car-roll car-roll commented Jul 28, 2020

JENKINS-63254

PoC version 2 for groovy string interpolation interception.

This version does not require any modifications to core. Sensitive variables are registered in BindingStep via API from EnvironmentExtender. Scanning occurs in DSL.flattenGString().

Requires:
jenkinsci/credentials-binding-plugin#105
jenkinsci/workflow-step-api-plugin#57
jenkins-infra/jenkins.io#3814

This PR also subsumes jenkinsci/workflow-durable-task-step-plugin#132

Strings that have been interpolated through DSL.flattenGstring are now tracked through DSL.parseArgs() and stored in NamedArgsAndClosure. The interpolated strings are then compared against the list of sensitive variables from EnvironmentExpander via the api established in jenkinsci/workflow-step-api-plugin#57. Interpolated string warnings are displayed in the console output as well as the build page.

Special note: changes were made to DSL.invokeDescribable, specifically when there is no associated meta-step for a symbol (i.e. when building up a nested argument for a step). Previously, the method would return an UninstantiatedDescribable and it would be instantiated later in DSL.invokeStep. Now, the UninstantiatedDescribable is packed into NamedArgsAndClosure and later unpacked when invokeStep calls parseArgs.

The changes in ArgumentsActionImpl address Jennkins-47101. It uses the new workflow-step API to identify sensitive variables. The SAFE_ENVIRONMENT_VARIABLES list has been removed and instead of checking for safe variables, we now check for sensitive variables. Sensitive variables are no longer dropped with NotStoredReason.MASKED_VALUE, but are now replaced with the variables name (e.g. "echo s3cret" becomes "echo ${PASSWORD}"). This allows us to display the signature of the steps that contain interpolation warnings.
Example screenshot here:
Screen Shot 2020-10-03 at 2 19 08 PM

@car-roll car-roll changed the title Interpolation v2 [JENKINS-63254] Insecure Groovy String Interpolation Warnings v2 Jul 31, 2020
@car-roll car-roll closed this Jul 31, 2020
@car-roll car-roll reopened this Jul 31, 2020
Copy link
Member

@dwnusbaum dwnusbaum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! Added a few comments/suggestions.

pom.xml Outdated Show resolved Hide resolved
src/main/java/org/jenkinsci/plugins/workflow/cps/DSL.java Outdated Show resolved Hide resolved
src/main/java/org/jenkinsci/plugins/workflow/cps/DSL.java Outdated Show resolved Hide resolved
src/main/java/org/jenkinsci/plugins/workflow/cps/DSL.java Outdated Show resolved Hide resolved
src/main/java/org/jenkinsci/plugins/workflow/cps/DSL.java Outdated Show resolved Hide resolved
pom.xml Outdated Show resolved Hide resolved
@jglick jglick self-requested a review August 19, 2020 16:54
@jglick
Copy link
Member

jglick commented Aug 19, 2020

From a very brief glance I suspect this is what should also be extended to finally fix JENKINS-47101.

@jglick
Copy link
Member

jglick commented Sep 4, 2020

be extended to finally fix JENKINS-47101

Let me know if you want suggestions about that. I do not think it would be out of scope: you look to be defining the same APIs which have long been proposed to clean up the way sensitive environment variables are handled in ArgumentsActionImpl, so it would be appropriate to verify that the API design is right by actually doing that cleanup. Basically, with the API in place, you should just be able to delete a bunch of tricky code and replace it with a short, obvious call.

Copy link
Member

@dwnusbaum dwnusbaum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking really close, but we need to fix the use of the system property and the accidental serialization of Run in the inner class of InterpolatedSecretsAction.

/**
* Calls {@link Step}s and other DSL objects.
*/
@PersistIn(PROGRAM)
public class DSL extends GroovyObjectSupport implements Serializable {
@SuppressFBWarnings("MS_SHOULD_BE_FINAL") // Used to control warning behavior of unsafe Groovy interpolation
@Restricted(NoExternalUse.class)
public static String UNSAFE_GROOVY_INTERPOLATION = DSL.class.getName() + ".UNSAFE_GROOVY_INTERPOLATION";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now this is just a string, you need to look up the property with this name, either here or in the places where you want to check the property's value, and I would set a default value so that there are no NPEs if the property is unset:

Suggested change
public static String UNSAFE_GROOVY_INTERPOLATION = DSL.class.getName() + ".UNSAFE_GROOVY_INTERPOLATION";
public static String UNSAFE_GROOVY_INTERPOLATION = System.getProperty(DSL.class.getName() + ".UNSAFE_GROOVY_INTERPOLATION", "warn");

src/main/java/org/jenkinsci/plugins/workflow/cps/DSL.java Outdated Show resolved Hide resolved
public static class InterpolatedWarnings {
final String stepName;
final List<String> interpolatedVariables;
final Run run;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since InterpolatedWarnings is stored in a non-transient field in InterpolatedSecretsAction, the run field here must also be transient or else there will be issues. It looks like you do use the field in getStepSignature, so I think you could either delete the field and make that method take the Run as a parameter, make run transient here and add methods to restore it when onLoad and onAttached are called in the outer class, or maybe you could make InterpolatedWarnings a non-static inner class and use the transient run field from the parent directly (but I'm not sure if @ExportedBean works with non-static inner classes).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3e54d36
made Run transient. Confirmed stepSignature in api

Map<String, ?> udArgs = ud.getArguments();
valueString = udArgs.entrySet().stream()
.map(InterpolatedSecretsAction::argumentToString)
.collect(Collectors.joining(", ", udName + "(", ")"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is right when using symbols, but when $class is used the syntax is a list like [$class: udName, args...].

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(A map, not a list.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated in b52438d to handle $class. Not sure what I should be using to test those changes. I just looked up some examples I found online.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically any extension without a @Symbol.

@car-roll car-roll closed this Nov 3, 2020
@car-roll car-roll reopened this Nov 3, 2020
Remove @ symbol for UninstantiatedDescribable getStepSignature
List<InterpolatedSecretsAction.InterpolatedWarnings> warnings = reportAction.getWarnings();
MatcherAssert.assertThat(warnings.size(), is(1));
InterpolatedSecretsAction.InterpolatedWarnings stepWarning = warnings.get(0);
MatcherAssert.assertThat(stepWarning.getStepSignature(), is("archiveArtifacts(delegate: archiveArtifacts(<anonymous>: ${PASSWORD}))"));
Copy link
Member

@dwnusbaum dwnusbaum Nov 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test and a few others shows there are still a few issues with InterpolatedWarnings.getStepSignature. It would be good to fix these, but I also think they are relatively unlikely to occur in practice with interpolated passwords, so maybe it would be better to merge this and handle that in a follow-up?

  1. When using symbol-based syntax and the UninstantiatedDescribable only has one required parameter and there is only one provided argument (i.e. hasSoleRequiredParameter returns true), the value can be provided directly instead of using the key: value syntax, meaning that we should never see things like <anonymous>: in the output. In this particular case, fixing this issue (but not the other issue) would make the output archiveArtifacts(archiveArtifacts(${PASSWORD})), which is a bit closer to what was written in the Pipeline.

  2. Meta steps like archiveArtifacts are handled incorrectly. For example, in this case the outer archiveArtifacts is actually the step step, but generally the outer step is should be omitted so only the inner describable is shown. The direct meta step invocation in DSL.invokeDescribable specifically exists to handle this case so users don't have to write use step explicitly. You can check for this case using StepDescriptor.isMetaStep (check that that FlowNode is an instance of StepNode and then call StepNode.getDescriptor) along with making sure that the argument is for the Describable type corresponding to the required parameter of the meta step, maybe with something like this in InterpolatedWarnings.getStepSignature:

    FlowNode node; // Maybe return this when looking up step arguments?
    if (node instanceof StepNode) {
        StepDescriptor descriptor = ((StepNode)node).getDescriptor();
        if (descriptor.isMetaStep()) {
            DescribableParameter p = DescribableModel.of(descriptor.clazz).getFirstRequiredParameter();
            if (p != null) {
                Object arg = ArgumentsAction.getResolvedArguments(f).get(p.getName());
                return argumentToString(arg);
            }
        }
    }
    // handle as normal step using existing code
    

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attempted to address comments in 7d8672c

@@ -436,6 +437,7 @@ protected Object invokeDescribable(String symbol, Object _args) {
NamedArgsAndClosure args = parseArgs(_args, metaStep!=null && metaStep.takesImplicitBlockArgument(),
UninstantiatedDescribable.ANONYMOUS_KEY, singleArgumentOnly, new HashSet<>());
UninstantiatedDescribable ud = new UninstantiatedDescribable(symbol, null, args.namedArgs);
ud.setModel(symbolModel);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to set the DescribableModel in order for hasSoleRequiredArgument to work.

if (arg instanceof UninstantiatedDescribable) {
return argumentToString(arg);
} else {
return stepName + "(" + argumentToString(arg) + ")";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know in the case of ArchiveArtifacts the inner argument was an UninstantiatedDescribable with a symbol so that it would print out the signature correctly. What I was less sure about, was if this was always going to be the case in these scenarios where there is an outer CoreStep.

@@ -101,7 +102,6 @@
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>script-security</artifactId>
<version>1.75</version>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current version of the BOM makes this redundant.

pom.xml Outdated
@@ -93,6 +93,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-support</artifactId>
<version>3.6-rc759.3e4ba3120e7a</version> <!-- TODO: https://github.com/jenkinsci/workflow-support-plugin/pull/110 -->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That PR needs to be released before we merge and release this one.

@@ -621,6 +626,22 @@ public void testReallyUnusualStepInstantiations() throws Exception {
equalTo(NotStoredReason.UNSERIALIZABLE));
}

@Issue("JENKINS-47101")
@Test public void passwordParametersSanitized() throws Exception {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@car-roll This is the new test I added for jenkinsci/workflow-support-plugin#110.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants