public class GormQuerySafetyTransformer extends org.codehaus.groovy.ast.ClassCodeVisitorSupport
org.codehaus.groovy.ast.ClassCodeVisitorSupport that detects GORM HQL/Cypher query text built from a
org.codehaus.groovy.ast.expr.GStringExpression that Groovy coerced to a plain String before it
reaches a GORM query method, e.g.:
String query = "from Book where name = ${userInput" // coerced to String right here
Book.executeQuery(query) // -> raw, unescaped text, no binding
}
When a groovy.lang.GString is passed directly to a GORM query method, GORM binds
each interpolated value as a query parameter — safe. Once the GString has been coerced
to a String (an explicit String-typed local, a .toString() call, or an
as String/cast coercion), that information is gone: a String carries no trace
of ever having been a GString, so this can only be caught here, before the coercion
erases it — a runtime check at the query boundary is structurally blind to this case.
A GString does not have to be flattened directly at the call site to be unsafe —
aliasing it through one or more intermediate variables still loses the binding the moment it is
assigned to a String-typed variable, however many hops away that happens:
def g = "from Book where name = ${userInput" // g: still a live GString
String q = g // flattened HERE, not at the executeQuery call
Book.executeQuery(q)
}
This is a build-breaking error for the local-variable case above, because the detection is precise: every flattening point is visible in the method being compiled. Two related patterns are lower-confidence and instead reported as compile-time warnings, which do not fail the build:
String-typed field initialized from an interpolated
GString, or assigned one via this.field = ..., that later reaches a query
method through this.field. Unlike locals, a field can be reassigned from a
constructor, another method, or a subclass that this check never visits, so it is flagged
rather than failed.+ from a
non-constant value and no GString involved at all, e.g.
"select ... " + userInput. This is a real injection shape, but concatenation is
common enough for benign, non-query purposes that a hard failure would be too blunt an
instrument. Concatenating a GString with anything else (e.g.
"...${x..." + " order by title"}) is a different matter - GString.plus
returns a plain String, so this flattens the interpolation immediately and is
reported as the build-breaking error above, not this warning.Both warnings share the same SUPPRESS_WARNINGS_VALUE suppression as the error case.
Known limitations (deliberate scope):
String built inside a helper method and
returned to the caller is invisible to this check.if/else (a variable unsafe after either branch stays unsafe after the statement), but not
across loops, switch, or try/catch - and is last-write-wins for
fields, which are not branch-sensitive at all.GString initializer or
this.field = ... assignment - it does not follow aliasing chains or
.toString()/cast coercions the way local tracking does.groovy.sql.Sql, or any datastore whose query
methods use names outside CANDIDATE_METHODS.| Modifiers | Name | Description |
|---|---|---|
static java.lang.String |
SUPPRESS_WARNINGS_VALUE |
The @SuppressWarnings value that silences this check (both the error and the two
warnings below) on the enclosing method (or, for calls outside any method, the enclosing
class). |
| Constructor and description |
|---|
GormQuerySafetyTransformer(org.codehaus.groovy.control.SourceUnit sourceUnit) |
| Type Params | Return Type | Name and description |
|---|---|---|
|
protected org.codehaus.groovy.control.SourceUnit |
getSourceUnit() |
|
public void |
visitBinaryExpression(org.codehaus.groovy.ast.expr.BinaryExpression expression) |
|
public void |
visitClass(org.codehaus.groovy.ast.ClassNode node) |
|
public void |
visitDeclarationExpression(org.codehaus.groovy.ast.expr.DeclarationExpression expression) |
|
public void |
visitIfElse(org.codehaus.groovy.ast.stmt.IfStatement ifElse)Visits an if/else branch-sensitively: each branch is walked from the same
starting state, and the two resulting states are merged pessimistically afterwards - a
variable unsafe at the end of either branch stays unsafe after the statement, since we
don't know at compile time which branch will actually run. |
|
public void |
visitMethod(org.codehaus.groovy.ast.MethodNode node) |
|
public void |
visitMethodCallExpression(org.codehaus.groovy.ast.expr.MethodCallExpression call) |
|
public void |
visitStaticMethodCallExpression(org.codehaus.groovy.ast.expr.StaticMethodCallExpression call) |
| Methods inherited from class | Name |
|---|---|
class org.codehaus.groovy.ast.ClassCodeVisitorSupport |
org.codehaus.groovy.ast.ClassCodeVisitorSupport#addError(java.lang.String, org.codehaus.groovy.ast.ASTNode), org.codehaus.groovy.ast.ClassCodeVisitorSupport#equals(java.lang.Object), org.codehaus.groovy.ast.ClassCodeVisitorSupport#getClass(), org.codehaus.groovy.ast.ClassCodeVisitorSupport#hashCode(), org.codehaus.groovy.ast.ClassCodeVisitorSupport#notify(), org.codehaus.groovy.ast.ClassCodeVisitorSupport#notifyAll(), org.codehaus.groovy.ast.ClassCodeVisitorSupport#toString(), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visit(org.codehaus.groovy.ast.expr.Expression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visit(org.codehaus.groovy.ast.stmt.Statement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitAnnotations(org.codehaus.groovy.ast.AnnotatedNode), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitArgumentlistExpression(org.codehaus.groovy.ast.expr.ArgumentListExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitArrayExpression(org.codehaus.groovy.ast.expr.ArrayExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitAssertStatement(org.codehaus.groovy.ast.stmt.AssertStatement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitAttributeExpression(org.codehaus.groovy.ast.expr.AttributeExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitBinaryExpression(org.codehaus.groovy.ast.expr.BinaryExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitBitwiseNegationExpression(org.codehaus.groovy.ast.expr.BitwiseNegationExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitBlockStatement(org.codehaus.groovy.ast.stmt.BlockStatement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitBooleanExpression(org.codehaus.groovy.ast.expr.BooleanExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitBreakStatement(org.codehaus.groovy.ast.stmt.BreakStatement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitBytecodeExpression(org.codehaus.groovy.classgen.BytecodeExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitCaseStatement(org.codehaus.groovy.ast.stmt.CaseStatement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitCastExpression(org.codehaus.groovy.ast.expr.CastExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitCatchStatement(org.codehaus.groovy.ast.stmt.CatchStatement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitClass(org.codehaus.groovy.ast.ClassNode), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitClassExpression(org.codehaus.groovy.ast.expr.ClassExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitClosureExpression(org.codehaus.groovy.ast.expr.ClosureExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitClosureListExpression(org.codehaus.groovy.ast.expr.ClosureListExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitConstantExpression(org.codehaus.groovy.ast.expr.ConstantExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitConstructor(org.codehaus.groovy.ast.ConstructorNode), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitConstructorCallExpression(org.codehaus.groovy.ast.expr.ConstructorCallExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitContinueStatement(org.codehaus.groovy.ast.stmt.ContinueStatement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitDeclarationExpression(org.codehaus.groovy.ast.expr.DeclarationExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitDoWhileLoop(org.codehaus.groovy.ast.stmt.DoWhileStatement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitEmptyExpression(org.codehaus.groovy.ast.expr.EmptyExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitEmptyStatement(org.codehaus.groovy.ast.stmt.EmptyStatement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitExpressionStatement(org.codehaus.groovy.ast.stmt.ExpressionStatement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitField(org.codehaus.groovy.ast.FieldNode), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitFieldExpression(org.codehaus.groovy.ast.expr.FieldExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitForLoop(org.codehaus.groovy.ast.stmt.ForStatement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitGStringExpression(org.codehaus.groovy.ast.expr.GStringExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitIfElse(org.codehaus.groovy.ast.stmt.IfStatement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitImports(org.codehaus.groovy.ast.ModuleNode), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitLambdaExpression(org.codehaus.groovy.ast.expr.LambdaExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitListExpression(org.codehaus.groovy.ast.expr.ListExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitListOfExpressions(java.util.List), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitMapEntryExpression(org.codehaus.groovy.ast.expr.MapEntryExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitMapExpression(org.codehaus.groovy.ast.expr.MapExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitMethod(org.codehaus.groovy.ast.MethodNode), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitMethodCallExpression(org.codehaus.groovy.ast.expr.MethodCallExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitMethodPointerExpression(org.codehaus.groovy.ast.expr.MethodPointerExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitMethodReferenceExpression(org.codehaus.groovy.ast.expr.MethodReferenceExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitNotExpression(org.codehaus.groovy.ast.expr.NotExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitPackage(org.codehaus.groovy.ast.PackageNode), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitPostfixExpression(org.codehaus.groovy.ast.expr.PostfixExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitPrefixExpression(org.codehaus.groovy.ast.expr.PrefixExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitProperty(org.codehaus.groovy.ast.PropertyNode), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitPropertyExpression(org.codehaus.groovy.ast.expr.PropertyExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitRangeExpression(org.codehaus.groovy.ast.expr.RangeExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitReturnStatement(org.codehaus.groovy.ast.stmt.ReturnStatement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitShortTernaryExpression(org.codehaus.groovy.ast.expr.ElvisOperatorExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitSpreadExpression(org.codehaus.groovy.ast.expr.SpreadExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitSpreadMapExpression(org.codehaus.groovy.ast.expr.SpreadMapExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitStaticMethodCallExpression(org.codehaus.groovy.ast.expr.StaticMethodCallExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitSwitch(org.codehaus.groovy.ast.stmt.SwitchStatement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitSynchronizedStatement(org.codehaus.groovy.ast.stmt.SynchronizedStatement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitTernaryExpression(org.codehaus.groovy.ast.expr.TernaryExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitThrowStatement(org.codehaus.groovy.ast.stmt.ThrowStatement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitTryCatchFinally(org.codehaus.groovy.ast.stmt.TryCatchStatement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitTupleExpression(org.codehaus.groovy.ast.expr.TupleExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitUnaryMinusExpression(org.codehaus.groovy.ast.expr.UnaryMinusExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitUnaryPlusExpression(org.codehaus.groovy.ast.expr.UnaryPlusExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitVariableExpression(org.codehaus.groovy.ast.expr.VariableExpression), org.codehaus.groovy.ast.ClassCodeVisitorSupport#visitWhileLoop(org.codehaus.groovy.ast.stmt.WhileStatement), org.codehaus.groovy.ast.ClassCodeVisitorSupport#wait(), org.codehaus.groovy.ast.ClassCodeVisitorSupport#wait(long), org.codehaus.groovy.ast.ClassCodeVisitorSupport#wait(long, int) |
The @SuppressWarnings value that silences this check (both the error and the two
warnings below) on the enclosing method (or, for calls outside any method, the enclosing
class).
Visits an if/else branch-sensitively: each branch is walked from the same
starting state, and the two resulting states are merged pessimistically afterwards - a
variable unsafe at the end of either branch stays unsafe after the statement, since we
don't know at compile time which branch will actually run. Without this, whichever branch
happens to be visited last would silently win, e.g. a variable flattened only in the
if branch would be forgotten if the else branch reassigns it safely.