Skip to content

Commit a2dc2de

Browse files
committed
fixed Groovy sandbox bypass via constructor invocation
[FIXES SECURITY-1342]
1 parent 0a8a304 commit a2dc2de

File tree

7 files changed

+83
-4
lines changed

7 files changed

+83
-4
lines changed

docs/Home.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ Browse the Jenkins issue tracker to see any [open issues](https://issues.jenkins
3232
* Increased the minimum supported Jenkins version to 2.121
3333
* Replaced built-in support for periodic folder trigger, see [Migration](Migration#migrating-to-172)
3434
([JENKINS-55429](https://issues.jenkins-ci.org/browse/JENKINS-55429))
35+
* Fixed Groovy sandbox bypass via constructor invocation
36+
([SECURITY-1342](https://issues.jenkins-ci.org/browse/SECURITY-1342))
3537
* Removed anything that has been deprecated in 1.70, see [Migration](Migration#migrating-to-170)
3638
* Removed anything that has been deprecated in 1.69, see [Migration](Migration#migrating-to-169)
3739
* Removed anything that has been deprecated in 1.68, see [Migration](Migration#migrating-to-168)

job-dsl-core/src/main/groovy/javaposse/jobdsl/dsl/AbstractDslScriptLoader.groovy

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ abstract class AbstractDslScriptLoader<S extends JobParent, G extends GeneratedI
9898
logger.println('Processing provided DSL script')
9999
}
100100

101-
Script script = groovyShell.parse(createGroovyCodeSource(scriptRequest))
101+
Script script = parseScript(groovyShell, createGroovyCodeSource(scriptRequest))
102102
script.binding = createBinding(scriptRequest)
103103
script.binding.setVariable('jobFactory', script)
104104

@@ -130,6 +130,10 @@ abstract class AbstractDslScriptLoader<S extends JobParent, G extends GeneratedI
130130
new GroovyCodeSource(scriptRequest.body, scriptRequest.scriptName ?: 'script', DEFAULT_CODE_BASE)
131131
}
132132

133+
protected Script parseScript(GroovyShell groovyShell, GroovyCodeSource codeSource) {
134+
groovyShell.parse(codeSource)
135+
}
136+
133137
protected void runScript(Script script) {
134138
script.run()
135139
}

job-dsl-plugin/build.gradle

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ plugins {
1616

1717
description = 'Jenkins plugin to leverage job-dsl-core to programmatic create jobs from inside Jenkins.'
1818

19+
repositories {
20+
maven {
21+
credentials {
22+
username "$artifactoryUsername"
23+
password "$artifactoryPassword"
24+
}
25+
url('https://repo.jenkins-ci.org/security1336-staging')
26+
}
27+
}
28+
1929
// keep the generated sources outside of the build directory, see http://stackoverflow.com/a/21003914/1271460
2030
def generatedSourcesDir = 'generated'
2131

@@ -89,7 +99,7 @@ dependencies {
8999
exclude group: 'org.jvnet.hudson', module:'xstream'
90100
}
91101
jenkinsPlugins 'org.jenkins-ci.plugins:structs:1.13'
92-
jenkinsPlugins 'org.jenkins-ci.plugins:script-security:1.25'
102+
jenkinsPlugins 'org.jenkins-ci.plugins:script-security:1.54'
93103
optionalJenkinsPlugins('org.jenkins-ci.plugins:vsphere-cloud:1.1.11') {
94104
exclude group: 'dom4j'
95105
}

job-dsl-plugin/src/main/groovy/javaposse/jobdsl/plugin/JobDslWhitelist.groovy

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import javaposse.jobdsl.plugin.structs.DescribableContext
66
import javaposse.jobdsl.plugin.structs.DescribableListContext
77
import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.AbstractWhitelist
88

9+
import java.lang.reflect.Constructor
910
import java.lang.reflect.Method
1011

1112
/**
@@ -17,6 +18,11 @@ class JobDslWhitelist extends AbstractWhitelist {
1718
AbstractExtensibleContext, DescribableContext, DescribableListContext
1819
]
1920

21+
@Override
22+
boolean permitsConstructor(Constructor<?> constructor, Object[] args) {
23+
constructor.declaringClass == JenkinsJobParent
24+
}
25+
2026
@Override
2127
boolean permitsMethod(Method method, Object receiver, Object[] args) {
2228
Context.isAssignableFrom(method.declaringClass) ||

job-dsl-plugin/src/main/groovy/javaposse/jobdsl/plugin/SandboxDslScriptLoader.groovy

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ import org.acegisecurity.AccessDeniedException
1010
import org.codehaus.groovy.control.CompilerConfiguration
1111
import org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException
1212
import org.jenkinsci.plugins.scriptsecurity.sandbox.Whitelist
13+
import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.ClassLoaderWhitelist
1314
import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.GroovySandbox
1415
import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.ProxyWhitelist
1516
import org.jenkinsci.plugins.scriptsecurity.scripts.ApprovalContext
1617
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval
1718

19+
import java.util.concurrent.Callable
20+
1821
class SandboxDslScriptLoader extends SecureDslScriptLoader {
1922
private final Item seedJob
2023

@@ -41,24 +44,49 @@ class SandboxDslScriptLoader extends SecureDslScriptLoader {
4144
}
4245

4346
@Override
44-
protected void runScript(Script script) {
47+
protected JenkinsJobParent runScriptEngine(ScriptRequest scriptRequest, GroovyShell groovyShell) {
4548
if (ACL.SYSTEM == Jenkins.authentication) {
4649
// the build must run as an actual user
4750
throw new AccessDeniedException(Messages.SandboxDslScriptLoader_NotAuthenticated())
4851
}
4952

5053
try {
51-
GroovySandbox.run(script, new ProxyWhitelist(Whitelist.all(), new JobDslWhitelist()))
54+
return super.runScriptEngine(scriptRequest, groovyShell)
5255
} catch (RejectedAccessException e) {
5356
ScriptApproval.get().accessRejected(e, ApprovalContext.create().withItem(seedJob))
5457
throw new DslException(e.message, e)
5558
}
5659
}
5760

61+
@Override
62+
protected Script parseScript(GroovyShell groovyShell, GroovyCodeSource codeSource) {
63+
GroovySandbox.runInSandbox(new Callable<Script>() {
64+
@Override
65+
Script call() throws Exception {
66+
groovyShell.parse(codeSource)
67+
}
68+
}, createWhitelist())
69+
}
70+
71+
@Override
72+
protected void runScript(Script script) {
73+
GroovySandbox.runInSandbox(new Callable<Object>() {
74+
@Override
75+
Object call() throws Exception {
76+
script.run()
77+
}
78+
}, new ProxyWhitelist(new ClassLoaderWhitelist(script.class.classLoader), createWhitelist()))
79+
}
80+
81+
@Override
5882
protected boolean isGroovyShellCacheEnabled() {
5983
false
6084
}
6185

86+
private static ProxyWhitelist createWhitelist() {
87+
new ProxyWhitelist(Whitelist.all(), new JobDslWhitelist())
88+
}
89+
6290
private static class WorkspaceClassLoader extends URLClassLoader {
6391
private final Item seedJob
6492

job-dsl-plugin/src/test/groovy/javaposse/jobdsl/plugin/ExecuteDslScriptsSpec.groovy

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,25 @@ folder('folder-a/folder-b') {
14861486
ScriptApproval.get().pendingSignatures*.signature == ['staticMethod java.lang.System exit int']
14871487
}
14881488
1489+
def 'run script in sandbox with no-arg constructor'() {
1490+
setup:
1491+
String script = this.class.getResourceAsStream('security1342.groovy').text
1492+
1493+
jenkinsRule.instance.securityRealm = jenkinsRule.createDummySecurityRealm()
1494+
jenkinsRule.instance.authorizationStrategy = new MockAuthorizationStrategy()
1495+
.grant(Jenkins.ADMINISTER).everywhere().to('admin')
1496+
FreeStyleProject job = jenkinsRule.createFreeStyleProject('seed')
1497+
job.buildersList.add(new ExecuteDslScripts(scriptText: script, sandbox: true))
1498+
setupQIA('admin', job)
1499+
1500+
when:
1501+
FreeStyleBuild build = job.scheduleBuild2(0).get()
1502+
1503+
then:
1504+
build.result == FAILURE
1505+
ScriptApproval.get().pendingSignatures*.signature == ['new java.io.File java.lang.String']
1506+
}
1507+
14891508
def 'run script in sandbox with import from workspace'() {
14901509
setup:
14911510
String script = 'import Helper\njob(Helper.computeName()) { description("foo") }'
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package javaposse.jobdsl.plugin
2+
3+
class Foo extends JenkinsJobParent {
4+
Foo() { new File("test") }
5+
6+
@Override
7+
Object run() {
8+
return null
9+
}
10+
}

0 commit comments

Comments
 (0)