GROOVY-12244: SecureASTCustomizer does not check authored code relocated into a synthetic method (includes PR#2771) - #2779
Conversation
There was a problem hiding this comment.
Pull request overview
Updates SecureASTCustomizer coverage to ensure authored code remains subject to restrictions even when it appears outside normal method bodies (constructors/initializers/field initializers) or is relocated by AST transforms into synthetic members, addressing GROOVY-12244.
Changes:
- Extend
SecureASTCustomizervisitation to constructors, initializer blocks, field initializers, and authored statements relocated into synthetic methods. - Add regression tests covering disallowed receivers in script/constructor/initializer/field initializer contexts, plus transform-relocation scenarios.
- Update user documentation and Javadoc to reflect the new/clarified coverage model and the “source position” rule for generated members.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/main/java/org/codehaus/groovy/control/customizers/SecureASTCustomizer.java | Expands visitation logic to cover constructors/initializers/field initializers and authored statements within synthetic methods while filtering generated code via source positions. |
| src/test/groovy/org/codehaus/groovy/control/customizers/SecureASTCustomizerTest.groovy | Adds new regression tests for authored code in constructors/initializers/fields and for authored code relocated into generated members by AST transforms. |
| src/spec/doc/core-domain-specific-languages.adoc | Updates SecureASTCustomizer limitation/coverage documentation to match the new behavior and clarify the “source position” distinction. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…lizer blocks or field initializers SecureASTCustomizer visited the script statement block and method bodies only, so code outside a method body escaped every configured restriction: disallowedReceivers, the statement and expression allow/deny lists, and any registered StatementChecker or ExpressionChecker. With disallowedReceivers = ['java.lang.System'], a call in a constructor, a static or instance initializer block, or a field initializer all compiled and ran, while the same call in the script body was correctly rejected. The existing filters could not reach these. A static initializer ends up in <clinit>, which is synthetic and so excluded by filterMethods; instance initializers live in a separate getObjectInitializerStatements() list; and field initializers hang off FieldNode, whose property backing fields are themselves synthetic. Add visitConstructorsAndInitializers(), applying the securing visitor to declared constructors, object initializer statements, the statements inside <clinit>, and field initial expressions. Only nodes carrying a source position are visited. Constructors and initializers are not written solely by the author of the secured source: every script class has generated constructors, and AST transformations add their own. Visiting those rejects valid programs -- a first cut broke four existing tests on the script class's generated super(Binding) call, which is not marked synthetic and so cannot be excluded by any flag. A generated member may nonetheless contain authored code, because a transformation can move it there: @TupleConstructor(pre=...) and @MapConstructor(pre=...) relocate the supplied closure body into the constructor they generate, and @asttest aside, this relocation is the usual fate of a closure supplied as an annotation member. Such statements keep the source position they had in the original source, so the body of a member with no source position of its own is filtered statement by statement rather than skipped outright. The <clinit> body is treated the same way, since that method is always generated while its statements need not be. Tests cover each closed gap, the two relocation cases, the script-body control, and pin the exemption for generated constructors so a later simplification cannot drop the source-position check unnoticed. Both Limitations sections, in the user guide and the javadoc, are updated to match. Constructors still do not count towards methodDefinitionAllowed, and annotation members remain unvisited; both are separable changes.
…ted into a synthetic method SecureASTCustomizer skips synthetic methods when visiting method bodies, which is right for the many members the compiler generates but wrong when a transformation has relocated code the author wrote into one. ConditionalInterruptibleASTTransformation does exactly that: it lifts the closure supplied to @ConditionalInterrupt into a private synthetic method and calls it at every method start and every loop. With disallowedReceivers = ['java.lang.System'] configured, that closure was permitted, while the same call written in a method body was rejected. This is about consistency rather than catching more code. After GROOVY-12238 the restrictions reach method bodies, constructor bodies, static and instance initializers, field initializers, closures relocated into a generated constructor by @TupleConstructor(pre=...), and groovy-contracts conditions inlined into loop bodies. @ConditionalInterrupt was the sole exception, and nothing visible to whoever configured the customizer explained why: the difference is that one transformation relocates into a synthetic method while its neighbours relocate into constructors, ordinary methods or generated classes. Add visitSyntheticMethods(), applying the securing visitor to the statements of a synthetic method which carry a source position. Generated statements carry none and are skipped, so accessors, delegate forwarders, record components, enum machinery and trait bridges remain exempt -- a scan of those constructs found no synthetic method holding source-positioned code except the one @ConditionalInterrupt creates. <clinit> continues to be handled by visitConstructorsAndInitializers. Measured across every .groovy file under src/test (1632 files, those using @grab excluded) with a customizer restricting System, Thread, Runtime and ProcessBuilder: verdicts identical to before the change, with 78 rejections occurring throughout, so the new traversal was exercised and produced no false positive. No file was newly caught, since none combines @ConditionalInterrupt with a restriction -- the case for the change rests on uniform treatment, not on catching more code. Tests cover the relocated condition and pin the exemption for ordinary generated synthetic methods, so a later simplification cannot drop the source-position filter unnoticed. Both Limitations sections are updated.
dc2144a to
328d5d4
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #2779 +/- ##
==================================================
+ Coverage 70.0527% 70.0688% +0.0160%
- Complexity 35654 35674 +20
==================================================
Files 1560 1560
Lines 131885 131919 +34
Branches 24209 24220 +11
==================================================
+ Hits 92389 92434 +45
+ Misses 31143 31132 -11
Partials 8353 8353
🚀 New features to boost your workflow:
|
✅ All tests passed ✅🏷️ Commit: 328d5d4 Learn more about TestLens at testlens.app. |
No description provided.