-
Notifications
You must be signed in to change notification settings - Fork 159
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
Add groovy SimpleTemplateEngine utility step #162
Conversation
Completed an automated test for SimpleTemplateEngineStep plugin.
@basil can you please review or inform us about the next steps to get this feature accepted? Thanks! |
I am not a maintainer of this plugin. |
Do you know who is? |
@rsandell can you please review this PR? |
I'm not sure but it looks like a big security concern here that you could bypass the groovy sandbox with this step as it is implemented now. |
I think you are talking about the code in this method. Wow. I had no idea. What do you mean by "uses at least one of the alternatives"? Is that's the conditional here? |
The two alternatives you can implement is the same as any other groovy script in Jenkins; Either run in the Groovy sandbox where each thing that gets run is done against a white/black list. Or it can be run with whole script approval, where the entire script is sent to an administrator to approve before it is allowed to be executed on the controller, if it is already approved it will of course be allowed to run without involving an admin the second time. |
You don't need to make it as complicated as email-ext. You could make it behave more like pipeline itself with a And even simpler approach would be to just implement one of them (sandbox or whole script approval) and I would be fine with that as well. It could then be enhanced at a later date with the other if someone wants it. |
@rsandell I added a I am not sure of what I am doing, a code review would be much appreciated. Also, I would like to do proper testing to prove the sandbox code I wrote is correct. I added TODOs in the tests to indicate what I am missing. Can you please provide advice? Thanks! |
new ProxyWhitelist(Whitelist.all()) | ||
); | ||
} else { | ||
renderedTemplate = templateR.make(bindings).toString(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this still allows for running unprotected code in the controller jvm, just set runInSandbox
to false
and you have bypassed the sandbox and now anyone that can edit a Jenkinsfile or configure a job effectively have administrator privileges. See https://javadoc.jenkins.io/plugin/script-security/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
renderedTemplate = templateR.make(bindings).toString(); | |
ScriptApproval.get().using(script, GroovyLanguage.get()); //The code above needs to be reworked so that we can have the script string for this line. | |
renderedTemplate = templateR.make(bindings).toString(); |
FilePath f = ws.child(step.getFile()); | ||
if (f.exists() && !f.isDirectory()) { | ||
try (InputStream is = f.read()) { | ||
template = engine.createTemplate(IOUtils.toString(is, StandardCharsets.UTF_8)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know but it could be that static code in the template (if that is possible) could be run here. Needs to be tested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs to be checked if it is allowed to be run if it is not running in the sandbox.
template = engine.createTemplate(IOUtils.toString(is, StandardCharsets.UTF_8)); | |
String tmpl = IOUtils.toString(is, StandardCharsets.UTF_8); | |
if (!isRunInSandBox()) { | |
ScriptApproval.get().configuring(tmpl, GroovyLanguage.get(), ApprovalContext.create().withItem(getContext().get(Item.class)), false); | |
} | |
template = engine.createTemplate(tmpl); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the code for SimpleTemplateEngine
it is in createTemplate
that the script is parsed, so potentially some static code could run outside of the sandbox, but I am not sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it is not safe to call createTemplate
outside of the sandbox.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It turns out that there is more nuance to this that I forgot. The fixes for SECURITY-2020 mean that at worst, users who try to create very complex templates that use a wide range of Groovy features may get a false-positive RejectedAccessException
with the current code that does not have an active sandbox when createTemplate
is called.
...ain/java/org/jenkinsci/plugins/pipeline/utility/steps/template/SimpleTemplateEngineStep.java
Outdated
Show resolved
Hide resolved
...org/jenkinsci/plugins/pipeline/utility/steps/template/SimpleTemplateEngineStepExecution.java
Outdated
Show resolved
Hide resolved
@daniel-beck @dwnusbaum @Wadeck I would appreciate some help in reviewing this to make sure we are secure introducing this feature. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recommend this not be merged unless approved by Jesse or Devin.
} else { | ||
renderedTemplate = templateR.make(bindings).toString(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Users must not be able to opt out of Script Security protection. The only choice they get is:
- Whole-script approval
- Sandboxing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like Bobby noticed, there are security issues with this PR in its current form.
For what it's worth, I have access to a proprietary Jenkins plugin which encapsulates SimpleTemplateEngine
, and in that plugin, the source code of SimpleTemplateEngine
itself was copied into the plugin and various methods were modified to interact with the sandbox. I would have to investigate things more carefully to know if SimpleTemplateEngine
can be used safely just with external sandbox protection.
FilePath f = ws.child(step.getFile()); | ||
if (f.exists() && !f.isDirectory()) { | ||
try (InputStream is = f.read()) { | ||
template = engine.createTemplate(IOUtils.toString(is, StandardCharsets.UTF_8)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it is not safe to call createTemplate
outside of the sandbox.
@dwnusbaum What would be really helpful for me is have a test scenario that verifies that an unapproved signature is effectively rejected at run time. I am not sure how to do that. Can you give me some advice by looking at my unit test code please? Thanks! |
@martinda The problems here are kind of obscure because |
982240b
to
ecf90cc
Compare
Sorry for the noise, I pushed a commit too soon and forced it back. Thanks for all your help so far. I am still working on it. |
If I understand correctly, I should be following the guidelines here and I should pass the "user written template" as a |
I am completely overwhelmed by what needs to be done here. It looks like I have to learn a LOT of things. I am trying to convert the text input to a SecureGroovyScript input, which takes me to re-write the descriptor... next I am finding myself diving deep into how to use the CustomDescribableModel and I am wondering... am I getting lost? |
@martinda In this case, you cannot use |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry it took so long, but I added some comments. I think that right now, the template is not being sandbox-transformed at all.
As a general note, I am somewhat apprehensive of steps like this. I think that in most cases, users would be better off applying templates on a build agent using their templating tool of choice in a sh
or bat
step. Users who really like SimpleTemplateEngine
or one of Groovy's many templating engines could use the withGroovy
step from jenkinsci/groovy-plugin#22 to simplify things compared to a plain sh
step.
That kind of approach prevents the controller from needing to parse and run the template, which means that security concerns become irrelevant, and it means that we do not have to maintain another instance of embedded Groovy scripting in Jenkins that is slightly different from other instances and may have unique security issues.
*/ | ||
public class SimpleTemplateEngineStep extends AbstractFileOrTextStep { | ||
|
||
protected Map<String, Object> bindings; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Map<String, Object>
is not a legal data binding type. It works fine in the actual Pipeline script today when using workflow-cps
, but it will not work with tools like the Snippet Generator or other things that analyze steps reflectively (e.g. the step documentation generator, or the display of the step's arguments in the Pipeline Steps view, Blue Ocean, or similar plugins). See jenkinsci/structs-plugin#52 (comment) and other comments in that thread.
The "correct" option would be to introduce a Describable
type that has a String name
and MyDescribable value
parameter where MyDescribable
is a second Describable
type with subclasses for all types values you want to support (unless you just want to support String
). That would add quite a bit of complexity though. In practice many steps have parameters with illegal types and IDK how much users care about the issues that creates.
public class SimpleTemplateEngineStep extends AbstractFileOrTextStep { | ||
|
||
protected Map<String, Object> bindings; | ||
protected boolean runInSandbox; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IDK if it even makes sense to provide this as an option. I would probably just always use the sandbox. Users with very complex templates where the sandbox causes problems should probably just use a sh
step to run whatever templating tool they want on an agent.
Otherwise, you will have to interact with ScriptApproval
in any code path where sandbox
is false, which adds some complexity. In this case you would need to follow the instructions in The Hard Way because this is not a standard case.
FilePath f = ws.child(step.getFile()); | ||
if (f.exists() && !f.isDirectory()) { | ||
try (InputStream is = f.read()) { | ||
template = engine.createTemplate(IOUtils.toString(is, StandardCharsets.UTF_8)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It turns out that there is more nuance to this that I forgot. The fixes for SECURITY-2020 mean that at worst, users who try to create very complex templates that use a wide range of Groovy features may get a false-positive RejectedAccessException
with the current code that does not have an active sandbox when createTemplate
is called.
throw new IllegalArgumentException(Messages.SimpleTemplateEngineStepExecution_missingBindings(fName)); | ||
} | ||
|
||
SimpleTemplateEngine engine = new SimpleTemplateEngine(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The basic constructor here will create a brand-new GroovyShell
with a fresh GroovyClassLoader
, which is unsafe. You need to use GroovySandbox.createSecureCompilerConfiguration
and GroovySandbox. createSecureClassLoader
to create a secure GroovyShell
and pass that to the constructor.
I think that right now, the template is not sandbox-transformed at all, so even basic exploits like <% out.print(System.getProperties()) %>
would not be blocked.
I think also that the straightforward code may introduce memory leaks for users with complex templates. Addressing that would require copying a lot of code from SecureGroovyScript
(all of the cleanup*
methods, but we would probably want to just create an API since at that point it would exist in 3 places (workflow-cps
has similar code))
renderedTemplate = GroovySandbox.runInSandbox( | ||
() -> { | ||
return templateR.make(bindings).toString(); | ||
}, | ||
new ProxyWhitelist(Whitelist.all()) | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, the preferred APIs to use would be something like this:
try (GroovySandbox.Scope scope = new GroovySandbox()
.withWhitelist(new ProxyWhitelist(
new ClassLoaderWhitelist(... the result of GroovySandbox.createSecureClassLoader up above ...),
Whitelist.all())
.enter()) {
return templateR.make(bindings).toString();
}
j.assertLogContains("hudson.remoting.ProxyException: groovy.lang.MissingPropertyException: No such property: wrong for class: SimpleTemplateScript", run); | ||
} | ||
|
||
// TODO: The next test needs input stimulus that only works when runInSandbox is true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typically this should be something that the sandbox prevents, e.g. have the template be <% out.print(System.getProperties) %>
or <% Jenkins.getInstance() %>
, and then assert that the build fails and check the log messages for a RejectedAccessException
for the relevant method.
Thank you for all the comments and advice. I have worked on it quite a bit and it looks very different now. I will try to push my changes as soon as I can. |
I tried the following: node() {
String text = "<%= value %>"
Map binding = ["value":"hello"]
String result = withGroovy {
def engine = new groovy.text.SimpleTemplateEngine()
return engine.createTemplate(text).make(binding).toString()
}
echo(result)
} But I get the same problems as reported here: https://issues.jenkins.io/browse/JENKINS-38432 As shown on this Stackoverflow entry, Many people attempt to use SimpleTemplateEngine, and it would save me time if it just worked. This is why I wanted to create this step. Again thank you very much for all your advice and comments. I will try to make this PR better. |
This won't work since the block content is still evaluated in the Pipeline. It's basically a convenient wrapper block to make Groovy available on the PATH for shell/batch steps. See what the tests are doing for usage examples: https://github.com/jenkinsci/groovy-plugin/pull/22/files#diff-965daf852d92fd8a6c1372216241b76e3dda319a52f160d84849265552e6fca7R39-R40 |
The simpler solution is to use the Groovy StreamingTemplateEngine as it does not cause problems. |
Add a pipeline utility step that calls the groovy SimpleTemplateEngine.
Added the pipeline step, the step execution and the unit tests for success and exception cases.