You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/09/03 06:08:33 UTC

(groovy) branch master updated (b0fb70bfc9 -> 0a2d6ac61f)

This is an automated email from the ASF dual-hosted git repository.

paulk-asert pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


    from b0fb70bfc9 GROOVY-12309: Preserve Object[] closure call semantics
     new 734a225be8 THREAT_MODEL: cover the two listeners an operator can start (tmd-2)
     new 0a2d6ac61f GROOVY-12347: DomToGroovy: escape document text on its way into generated source

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 THREAT_MODEL.md                                    | 50 ++++++++++++++++---
 .../org/apache/groovy/xml/tools/DomToGroovy.java   | 46 ++++++++++++++---
 .../groovy-xml/src/spec/doc/xml-userguide.adoc     | 18 ++++---
 .../apache/groovy/xml/tools/DomToGroovyTest.groovy | 57 ++++++++++++++++++++++
 4 files changed, 150 insertions(+), 21 deletions(-)


(groovy) 02/02: GROOVY-12347: DomToGroovy: escape document text on its way into generated source

Posted by pa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 0a2d6ac61f035d5875efbe3f2fd1f32482ad93a7
Author: Paul King <pa...@asert.com.au>
AuthorDate: Thu Sep 3 14:13:05 2026 +1000

    GROOVY-12347: DomToGroovy: escape document text on its way into generated source
    
    Three places wrote document content into the generated script without
    escaping it, so text that was ordinary in XML became syntax in Groovy.
    
    printQuoted escaped the single-line branch and not the triple-quoted one,
    where a backslash still starts an escape sequence: XML text holding a
    Windows path round-tripped as a tab, a newline and the remains of the
    path, and text holding three quotes produced source that did not compile.
    It now escapes backslashes, and quotes only where they would run into the
    closing delimiter, so apostrophes in prose stay readable.
    
    printComment wrote a comment's data into a Groovy block comment, so a
    comment carrying the terminator closed it early and left the rest of the
    document standing as code. The terminator is now broken up.
    
    printPI placed a processing instruction's target and data between quotes
    it did not escape. Both now go through printQuoted, which quotes them,
    escapes them, and handles data spanning lines.
    
    The class javadoc described a SAX handler; the class takes a DOM Document
    and produces MarkupBuilder calls to read and edit. It now says so, and
    says that generating from a document and evaluating the result runs
    whatever that document produced.
    
    The user guide offered the conversion as a way to "automate generation"
    and demonstrated evaluating the output directly. It now presents the tool
    as an authoring aid whose output is a starting point, and qualifies the
    evaluation in the example. Its class name and package were left over from
    before the 2019 rename.
---
 .../org/apache/groovy/xml/tools/DomToGroovy.java   | 46 ++++++++++++++---
 .../groovy-xml/src/spec/doc/xml-userguide.adoc     | 18 ++++---
 .../apache/groovy/xml/tools/DomToGroovyTest.groovy | 57 ++++++++++++++++++++++
 3 files changed, 107 insertions(+), 14 deletions(-)

diff --git a/subprojects/groovy-xml/src/main/java/org/apache/groovy/xml/tools/DomToGroovy.java b/subprojects/groovy-xml/src/main/java/org/apache/groovy/xml/tools/DomToGroovy.java
index 535f509425..657b7c721d 100644
--- a/subprojects/groovy-xml/src/main/java/org/apache/groovy/xml/tools/DomToGroovy.java
+++ b/subprojects/groovy-xml/src/main/java/org/apache/groovy/xml/tools/DomToGroovy.java
@@ -47,7 +47,14 @@ import java.util.HashMap;
 import java.util.Map;
 
 /**
- * A SAX handler for turning XML into Groovy scripts
+ * Turns a DOM document into the {@link groovy.xml.MarkupBuilder} calls that would produce
+ * it, as an aid to writing builder code by hand. The result is a starting point to edit
+ * rather than a finished script: names that are not valid Groovy identifiers are quoted,
+ * and the layout is meant to be read.
+ * <p>
+ * The output is Groovy source. Generating it from a document and then evaluating it runs
+ * whatever that document produced, so generation belongs in authoring, not in a runtime
+ * path fed by documents the author does not control.
  */
 public class DomToGroovy {
 
@@ -289,7 +296,7 @@ public class DomToGroovy {
     protected void printQuoted(String text) {
         if (text.contains("\n")) {
             print("'''");
-            print(text);
+            print(escapeTripleQuote(text));
             print("'''");
         } else {
             print(qt);
@@ -306,11 +313,11 @@ public class DomToGroovy {
      */
     protected void printPI(ProcessingInstruction instruction, boolean endWithComma) {
         printIndent();
-        print("mkp.pi(" + qt);
-        print(instruction.getTarget());
-        print(qt + ", " + qt);
-        print(instruction.getData());
-        printEnd(qt + ");", endWithComma);
+        print("mkp.pi(");
+        printQuoted(instruction.getTarget());
+        print(", ");
+        printQuoted(instruction.getData());
+        printEnd(");", endWithComma);
     }
 
     /**
@@ -324,7 +331,9 @@ public class DomToGroovy {
         if (!text.isEmpty()) {
             printIndent();
             print("/* ");
-            print(text);
+            // a comment carrying the block-comment terminator would close this comment
+            // early and leave the rest of the document standing as code
+            print(text.replace("*/", "* /"));
             printEnd(" */", endWithComma);
         }
     }
@@ -355,6 +364,27 @@ public class DomToGroovy {
         return text.replaceAll("\\\\", "\\\\\\\\").replaceAll(qt, "\\\\" + qt);
     }
 
+    /**
+     * Escapes text for a triple-quoted Groovy string literal. A backslash always starts an
+     * escape sequence and so needs escaping; a quote needs it only where it would otherwise
+     * run into the closing delimiter, which leaves apostrophes in ordinary prose alone.
+     *
+     * @param text raw text
+     * @return escaped text
+     */
+    protected String escapeTripleQuote(String text) {
+        String escaped = text.replace("\\", "\\\\");
+        StringBuilder result = new StringBuilder(escaped.length());
+        for (int i = 0; i < escaped.length(); i += 1) {
+            char c = escaped.charAt(i);
+            if (c == '\'' && (i + 1 == escaped.length() || escaped.charAt(i + 1) == '\'')) {
+                result.append('\\');
+            }
+            result.append(c);
+        }
+        return result.toString();
+    }
+
     /**
      * Returns the namespace map extended with any newly encountered element or attribute prefixes.
      *
diff --git a/subprojects/groovy-xml/src/spec/doc/xml-userguide.adoc b/subprojects/groovy-xml/src/spec/doc/xml-userguide.adoc
index 79fb376409..cee46193d7 100644
--- a/subprojects/groovy-xml/src/spec/doc/xml-userguide.adoc
+++ b/subprojects/groovy-xml/src/spec/doc/xml-userguide.adoc
@@ -510,20 +510,26 @@ include::../test/UserGuideStreamingMarkupBuilderTest.groovy[tags=testMkp,indent=
 `mkp.yield`
 <2> Checking the values later on with `XmlSlurper`
 
-=== DOMToGroovy
+=== DomToGroovy
 
-Suppose we have an existing XML document and we want to automate
-generation of the markup without having to type it all in? We just
-need to use `org.codehaus.groovy.tools.xml.DOMToGroovy` as shown in
+Suppose we have an existing XML document and we want a head start on
+writing the markup rather than typing it all in? We just
+need to use `org.apache.groovy.xml.tools.DomToGroovy` as shown in
 the following example:
 
+`DomToGroovy` is an authoring aid. It produces builder code to read and edit,
+so the generated source is a starting point rather than a finished script.
+The example below evaluates it directly to show that the round trip works;
+do that only for documents you control, since evaluating generated source
+runs whatever the document produced.
+
 [source,groovy]
-.Building MarkupBuilder from DOMToGroovy
+.Building MarkupBuilder from DomToGroovy
 ----
 include::../test/UserGuideMarkupBuilderTest.groovy[tags=testDOMToGroovy,indent=0]
 ----
 
-<1> Creating `DOMToGroovy` instance
+<1> Creating `DomToGroovy` instance
 <2> Converts the XML to `MarkupBuilder` calls which are available in the output `StringWriter`
 <3> Using `output` variable to create the whole MarkupBuilder
 <4> Back to XML string
diff --git a/subprojects/groovy-xml/src/test/groovy/org/apache/groovy/xml/tools/DomToGroovyTest.groovy b/subprojects/groovy-xml/src/test/groovy/org/apache/groovy/xml/tools/DomToGroovyTest.groovy
index a5478859dd..dcb8c3b818 100644
--- a/subprojects/groovy-xml/src/test/groovy/org/apache/groovy/xml/tools/DomToGroovyTest.groovy
+++ b/subprojects/groovy-xml/src/test/groovy/org/apache/groovy/xml/tools/DomToGroovyTest.groovy
@@ -19,6 +19,7 @@
 package org.apache.groovy.xml.tools
 
 import groovy.test.StringTestUtil
+import groovy.xml.XmlSlurper
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import org.w3c.dom.Document
@@ -107,6 +108,62 @@ class DomToGroovyTest {
         checkConversion(TEST_XML_5, EXPECTED_BUILDER_SCRIPT_5)
     }
 
+    @Test
+    void multilineTextSurvivesTheRoundTrip() {
+        // multi-line content is emitted in a triple-quoted literal, where a backslash
+        // still starts an escape sequence and a run of quotes can close the literal early
+        ['line one\nC:\\temp\\new',
+         'line one\n' + "'" * 3,
+         "line one\nends with a quote'",
+         "line one\ndon't touch ordinary prose"].each { text ->
+            assert roundTrip(text) == text
+        }
+    }
+
+    @Test
+    void apostrophesInProseAreNotEscaped() {
+        // the generated source is meant to be read and edited, so only quotes that would
+        // run into the closing delimiter are escaped
+        assert generate("line one\ndon't split ordinary prose").contains("don't split")
+    }
+
+    @Test
+    void commentContentCannotCloseTheGeneratedComment() {
+        String generated = generateFrom('<a><!-- */ marker() /* --><b>ok</b></a>')
+        assert !generated.contains('*/ marker')
+        assert generated.contains('* /')
+    }
+
+    @Test
+    void processingInstructionDataIsQuoted() {
+        String generated = generateFrom("<a><?target it's data?><b>ok</b></a>")
+        // the apostrophe must not close the literal the data sits in
+        assert generated.contains("it\\'s data")
+    }
+
+    private String generateFrom(String xml) {
+        Document document = builder.parse(new ByteArrayInputStream(xml.bytes))
+        StringWriter writer = new StringWriter()
+        new DomToGroovy(new PrintWriter(writer)).print(document)
+        writer.toString().trim()
+    }
+
+    private String roundTrip(String text) {
+        String recovered = new GroovyShell().evaluate(
+                'def out = new StringWriter()\n' +
+                'def mb = new groovy.xml.MarkupBuilder(out)\n' +
+                'mb.' + generate(text) + '\n' +
+                'out.toString()')
+        new XmlSlurper().parseText(recovered).b.text()
+    }
+
+    private String generate(String text) {
+        Document document = builder.parse(new ByteArrayInputStream("<a><b>${text}</b></a>".bytes))
+        StringWriter writer = new StringWriter()
+        new DomToGroovy(new PrintWriter(writer)).print(document)
+        writer.toString().trim()
+    }
+
     private void checkConversion(String testXml, String expectedScript) throws SAXException, IOException {
         ByteArrayInputStream inputStream = new ByteArrayInputStream(testXml.getBytes())
         Document document = builder.parse(inputStream)


(groovy) 01/02: THREAT_MODEL: cover the two listeners an operator can start (tmd-2)

Posted by pa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 734a225be8012ffc9f495e97d44bb2f5b8f74a84
Author: Paul King <pa...@asert.com.au>
AuthorDate: Thu Sep 3 14:41:46 2026 +1000

    THREAT_MODEL: cover the two listeners an operator can start (tmd-2)
    
    The model said Groovy is not deployed as a network service and listed such
    a tool as a future condition, while `groovy -l` and JmxBuilder connector
    servers already shipped. Both are development tools rather than services:
    they run only where an operator started one, they bind loopback unless
    given a wider address, and neither evaluates what a peer sends as code.
    They belong with the shell and console tools in §9, so §2 carries them in
    the interactive tools row rather than as an exception to the framing.
    
    §7 gains the peer as an adversary and the closed capability list gains
    connecting to such a listener. The two differ in what they offer: a line
    reaches `groovy -l` as the `line` binding and a script that evaluates it
    has opted into §3, whereas a connector server offers whatever its MBean
    server holds, which for the platform server includes dumping the heap and
    running diagnostic commands.
    
    §9 records that neither authenticates or encrypts by default, and gives
    connector servers their own false-friends row: every connector security
    property was assembled and discarded between 2008 and 2026, so no
    configuration produced a secured connector in that window and none can
    have been relied upon.
    
    §10 tells whoever widens a listener what they take on, and §12 says why a
    loopback-bound development tool does not meet its condition and what would.
---
 THREAT_MODEL.md | 50 +++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 43 insertions(+), 7 deletions(-)

diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md
index be9d9a3f86..96ac442f62 100644
--- a/THREAT_MODEL.md
+++ b/THREAT_MODEL.md
@@ -90,7 +90,7 @@ not deployed as a network service of its own. The shapes that matter:
 | **Embedded runtime** | An application puts `groovy-*` jars on its classpath and calls Groovy APIs (GDK methods, `groovy.sql.Sql`, parsers, builders) and/or evaluates Groovy it authored. |
 | **Dynamic evaluation host** | An application uses `GroovyShell`, `Eval`, `GroovyClassLoader`, `GroovyScriptEngine`, JSR-223 (`groovy-jsr223`), or the template engines to run Groovy decided at runtime. |
 | **Build / compile** | `groovyc`, the Gradle/Maven integrations, and AST transforms compile developer-authored source. |
-| **Interactive / CLI tools** | `groovy`, `groovysh`, `groovyConsole`, `groovydoc`, `groovy-servlet` run on inputs the operator provides. |
+| **Interactive / CLI tools** | `groovy`, `groovysh`, `groovyConsole`, `groovydoc`, `groovy-servlet` run on inputs the operator provides. `groovy -l` and `JmxBuilder`'s connector server belong here too: they take those inputs from a socket rather than a terminal, bound to loopback unless the operator widens them ([§7](#7-adversary-model)). |
 
 ### Caller roles
 
@@ -385,12 +385,28 @@ single "remote attacker." The adversaries that matter:
 - **A local user on a shared host.** Can read predictable or
   world-readable artifacts that Groovy tooling writes (the class of issue
   behind CVE-2020-17521).
+- **A peer of a listener the operator started.** Two development tools
+  listen on a socket when asked to: `groovy -l`, which runs the operator's
+  script once per line received with the line supplied as the `line`
+  binding, and `JmxBuilder`'s connector server, which exposes an MBean
+  server. Both exist only where an operator started one, both bind the
+  loopback address unless given a wider one, and neither evaluates what a
+  peer sends as code. For `groovy -l` this is the data supplier above
+  reached over a socket Groovy opened rather than through the embedding
+  application, carrying the same capability and no more; what a peer
+  achieves is whatever the operator's script does with `line`, and a script
+  that evaluates it has opted into [§3](#3-out-of-scope-explicit-non-goals).
+  A connector server is the wider of the two: it offers whatever the MBean
+  server holds, and the default is the platform server, whose beans can dump
+  the heap and run diagnostic commands. Widening either past loopback puts
+  that within reach of any peer that can route to the port.
 
 **Capabilities the in-scope adversary has (closed list).** Supplying bytes
 to a data parser; supplying values bound as data (SQL parameters, template
 *bindings* — not template *text*); supplying oversized, deeply-nested, or
-syntactically adversarial *data*; and, for the local adversary, reading
-files on the shared host subject to OS permissions. **A finding that
+syntactically adversarial *data*; connecting to a listener the operator
+started and sending it lines or MBean requests; and, for the local
+adversary, reading files on the shared host subject to OS permissions. **A finding that
 requires any capability not on this list is out of model**
 ([§11b](#11b-scanner-calibration-default-downgrade-rules)).
 
@@ -499,6 +515,11 @@ Groovy does **not** claim, and you must not assume, any of the following.
   objects.** See [§3](#3-out-of-scope-explicit-non-goals).
 - **`SecurityManager`-based confinement.** The JDK `SecurityManager` is
   deprecated/removed; Groovy does not rely on it and neither can you.
+- **Authentication or transport security for the listeners.** Neither
+  `groovy -l` nor a `JmxBuilder` connector authenticates a peer by default,
+  and neither encrypts unless configured to. Binding loopback by default
+  limits who can arrive; beyond loopback the operator owns authentication,
+  and the port belongs behind whatever the deployment already uses.
 
 ### False friends — features that look like security but are not
 
@@ -508,7 +529,8 @@ Groovy does **not** claim, and you must not assume, any of the following.
 | `@ThreadInterrupt` / `@TimedInterrupt` / `@ConditionInterrupt` | Cooperative interruption checks woven into loops/methods to tame *runaway* (buggy) scripts | Cooperative and removable by the code being run; no defense against deliberately malicious scripts. |
 | `@CompileStatic` / `@TypeChecked` | Compile-time type checking for correctness and performance | Not a security control; statically-compiled code is exactly as privileged. |
 | `groovy.sql.Sql` (beyond P1) | Convenience SQL access | Only the GString/bind forms parameterize. A `String` you concatenate yourself, dynamic identifiers via `Sql.expand`, or `DataSet` table/column names, are **not** protected (CWE-89). |
-| The Groovy "shell"/console tools | Developer/operator conveniences | `groovysh`/`groovyConsole` run code with the user's full privileges; they are not multi-tenant or sandboxed. |
+| The Groovy "shell"/console tools | Developer/operator conveniences | `groovysh`/`groovyConsole` run code with the user's full privileges; they are not multi-tenant or sandboxed. `groovy -l` is the same kind of tool with a socket for an input channel instead of a terminal &mdash; an aid to experimenting with a line-driven script, not an application server. |
+| `JmxBuilder` connector servers | A development and experimentation aid | Every connector security property was assembled and discarded from 2008 until 2026, so no configuration produced a secured connector in that window and none can have been relied upon. It exposes an MBean server rather than being a managed service, and authenticates nobody unless told to. |
 
 Well-known attack classes Groovy does **not** defend against on the
 application's behalf: command injection from app-built command strings
@@ -575,6 +597,13 @@ If you embed or script with Groovy, **you** own these:
     your exposure, not Groovy's obligations** — a defect in a tool module is
     still a defect, and the interactive user is a caller role in this model
     ([§2](#2-scope-and-intended-use)), not an out-of-scope one.
+12. **If you start a listener, you own the exposure.** `groovy -l` and
+    `JmxBuilder` connector servers bind loopback unless you pass a wider
+    address, and neither authenticates anyone. Give one a reachable address
+    only where the peers are ones it is prepared to serve, and put
+    authentication in front of the port. A connector server reaches whatever
+    its MBean server holds, so prefer a purpose-built server over the
+    platform one when the port is not confined to the host.
 
 ---
 
@@ -682,9 +711,16 @@ carries a hardening obligation.
   classes (would create a deserialization surface that *is* in model).
 - Changing the secure-by-default XML posture (P2) or temp-artifact
   permissions (P4).
-- A new tool or subproject that accepts input over the network as a
-  service (would introduce a remote network adversary the current model
-  does not have).
+- A new tool or subproject that accepts input over the network **as a
+  service** (would introduce a remote network adversary the current model
+  does not have). `groovy -l` and `JmxBuilder` connector servers do not
+  meet this: they are development tools of the
+  [§9](#9-security-properties-the-project-does-not-provide) shell/console
+  family, bound to loopback by default, and their peer is covered by the
+  listener bullet in [§7](#7-adversary-model). What would meet it is a
+  surface accepting something richer — code, serialized objects, or
+  commands Groovy itself interprets rather than the operator's script — or
+  one listening beyond loopback by default.
 - Hardening of serializable runtime types (e.g. making closure fields
   transient, or removing `Serializable`) — would move some [§11a](#11a-known-non-findings-recurring-false-positives)
   items into scope as defense-in-depth.