Compilation Customizers & Secure Scripting
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Groovy 6.0 (6.0.0, 2026-09-22) and every example was run with Groovy 6.0.0 on JDK 26 (checked on 2026-10-14), except the one labelled "not run here". A compilation customizer is a hook that changes how a script or class is compiled without touching its source: it can add imports, apply an AST transformation, forbid constructs, or do any of those only for some files. Customizers are the standard way to build a DSL host (see Domain-Specific Languages in Groovy) and the first layer of defence when a Java application evaluates scripts written by someone else (the embedding APIs are on Integrating Groovy into Java Applications). The scenario is a greenhouse application that lets operators supply small rule scripts. Compile-time AST transformations themselves are covered on Writing AST Transformations & Macros. The same JSR 223 entry point is used from a JMeter test plan on JMeter Scripting with JSR 223 & Groovy.
CompilerConfiguration and the customizer model
Whether Groovy compiles with groovyc or evaluates text through GroovyShell, a
org.codehaus.groovy.control.CompilerConfiguration is used under the hood. It holds the source encoding, the classpath, the
target bytecode level and the list of compilation customizers. The pattern is always the same: create a configuration, add
customizers, then hand the configuration to the host (GroovyShell, GroovyScriptEngine, GroovyClassLoader).
A customizer extends org.codehaus.groovy.control.customizers.CompilationCustomizer. It runs in one specific compilation
phase and is called once for every class node being compiled. A script is itself a class, so a customizer sees the script
class and every class declared inside the script. Groovy ships the customizers described below, and you can write your own by
extending CompilationCustomizer and passing the phase to its constructor.
Import customizer
ImportCustomizer adds imports to every compiled unit, so the author of a DSL script does not have to write them. It supports
all the import forms of the language: class imports (optionally aliased), star imports, static imports (optionally aliased)
and static star imports.
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ImportCustomizer
def icz = new ImportCustomizer()
icz.addImports('java.util.concurrent.atomic.AtomicInteger')
icz.addImport('CHM', 'java.util.concurrent.ConcurrentHashMap')
icz.addStaticImport('java.lang.Math', 'PI')
icz.addStaticStars('java.lang.Math')
def config = new CompilerConfiguration()
config.addCompilationCustomizers(icz)
def shell = new GroovyShell(config)
def result = shell.evaluate '''
def counter = new AtomicInteger(40)
def map = new CHM()
map.put('answer', counter.addAndGet(2))
[map: map, area: (PI * 2 * 2).round(2), root: sqrt(144)]
'''
println result
assert result.map.answer == 42
[map:[answer:42], area:12.57, root:12.0]
The evaluated text contains no import line at all: AtomicInteger, the alias CHM, PI and sqrt all resolve because the
customizer added the imports before the script was compiled. The other methods are addStarImports (a package, as in
import java.util.concurrent.*) and addStaticImport with an alias (import static java.lang.Math.PI as pi).
AST transformation customizer
A global AST transformation applies to every class compiled as long as it is on the classpath, which increases compilation
time and can change code that should not be touched. ASTTransformationCustomizer applies a transformation selectively, only
in the compiler configuration it is added to. It also solves a practical problem: a script cannot be annotated, because its
class is implicit, so @Log cannot be written on it. The customizer applies the transformation to the script class (and to
every class declared in the script) as if the annotation were there. Annotation parameters are passed as named arguments.
import groovy.util.logging.Log
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
def config = new CompilerConfiguration()
config.addCompilationCustomizers(new ASTTransformationCustomizer(Log, value: 'LOGGER'))
def script = new GroovyShell(config).parse('''
LOGGER.info 'logger injected into a plain script'
LOGGER.class.name
''')
println script.run()
println script.class.declaredFields*.name.findAll { it == 'LOGGER' }
oct 14, 2026 10:32:57 P. M. Script1 run
INFO: logger injected into a plain script
java.util.logging.Logger
[LOGGER]
The first two lines are the java.util.logging console handler writing to standard error (the date format follows the default
locale). The customizer behaves like @Log(value='LOGGER'). Because the customizer works with objects rather than AST nodes,
only some values convert to annotation parameters: primitives, strings and class literals become constant expressions, while
a transformation that takes a closure (such as @ConditionalInterrupt) needs a ClosureExpression, which can be produced
with AstBuilder.
The secure AST customizer
SecureASTCustomizer lets the author of a DSL restrict the grammar of the language: which constructs, tokens, constants,
imports and method receivers a script may use. It works on an allow list (only what is listed is permitted) or a deny list
(what is listed is forbidden), chosen separately for each feature, though an allow list is the usual choice. It can:
-
allow or forbid closures, method definitions and package definitions
-
restrict imports, static imports and star imports
-
restrict the receivers of method calls
-
restrict the AST expression and statement kinds
-
restrict the grammar tokens (operators) and the types of constants
The official documentation is explicit about the nature of the guarantee: the customizer is "secure" in one aspect only, limiting the constructs a DSL permits. It checks the syntax tree at compile time, not behaviour at run time, and it does not replace other security measures. The example is a rule language that only evaluates arithmetic.
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.MultipleCompilationErrorsException
import org.codehaus.groovy.control.customizers.SecureASTCustomizer
import static org.codehaus.groovy.syntax.Types.*
def scz = new SecureASTCustomizer()
scz.with {
closuresAllowed = false
methodDefinitionAllowed = false
allowedImports = []
allowedStaticImports = []
allowedStaticStarImports = ['java.lang.Math']
allowedTokens = [PLUS, MINUS, MULTIPLY, DIVIDE, REMAINDER, POWER].asImmutable()
allowedConstantTypesClasses = [Integer, Long, Double, BigDecimal, Integer.TYPE, Long.TYPE, Double.TYPE].asImmutable()
allowedReceiversClasses = [Math, Integer, Long, Double, BigDecimal].asImmutable()
}
def config = new CompilerConfiguration()
config.addCompilationCustomizers(scz)
def run = { String src ->
try {
new GroovyShell(config).evaluate(src)
} catch (MultipleCompilationErrorsException e) {
'REJECTED: ' + e.errorCollector.getError(0).cause.message.readLines().first()
}
}
println run('2 + 3 * 4')
println run('Math.sqrt(16) + 1')
println run('"ls".execute()')
println run('[1, 2].each { println it }')
println run('def f() { 1 }')
println run('import java.io.File; 1')
println run('System.exit(0)')
println run('1 < 2')
14
5.0
REJECTED: Method calls not allowed on [java.lang.String]
REJECTED: Method calls not allowed on [java.util.List]
REJECTED: Method definitions are not allowed
REJECTED: Importing [java.io.File] is not allowed
REJECTED: Method calls not allowed on [java.lang.System]
REJECTED: Token ("<" at 1:3: "<" ) is not allowed
Two points from the output. A rejected script fails at compile time with a MultipleCompilationErrorsException, before any of
it runs. And 1 < 2 is rejected because < is not in the token allow list, which shows how fine-grained the grammar control
is: the allowed tokens are constants of org.codehaus.groovy.syntax.Types.
Custom statement and expression checkers
When the built-in flags are not enough, SecureASTCustomizer.ExpressionChecker and SecureASTCustomizer.StatementChecker are
single-method interfaces (isAuthorized) that receive an AST node and return a boolean. There is, for example, no flag to
forbid the direct field access a.@val, but an expression checker does it in one line.
import org.codehaus.groovy.ast.expr.AttributeExpression
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.MultipleCompilationErrorsException
import org.codehaus.groovy.control.customizers.SecureASTCustomizer
def scz = new SecureASTCustomizer()
scz.addExpressionCheckers({ expr -> !(expr instanceof AttributeExpression) } as SecureASTCustomizer.ExpressionChecker)
def config = new CompilerConfiguration()
config.addCompilationCustomizers(scz)
def run = { String src ->
try { new GroovyShell(config).evaluate(src) }
catch (MultipleCompilationErrorsException e) { 'REJECTED: ' + e.errorCollector.getError(0).cause.message.readLines().first() }
}
println run('class P { int val }; def p = new P(val: 123); p.@val')
println run('class P { int val }; def p = new P(val: 123); p.val')
REJECTED: Expression [AttributeExpression] is not allowed: p.@val
123
What the secure AST customizer does not do
The customizer sees static types written in the source, never run-time types, and that is the root of its limits. The
receiver of a call such as s.getProperty(...) is whatever type the AST declares for s; with def that is Object. A deny
list that names java.lang.System therefore stops System.getProperty(...) but not the same call made through a variable.
An allow list closes the gap, because Object is not on it.
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.MultipleCompilationErrorsException
import org.codehaus.groovy.control.customizers.SecureASTCustomizer
def shellWith = { Closure setup ->
def scz = new SecureASTCustomizer()
scz.with(setup)
def config = new CompilerConfiguration()
config.addCompilationCustomizers(scz)
new GroovyShell(config)
}
def run = { GroovyShell sh, String src ->
try { sh.evaluate(src) }
catch (MultipleCompilationErrorsException e) {
'REJECTED: ' + e.errorCollector.getError(0).cause.message.readLines().first()
}
}
// a deny list is only as good as the types it names
def deny = shellWith { disallowedReceivers = ['java.lang.System'] }
println run(deny, "System.getProperty('java.specification.version')")
println run(deny, "def s = System; s.getProperty('java.specification.version')")
// an allow list closes the gap: Object receivers are rejected
def allow = shellWith { allowedReceiversClasses = [Math, Integer] }
println run(allow, "Math.max(1, 2)")
println run(allow, "def s = System; s.getProperty('java.specification.version')")
REJECTED: Method calls not allowed on [java.lang.System]
26
2
REJECTED: Method calls not allowed on [java.lang.Object]
Read the output in order: the first deny-list script is rejected, the second one (same call through a variable) returns the JDK
specification version, 26; the allow-list shell accepts Math.max(1, 2) (result 2) and rejects the indirect call because
the receiver is Object. Other limits follow from the same cause: the customizer does not constrain what permitted code does
at run time (an allowed method can still be slow, allocate without bound, or read a file), it cannot see code that does not
exist yet when the script is compiled, and it cannot stop a script from exhausting the CPU or the heap. It narrows the language;
it is not a sandbox. The official documentation says it does not replace other security measures.
Wider coverage in Groovy 6.0
Groovy 6.0 closes several gaps in what the customizer checks. Its restrictions now also apply to constructors, static and
instance initializer blocks and field initializers, and to authored code that a transformation relocates into a generated or
synthetic member, such as the pre block of @TupleConstructor or the condition of @ConditionalInterrupt. Purely generated
code (accessors, delegate forwarders, enum machinery, trait bridges) remains exempt. Import rules also cover the target type of
a method pointer or method reference (ProcessBuilder.&new), casts and coercions that construct an instance from a list, map or
closure literal, and named-argument subscript construction. Authored code is told apart from generated code by line number:
generated code has a line number of zero or less, which is what SecureASTCustomizer.isFromSource tests. The deny-list
example above shows the effect on a field initializer and a constructor.
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.MultipleCompilationErrorsException
import org.codehaus.groovy.control.customizers.SecureASTCustomizer
def scz = new SecureASTCustomizer()
scz.disallowedReceivers = ['java.lang.System']
def config = new CompilerConfiguration()
config.addCompilationCustomizers(scz)
def run = { String src ->
try { new GroovyShell(config).evaluate(src) }
catch (MultipleCompilationErrorsException e) { 'REJECTED: ' + e.errorCollector.getError(0).cause.message.readLines().first() }
}
println run("class A { def v = System.getProperty('java.specification.version') }; new A().v")
println run("class B { B() { System.getProperty('x') } }; new B()")
REJECTED: Method calls not allowed on [java.lang.System]
REJECTED: Method calls not allowed on [java.lang.System]
The release notes describe the earlier behaviour as restrictions applied to the script body and method bodies, so a script
author could not rely on a customizer to police constructors or initializers on older lines. The run above proves only the 6.0
behaviour; it was not repeated on an older release. Groovy 6.0 also lets an embedder disable @ASTTest when compiling source it does not
control, covered under Compiling untrusted source: the @ASTTest switch.
Source aware customizer
SourceAwareCustomizer is a filter around another customizer. It takes the customizer to apply as a delegate and applies it
only when predicates on the org.codehaus.groovy.control.SourceUnit match. The predicates can look at the base name of the
file, its extension, the whole source unit (for example, its number of classes) or each class node.
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.classgen.GeneratorContext
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.control.customizers.*
def seen = []
def recorder = new CompilationCustomizer(CompilePhase.CONVERSION) {
void call(SourceUnit source, GeneratorContext context, ClassNode classNode) {
seen << "${source.name}:${classNode.name}"
}
}
def sac = new SourceAwareCustomizer(recorder)
sac.baseNameValidator = { it.endsWith('Bean') }
def config = new CompilerConfiguration()
config.addCompilationCustomizers(sac)
def shell = new GroovyShell(config)
shell.evaluate(new GroovyCodeSource('class Order {}; 1', 'OrderBean.groovy', '/x'))
shell.evaluate(new GroovyCodeSource('class Helper {}; 2', 'Util.groovy', '/x'))
println seen
[OrderBean.groovy:OrderBean, OrderBean.groovy:Order]
The recorder is a hand-written customizer (it also shows the shape of one: a phase passed to the constructor and a call
method). It is invoked for the two class nodes of OrderBean.groovy (the script class and Order) and not at all for
Util.groovy, whose base name does not end with Bean. The other predicates are extensionValidator, sourceUnitValidator
and classValidator.
The customizer builder
Writing new for every customizer gets verbose. CompilerCustomizationBuilder.withConfig takes a configuration and a closure
written in a small hierarchical DSL and registers the customizers for you: imports { ... } (normal, alias, star,
staticMember), ast(Class, params), secureAst { ... }, source(...) { ... }, and inline(phase: ...) { source, context,
classNode -> ... } for a customizer written directly in the closure. Several customizers can be declared in one block.
import groovy.transform.TimedInterrupt
import java.util.concurrent.TimeUnit
import org.codehaus.groovy.control.CompilerConfiguration
import static org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder.withConfig
def config = new CompilerConfiguration()
withConfig(config) {
imports { star 'java.time' }
ast(TimedInterrupt, value: 1, unit: TimeUnit.SECONDS)
}
println new GroovyShell(config).evaluate('Duration.ofMinutes(90).toHours()')
1
The source-aware form of the builder selects files by extension, extensions, basename, basenames or by validator
closures (extensionValidator, basenameValidator, unitValidator). A common use is statically compiling only the files
that follow a naming convention.
import groovy.transform.CompileStatic
import org.codehaus.groovy.control.CompilerConfiguration
import static org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder.withConfig
def config = new CompilerConfiguration()
withConfig(config) {
source(extension: 'sgroovy') {
ast(CompileStatic)
}
}
def shell = new GroovyShell(config)
def dynamicBody = 'def s = "abc"; s.noSuchMethod()'
try { shell.evaluate(new GroovyCodeSource(dynamicBody, 'plain.groovy', '/groovy/shell')) }
catch (MissingMethodException e) { println "plain.groovy -> runtime MissingMethodException" }
try { shell.evaluate(new GroovyCodeSource(dynamicBody, 'checked.sgroovy', '/groovy/shell')) }
catch (org.codehaus.groovy.control.MultipleCompilationErrorsException e) {
println "checked.sgroovy -> " + e.errorCollector.getError(0).cause.message.readLines().first()
}
plain.groovy -> runtime MissingMethodException
checked.sgroovy -> [Static type checking] - Cannot find matching method java.lang.String#noSuchMethod(). Please check if the declared type is correct and if the method exists.
The same script text is dynamic when it comes from a .groovy source and statically checked when its name ends with
.sgroovy; the second one fails at compile time instead of at the call. Static checking is explained on
Static Type Checking & @CompileStatic.
Customizing groovyc with --configscript
Everything above needs an embedding application to build a CompilerConfiguration. For classes compiled by groovyc (or
Ant, Gradle, Maven), the --configscript option (short form -cf) names a Groovy script that receives the configuration
before the files are compiled. The script sees the CompilerConfiguration as a variable called configuration, and the
builder’s withConfig is already available without an import. The documentation’s example makes @CompileStatic the default
for all compiled classes, and recommends keeping the configuration file outside the source directory (for instance src/conf
next to src/main).
// config.groovy
withConfig(configuration) {
ast(groovy.transform.CompileStatic)
}
// Main.groovy
class Main {
static void main(String[] args) {
String s = 'abc'
println s.noSuchMethod()
}
}
Compiling Main.groovy with that configuration script turns the dynamic call into a compile-time error:
$ groovyc --configscript=config.groovy Main.groovy
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
Main.groovy: 4: [Static type checking] - Cannot find matching method java.lang.String#noSuchMethod(). Please check if the declared type is correct and if the method exists.
@ line 4, column 17.
println s.noSuchMethod()
^
1 error
Without --configscript, the same file compiles and fails only when main runs, with a MissingMethodException. The
groovy launcher also accepts --configscript, and the option is how a build can enable a customizer such as
SecureASTCustomizer for the classes it compiles. A configuration script can also run arbitrary Groovy, for example to set
system properties that would otherwise go on a long command line.
Running untrusted scripts
A customizer limits what a script may say. Running code written by someone you do not trust is a wider problem, and the sensible answer is layers: restrict the language, bound the resources, and isolate the execution so that a mistake in one layer is not a compromise.
The Groovy documentation is clear that the customizer alone does not make a script safe, and since Groovy 6.0 the old JVM-level answer is gone (see Security Manager support removed in Groovy 6.0). The layers below are the ones that remain.
Bounding execution time
The interrupt transformations are applied through the customizer (they are the same annotations as in the
AST transformations
page, applied to code you do not control). @TimedInterrupt makes a script throw java.util.concurrent.TimeoutException
once a time budget is exhausted, and @ThreadInterrupt and @ConditionalInterrupt add checks for thread interruption or for
an arbitrary condition. The checks are inserted at method entry and at loop iterations (and in closures), so they stop code
that keeps executing Groovy bytecode.
import groovy.transform.TimedInterrupt
import java.util.concurrent.TimeUnit
import org.codehaus.groovy.control.CompilerConfiguration
import static org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder.withConfig
def config = new CompilerConfiguration()
withConfig(config) { ast(TimedInterrupt, value: 1, unit: TimeUnit.SECONDS) }
def shell = new GroovyShell(config)
def timed = { String label, String src ->
def t0 = System.nanoTime()
def outcome
try { outcome = shell.evaluate(src) } catch (java.util.concurrent.TimeoutException e) { outcome = 'TimeoutException' }
println "${label}: ${outcome} after about ${Math.round((System.nanoTime() - t0) / 1e9)} s"
}
timed('busy loop ', 'while (true) { }')
timed('single blocking', "Thread.sleep(3000); 'finished'")
busy loop : TimeoutException after about 1 s
single blocking: finished after about 3 s
The busy loop is stopped after about one second. The single blocking call is not: Thread.sleep(3000) is one call into the
JDK, so no check runs until it returns, and the script then finishes normally after three seconds. The same applies to a long
native or library operation, to a regular expression with catastrophic backtracking, or to a huge allocation. The interrupt
transformations bound Groovy-level work, nothing else. A hard limit has to come from outside the JVM that runs the script.
Source: Groovy API: TimedInterrupt
Process isolation
The strongest in-process-free option is a child JVM per script: the parent writes the script text to the child’s standard
input, caps its heap, and kills it when a timeout expires. A runaway loop, an out-of-memory error or a call to System.exit
then costs the parent one process, not itself. The example uses a one-line child (Child.groovy evaluates its standard input)
and a parent that starts it with ProcessBuilder.
// Child.groovy: the untrusted text arrives on stdin
println new GroovyShell().evaluate(System.in.text)
// parent.groovy
def groovyHome = System.getenv('GROOVY_HOME') ?: System.getProperty('groovy.home')
def run = { String code, long timeoutSeconds ->
def pb = new ProcessBuilder('java', '-Xmx64m', '-cp', "${groovyHome}/lib/*",
'groovy.ui.GroovyMain', 'Child.groovy').redirectErrorStream(true)
def proc = pb.start()
proc.outputStream.withWriter { it << code }
def finished = proc.waitFor(timeoutSeconds, java.util.concurrent.TimeUnit.SECONDS)
if (!finished) { proc.destroyForcibly(); return 'killed after timeout' }
def out = proc.inputStream.text
if (out.contains('OutOfMemoryError')) return "child JVM failed, exit value ${proc.exitValue()}: OutOfMemoryError"
out.readLines().findAll { !it.startsWith('WARNING') }.join('\n')
}
println run('6 * 7', 20)
println run('while (true) { }', 3)
println run('def l = []; while (true) { l << new byte[10_000_000] }', 20)
42
killed after timeout
child JVM failed, exit value 1: OutOfMemoryError
The three runs are a normal script, an infinite loop killed by the parent after three seconds, and a memory hog stopped by the 64 MB heap cap. The child also needs its own restrictions: run it as an unprivileged OS user, with no credentials in its environment, in an empty working directory, and combine this layer with the secure AST customizer inside the child.
Container isolation
A container (or any equivalent OS-level sandbox) adds what a JVM cannot: a separate file system view, no network, and CPU and memory limits enforced by the kernel. The usual shape is the same as the child JVM above, with the child started through the container runtime. The command below is the typical form; it was not run here, so treat it as a sketch to adapt (image name, paths and limits are placeholders).
docker run --rm -i --network none --read-only --memory 128m --cpus 1 --pids-limit 64 \
--user 10001:10001 --cap-drop ALL example/groovy-runner:6.0 < rule.groovy
The flags remove the network, make the root file system read-only, cap memory, CPU and process count, drop the process to a non-root user and remove all Linux capabilities. Combined with a wall-clock timeout in the parent, a script that escapes every Groovy-level restriction is still contained.
Security Manager support removed in Groovy 6.0
Earlier guidance, including the sandboxing section of the first edition of Groovy in Action, relies on the Java Security Manager with a policy file to limit
what a script can do (file, network, reflection, exit). The Security Manager was deprecated for removal in JDK 17
(JEP 411), and since JDK 24 it is permanently disabled
(JEP 486): it can no longer be enabled, so a policy file has no effect on those JDKs. Groovy 6
removes its own use of AccessController.doPrivileged and the related infrastructure. The 6.0 release notes tell users who
relied on it to adopt other mechanisms such as containers and operating-system security; Groovy 5 still includes the support
on JDK versions that have a Security Manager. Practically, a Groovy 6 deployment should be designed with the layers above
rather than with a policy file.
Compiling untrusted source: the @ASTTest switch
Compiling is not safe either, because some annotations run code at compile time. The closure of @ASTTest executes during
compilation, so merely compiling untrusted source would run it. Groovy 6.0 adds a system property for embedders that compile
source they do not control; it mirrors groovy.grape.enable, which disables @Grab.
import groovy.transform.ASTTest
@ASTTest(value = { println 'ASTTest closure ran at compile time' })
class T {}
println 'done'
$ groovy t.groovy
ASTTest closure ran at compile time
ASTTest closure ran at compile time
ASTTest closure ran at compile time
ASTTest closure ran at compile time
done
$ JAVA_OPTS="-Dgroovy.asttest.enable=false" groovy t.groovy
done
The closure ran four times in the first run (the repetition was observed, not investigated) and not at all with
-Dgroovy.asttest.enable=false. A host that compiles user text should set that property, set groovy.grape.enable=false
for the same reason, and run the compilation itself in the isolated process described above.
Groovy 6.0 hardening elsewhere
The 6.0 release notes group a broad set of hardening under "Security & Hardening". The items relevant to this page and to the neighbouring ones are summarized here, each covered in depth where the feature lives.
| Area | Change in 6.0 | Where |
|---|---|---|
XML |
Secure-by-default parsing (XXE and DTD protections) across the XML APIs |
|
SQL |
|
|
Regular expressions |
|
|
JSON |
Nesting-depth limits |
|
Grape |
Stricter coordinate validation; |
|
GroovyDoc |
Writes only inside its destination directory and does not follow symbolic links out of |
|
Groovlets |
A failing groovlet no longer returns internals to the client |
|
Embedding |
|
This page |
Security advisories
The Groovy project publishes advisories for released versions at https://groovy-lang.org/security.html. Only three are listed on that page; they are worth knowing because two of them are why untrusted data must never reach Java deserialization in an application that has Groovy on its classpath. Each is fixed by upgrading; releases before 2.4.4 were not released under Apache and have no official patches.
| Advisory | Problem | Fixed in |
|---|---|---|
CVE-2015-3253 |
An application with Groovy on the classpath that uses standard Java serialization between servers or to store data can execute code when a specially crafted serialized object is deserialized (impact: remote execution of untrusted code, denial of service). Affects the unsupported Codehaus versions 1.7.0 to 2.4.3 |
2.4.4 |
CVE-2016-6814 |
Similar to the previous one, through extra wrapping of objects and catching of exceptions. Affects 1.7.0 to 2.4.7 |
2.4.8 |
CVE-2020-17521 |
On Unix-like systems, the temporary directories that Groovy created for Java stubs and for two |
2.4.21, 2.5.14, 3.0.7, 4.0.0-alpha-2 |
Mitigations given by the project: isolate the code that deserializes objects, upgrade, avoid relying on serialization to
communicate remotely, and (for the temporary directory problem) set java.io.tmpdir to a directory exclusively owned by the
running user or use the JDK’s Files.createTempDirectory. The page lists no advisory for the Groovy 5 or 6 lines at the time
of checking (2026-10-14); the hardening items above are release-note changes rather than advisories.
Source: Groovy documentation: Security
Where the book is outdated
The first edition of Groovy in Action (Groovy 1.0, 2007) covers sandboxing scripts before compilation customizers existed
in their current form, and relies on the Java Security Manager, which Groovy 6.0 no longer supports. Today’s recipe is the one on this page: a
CompilerConfiguration with customizers, interrupt transformations, and isolation by process or container. Treat any
sandboxing recipe in older material that depends on a policy file as obsolete on Groovy 6.