Before we dig into the details about sandboxing, let’s try to "secure" our script using a traditional type checking extension. Registering a type checking extension is easy: just set the extensions
parameter of the @TypeChecked
annotation (or @CompileStatic
if you want to use static compilation):
@TypeChecked(extensions=['SecureExtension1.groovy'])
void foo() {
def c = System
c.exit(-1)
}
foo()
The extension will be searched on classpath in source form (there’s an option to have precompiled type checking extensions but this is beyond the scope of this blog post):
SecureExtension1.groovy
onMethodSelection { expr, methodNode -> (1)
if (methodNode.declaringClass.name=='java.lang.System') { (2)
addStaticTypeError("Method call is not allowed!", expr) (3)
}
}
1 |
when the type checker selects the target method of a call |
2 |
then if the selected method belongs to the System class |
3 |
make the type checker throw an error |
That’s really all needed. Now execute the code again, and you will see that there’s a compile time error!
/home/cchampeau/tmp/securetest.groovy: 6: [Static type checking] - Method call is not allowed!
@ line 6, column 3.
c.exit(-1)
^
1 error
So this time, thanks to the type checker, c
is really recognized as an instance of class System
and we can really disallow the call. This is a very simple example, but it doesn’t really go as
far as what we can do with the secure AST customizer in terms of configuration. The extension that we wrote has hardcoded checks, but it would probably be nicer if we could configure it. So let’s
start working with a bit more complex example.
Imagine that your application computes a score for a document and that you allow the users to customize the score. Then your DSL:
-
will expose (at least) a variable named score
-
will allow the user to perform mathematical operations (including calling methods like cos, abs, …)
-
should disallow all other method calls
An example of user script would be:
Such a DSL is easy to setup. It’s a variant of the one we defined earlier:
Sandbox.java
CompilerConfiguration conf = new CompilerConfiguration();
ImportCustomizer customizer = new ImportCustomizer();
customizer.addStaticStars("java.lang.Math"); (1)
conf.addCompilationCustomizers(customizer);
Binding binding = new Binding();
binding.setVariable("score", 2.0d); (2)
GroovyShell shell = new GroovyShell(binding,conf);
Double userScore = (Double) shell.evaluate("abs(cos(1+score))"); (3)
System.out.println("userScore = " + userScore);
1 |
add an import customizer that will add import static java.lang.Math.* to all scripts |
2 |
make the score variable available to the script |
3 |
execute the script |
|
There are options to cache the scripts, instead of parsing and compiling them each time. Please check the documentation for more details.
|
So far, our script works, but nothing prevents a hacker from executing malicious code. Since we want to use type checking, I would recommand to use the @CompileStatic
transformation
transparently:
-
it will activate type checking on the script, and we will be able to perform additional checks thanks to the type checking extension
-
it will improve the performance of the script
Adding @CompileStatic
transparently is easy. We just have to update the compiler configuration:
ASTTransformationCustomizer astcz = new ASTTransformationCustomizer(CompileStatic.class);
conf.addCompilationCustomizers(astcz);
Now if you try to execute the script again, you will face a compile time error:
Script1.groovy: 1: [Static type checking] - The variable [score] is undeclared.
@ line 1, column 11.
abs(cos(1+score))
^
Script1.groovy: 1: [Static type checking] - Cannot find matching method int#plus(java.lang.Object). Please check if the declared type is right and if the method exists.
@ line 1, column 9.
abs(cos(1+score))
^
2 errors
What happened? If you read the script from a "compiler" point of view, it doesn’t know anything about the "score" variable. You, as a developer, know that it’s a variable
of type double
, but the compiler cannot infer it. This is precisely what type checking extensions are designed for: you can provide additional information to the compiler,
so that compilation passes. In this case, we will want to indicate that the score
variable is of type double
.
So we will slightly change the way we transparently add the @CompileStatic
annotation:
ASTTransformationCustomizer astcz = new ASTTransformationCustomizer(
singletonMap("extensions", singletonList("SecureExtension2.groovy")),
CompileStatic.class);
This will "emulate" code annotated with @CompileStatic(extensions=['SecureExtension2.groovy'])
. Of course now we need to write the extension which will recognize the score
variable:
SecureExtension2.groovy
unresolvedVariable { var -> (1)
if (var.name=='score') { (2)
return makeDynamic(var, double_TYPE) (3)
}
}
1 |
in case the type checker cannot resolve a variable |
2 |
if the variable name is score |
3 |
then instruct the compiler to resolve the variable dynamically, and that the type of the variable is double |
You can find a complete description of the type checking extension DSL in https://docs.groovy-lang.org/latest/html/documentation/#type_checking_extensions[this section of the documentation],
but you have here an example of _mixed mode compilation : the compiler is not able to resolve the score
variable. You, as the designer of the DSL, know that the variable is in fact
found in the binding, and is of the double
, so the makeDynamic
call is here to tell the compiler: "ok, don’t worry, I know what I am doing, this variable can be resolved dynamically
and it will be of type `double`". That’s it!