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

RyanSkraba pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/avro.git


The following commit(s) were added to refs/heads/main by this push:
     new cd998891e5 AVRO-4311: [java] Escape backslashes in generated Javadoc 
(#3880)
cd998891e5 is described below

commit cd998891e595bf8b2444f538e84d9fd7a0336a3f
Author: Ismaël Mejía <[email protected]>
AuthorDate: Sun Aug 2 15:31:02 2026 +0200

    AVRO-4311: [java] Escape backslashes in generated Javadoc (#3880)
    
    * AVRO-4311: [java] Escape backslashes in generated Javadoc
    
    The code generator writes schema documentation strings into the
    Javadoc comments of generated Java sources. escapeForJavadoc escaped
    the comment terminator and HTML metacharacters but left backslashes
    untouched.
    
    The Java compiler translates Unicode escapes (\uXXXX) across the whole
    source file, including inside comments, before comments are recognized
    (JLS 3.3). A documentation string containing backslash sequences could
    therefore be reinterpreted by the compiler and change the generated
    source in unintended ways.
    
    Neutralize backslashes in escapeForJavadoc by encoding them as the
    HTML entity &#92;, so documentation content is always emitted as inert
    text. Add a regression test covering several escape forms. The
    string-literal path (escapeForJavaString) already doubles backslashes
    and is unaffected.
    
    * AVRO-4311: [java] Strengthen Javadoc escaping regression test
    
    Address review feedback: the end-to-end assertion only inspected lines
    starting with "/**" or "*", so the middle physical lines of a
    multi-line Javadoc block (a doc containing newlines) were not checked.
    
    Replace the line-based scan with one that removes all Java string
    literals (the embedded schema, which legitimately contains doubled
    backslashes) and then asserts no backslash remains in the surrounding
    code and comments. Add a doc vector that spans multiple physical lines.
---
 .../avro/compiler/specific/SpecificCompiler.java   | 14 ++++-
 .../compiler/specific/TestSpecificCompiler.java    | 70 ++++++++++++++++++++++
 2 files changed, 82 insertions(+), 2 deletions(-)

diff --git 
a/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java
 
b/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java
index 0295c14231..32a7d97cca 100644
--- 
a/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java
+++ 
b/lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java
@@ -1127,10 +1127,20 @@ public class SpecificCompiler {
   }
 
   /**
-   * Utility for template use. Escapes comment end with HTML entities.
+   * Utility for template use. Escapes content emitted into a Javadoc comment.
+   *
+   * <p>
+   * As well as escaping the comment terminator ({@code *}{@code /}) and HTML
+   * metacharacters, this neutralizes backslashes. This is required because the
+   * Java compiler translates Unicode escapes (of the form {@code \}{@code 
uXXXX})
+   * across the whole source file, including inside comments, as its first 
lexical
+   * step (JLS &sect;3.3). Without this, a schema doc value such as
+   * {@code \}{@code u002a\}{@code u002f} would be decoded by the compiler to
+   * {@code *}{@code /}, prematurely closing the comment and allowing arbitrary
+   * code to be injected into the generated source.
    */
   public static String escapeForJavadoc(String s) {
-    return s.replace("*/", "*&#47;").replace("<", "&lt;").replace(">", "&gt;");
+    return s.replace("\\", "&#92;").replace("*/", "*&#47;").replace("<", 
"&lt;").replace(">", "&gt;");
   }
 
   /**
diff --git 
a/lang/java/compiler/src/test/java/org/apache/avro/compiler/specific/TestSpecificCompiler.java
 
b/lang/java/compiler/src/test/java/org/apache/avro/compiler/specific/TestSpecificCompiler.java
index d9369fab03..8d939f0a51 100644
--- 
a/lang/java/compiler/src/test/java/org/apache/avro/compiler/specific/TestSpecificCompiler.java
+++ 
b/lang/java/compiler/src/test/java/org/apache/avro/compiler/specific/TestSpecificCompiler.java
@@ -1063,6 +1063,76 @@ public class TestSpecificCompiler {
     assertTrue(validAnnotationEmitted, "Valid annotation missing from 
generated output");
   }
 
+  @Test
+  void unicodeEscapesInDocsAreNeutralized() {
+    // The Java compiler decodes Unicode escapes (\ uXXXX) across the whole 
source
+    // file, including inside comments, before comments are recognized (JLS 
3.3).
+    // A doc value carrying the literal text "\ u002a\ u002f" therefore 
decodes to
+    // "*/" at compile time and could close the generated Javadoc comment 
early,
+    // enabling arbitrary code injection. Since a Unicode escape always 
requires a
+    // literal backslash, escapeForJavadoc neutralizes every backslash, which
+    // covers all escape variants at once.
+    String[] maliciousDocs = { //
+        "\\u002a\\u002f static { System.exit(1); } \\u002f\\u002a", // basic 
form
+        "\\uuuu002a\\uuuu002f System.exit(1);", // multiple 'u's are legal 
(JLS 3.3)
+        "\\u005cu002a\\u005cu002f System.exit(1);", // escape that would 
decode to a backslash
+        "\\U002A\\u002F", // uppercase hex / uppercase-U decoy
+        "prefix\\\\u002a\\\\u002f even-backslash-run", // even run of 
backslashes
+        "literal */ static { System.exit(1); } /* comment close", // no escape 
at all
+        "first line\\u002a\\u002f\nsecond line \\u002f\\u002a end" // spans 
multiple physical lines
+    };
+
+    for (String maliciousDoc : maliciousDocs) {
+      // Unit-level check on the escaping utility itself.
+      String escaped = SpecificCompiler.escapeForJavadoc(maliciousDoc);
+      assertFalse(escaped.contains("\\"), "Backslashes must be neutralized: " 
+ escaped);
+      assertFalse(escaped.contains("*/"), "Comment terminator must be 
neutralized: " + escaped);
+
+      // End-to-end check: no raw backslash may reach the generated source 
outside of
+      // string literals. A Java Unicode escape always requires a literal 
backslash,
+      // so the absence of backslashes everywhere except string literals 
proves no
+      // \ uXXXX sequence can be reconstituted by the compiler to close a 
comment.
+      Schema schema = 
SchemaBuilder.record("EvilRecord").namespace("org.apache.avro.codegentest.testdata")
+          
.doc(maliciousDoc).fields().name("field").doc(maliciousDoc).type().stringType().noDefault().endRecord();
+      Collection<SpecificCompiler.OutputFile> outputs = new 
SpecificCompiler(schema).compile();
+      assertEquals(1, outputs.size());
+      for (SpecificCompiler.OutputFile outputFile : outputs) {
+        // Remove Java string literals (the schema is embedded via 
escapeForJavaString,
+        // which doubles backslashes and is therefore immune) so that the 
remaining
+        // text is code and comments only. This checks every line of every 
Javadoc
+        // block, including the middle lines of a multi-line doc comment.
+        String withoutStringLiterals = 
removeJavaStringLiterals(outputFile.contents);
+        assertFalse(withoutStringLiterals.contains("\\"),
+            "Raw backslash reached generated code/comments: " + 
outputFile.path);
+      }
+    }
+  }
+
+  /**
+   * Returns the given Java source with the content of all double-quoted string
+   * literals removed, so tests can assert on code and comments without 
matching
+   * the (legitimately backslash-containing) embedded schema string literal.
+   */
+  private String removeJavaStringLiterals(String source) {
+    StringBuilder out = new StringBuilder(source.length());
+    boolean inString = false;
+    for (int i = 0; i < source.length(); i++) {
+      char c = source.charAt(i);
+      if (inString) {
+        if (c == '\\') {
+          i++; // skip the escaped character (e.g. \" or \\)
+        } else if (c == '"') {
+          inString = false;
+        }
+      } else if (c == '"') {
+        inString = true;
+      } else {
+        out.append(c);
+      }
+    }
+    return out.toString();
+  }
+
   private int countOccurrences(Pattern pattern, String textToSearch) {
     int count = 0;
     for (Matcher matcher = pattern.matcher(textToSearch); matcher.find();) {

Reply via email to