GROOVY-12273: Write config keys and values as data, not as source - #2810
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #2810 +/- ##
==================================================
+ Coverage 70.1516% 70.1864% +0.0348%
- Complexity 35828 35875 +47
==================================================
Files 1562 1562
Lines 132523 132597 +74
Branches 24379 24393 +14
==================================================
+ Hits 92967 93065 +98
+ Misses 31140 31121 -19
+ Partials 8416 8411 -5
🚀 New features to boost your workflow:
|
ConfigObject.writeTo documents a round trip with ConfigSlurper.parse, which compiles its output as a Groovy script. Keys were written bare unless they were Groovy keywords, and values were rendered by FormatHelper.inspect, which quotes a String but not other types. What that produced was source rather than data, and it was read back as whatever it happened to parse as. Measured before the change, writing a ConfigObject and parsing it back: key 'a b' did not parse key "a'b" did not parse key "x = <statement>; y" *** executed on re-parse *** nested block under key 'a b' did not parse GString value holding a dollar did not parse StringBuilder value did not parse GString inside a list came back altered a value of any other type did not parse The executing case is the one that matters: a key is data, and an application which stores an attacker-influenced entry name and later persists the configuration would run it. Render every key as an identifier when it is one and as a quoted literal otherwise, rather than only quoting keywords, and give a quoted leading key the receiver it needs to open a statement, which is what keyword keys have always been given. Nested blocks accept a quoted key unchanged, so only the rendering moved. writeValue now receives a key path whose components have already been rendered, because it is also called with a composed path and must not quote the path as a whole. Carry a value which has no literal form over to its text so that it is rendered as a quoted String: a CharSequence which is not a String would otherwise be written double quoted, where a dollar is live, and a value of any other type would be written as a bare toString(). Collections and maps are converted through, which covers the same value nested inside them. Numbers and booleans already write as themselves and are untouched.
paulk-asert
force-pushed
the
groovy12273
branch
from
August 19, 2026 00:48
30e5acb to
d660b6c
Compare
✅ All tests passed ✅🏷️ Commit: d660b6c Learn more about TestLens at testlens.app. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ConfigObject.writeTo documents a round trip with ConfigSlurper.parse, which compiles its output as a Groovy script. Keys were written bare unless they were Groovy keywords, and values were rendered by FormatHelper.inspect, which quotes a String but not other types. What that produced was source rather than data, and it was read back as whatever it happened to parse as.
Measured before the change, writing a ConfigObject and parsing it back:
key 'a b' did not parse
key "a'b" did not parse
key "x = ; y" *** executed on re-parse ***
nested block under key 'a b' did not parse
GString value holding a dollar did not parse
StringBuilder value did not parse
GString inside a list came back altered
a value of any other type did not parse
The executing case is the one that matters: a key is data, and an application which stores an attacker-influenced entry name and later persists the configuration would run it.
Render every key as an identifier when it is one and as a quoted literal otherwise, rather than only quoting keywords, and give a quoted leading key the receiver it needs to open a statement, which is what keyword keys have always been given. Nested blocks accept a quoted key unchanged, so only the rendering moved. writeValue now receives a key path whose components have already been rendered, because it is also called with a composed path and must not quote the path as a whole.
Carry a value which has no literal form over to its text so that it is rendered as a quoted String: a CharSequence which is not a String would otherwise be written double quoted, where a dollar is live, and a value of any other type would be written as a bare toString(). Collections and maps are converted through, which covers the same value nested inside them. Numbers and booleans already write as themselves and are untouched.