Skip to content

GROOVY-12273: Write config keys and values as data, not as source - #2810

Merged
paulk-asert merged 1 commit into
apache:masterfrom
paulk-asert:groovy12273
Aug 19, 2026
Merged

GROOVY-12273: Write config keys and values as data, not as source#2810
paulk-asert merged 1 commit into
apache:masterfrom
paulk-asert:groovy12273

Conversation

@paulk-asert

Copy link
Copy Markdown
Contributor

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.

@codecov-commenter

codecov-commenter commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.50000% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.1864%. Comparing base (5f56279) to head (d660b6c).
⚠️ Report is 12 commits behind head on master.

Files with missing lines Patch % Lines
src/main/java/groovy/util/ConfigObject.java 87.5000% 2 Missing and 3 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@                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     
Files with missing lines Coverage Δ
src/main/java/groovy/util/ConfigObject.java 85.8696% <87.5000%> (+1.2542%) ⬆️

... and 18 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This comment was marked as outdated.

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.
@testlens-app

testlens-app Bot commented Aug 19, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: d660b6c
▶️ Tests: 110365 executed
⚪️ Checks: 31/31 completed


Learn more about TestLens at testlens.app.

@paulk-asert
paulk-asert merged commit f7afa50 into apache:master Aug 19, 2026
32 checks passed
@paulk-asert
paulk-asert deleted the groovy12273 branch August 19, 2026 21:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants