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

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


The following commit(s) were added to refs/heads/branch-1.12 by this push:
     new 7cd02f6fd5 AVRO-4313: [java] Tighten javaAnnotation string-literal 
validation in SpecificCompiler (#3892)
7cd02f6fd5 is described below

commit 7cd02f6fd5479c613fab7f80a4b4d2c61aba1ec3
Author: Ismaël Mejía <[email protected]>
AuthorDate: Sun Jul 26 19:08:53 2026 +0200

    AVRO-4313: [java] Tighten javaAnnotation string-literal validation in 
SpecificCompiler (#3892)
    
    * AVRO-4313: [java] Tighten javaAnnotation string-literal validation in 
SpecificCompiler
    
    The string-literal grammar used to validate javaAnnotation values accepted
    an unescaped quote inside the literal body, letting a single literal span
    past its intended closing quote and absorb surrounding tokens. Constrain the
    body to recognized escape sequences or characters that are not a quote,
    backslash, or line terminator, and add a regression test.
    
    * AVRO-4313: Exclude all line terminators from annotation string literals
    
    Also reject NEL, LS and PS in addition to CR and LF so an annotation value
    cannot span multiple lines in the generated source.
    
    * AVRO-4313: Make injection regression test robust to multiple outputs
    
    Assert the injection payload is absent from every generated file and the
    valid annotation is emitted in at least one, rather than requiring it in
    each output file.
---
 .../avro/compiler/specific/SpecificCompiler.java   |  8 ++++++-
 .../compiler/specific/TestSpecificCompiler.java    | 27 ++++++++++++++++++++++
 2 files changed, 34 insertions(+), 1 deletion(-)

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 ea1e1a11b5..4cfe1eea48 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
@@ -1054,7 +1054,13 @@ public class SpecificCompiler {
   private static final String PATTERN_IDENTIFIER_PART = 
"\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*";
   private static final String PATTERN_IDENTIFIER = 
String.format("(?:%s(?:\\.%s)*)", PATTERN_IDENTIFIER_PART,
       PATTERN_IDENTIFIER_PART);
-  private static final String PATTERN_STRING = 
"\"(?:\\\\[\\\\\"ntfb]|(?<!\\\\).)*\"";
+  // A string literal is a quote, a body of escape sequences or characters that
+  // are not a quote, backslash or line terminator, and a closing quote. The 
body
+  // must not be able to contain an unescaped quote, otherwise a single literal
+  // could span past the intended closing quote and swallow surrounding tokens.
+  // Line terminators (CR, LF, NEL, LS, PS) are excluded so a value cannot 
break
+  // across lines in the generated source.
+  private static final String PATTERN_STRING = 
"\"(?:\\\\[\\\\\"ntfb]|[^\"\\\\\\r\\n\\x85\\x{2028}\\x{2029}])*\"";
   private static final String PATTERN_NUMBER = 
"(?:\\((?:byte|char|short|int|long|float|double)\\))?[x0-9_.]*[fl]?";
   private static final String PATTERN_LITERAL_VALUE = 
String.format("(?:%s|%s|true|false)", PATTERN_STRING,
       PATTERN_NUMBER);
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 178f2fa210..918e28a895 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
@@ -1031,6 +1031,33 @@ public class TestSpecificCompiler {
     }
   }
 
+  @Test
+  void annotationCannotBreakOutViaStringLiteral() {
+    // A crafted javaAnnotation value tries to terminate the first annotation,
+    // inject arbitrary declarations plus a static initializer, then reopen a
+    // second valid annotation. It relies on a string literal spanning past its
+    // intended closing quote. Such values must be rejected, not emitted 
verbatim.
+    String jsonSchema = "{\n" + "  \"type\": \"record\",\n" + "  \"name\": 
\"Injected\",\n"
+        + "  \"javaAnnotation\": [\n"
+        + "    \"java.lang.SuppressWarnings(\\\"x\\\") static { 
System.exit(1); } @java.lang.SuppressWarnings(\\\"y\\\")\",\n"
+        + "    \"SuppressWarnings(\\\"unchecked\\\")\"\n" + "  ],\n" + "  
\"fields\": [\n"
+        + "    {\"name\": \"value\", \"type\": \"string\"}\n" + "  ]\n" + "}";
+    Collection<SpecificCompiler.OutputFile> outputs = new 
SpecificCompiler(SchemaParser.parseSingle(jsonSchema))
+        .compile();
+    boolean validAnnotationEmitted = false;
+    for (SpecificCompiler.OutputFile outputFile : outputs) {
+      // The payload is echoed (safely escaped) inside the SCHEMA$ string 
constant,
+      // so we must distinguish that from a verbatim emission as code. Real 
injected
+      // code would carry unescaped quotes; the schema literal escapes them as 
\".
+      // The injection must be absent from every generated file.
+      assertFalse(outputFile.contents.contains("SuppressWarnings(\"x\") static 
{ System.exit(1); }"),
+          "Code injection present? " + outputFile.contents);
+      validAnnotationEmitted |= 
outputFile.contents.contains("@SuppressWarnings(\"unchecked\")");
+    }
+    // The legitimate annotation in the same list must still be emitted 
somewhere.
+    assertTrue(validAnnotationEmitted, "Valid annotation missing from 
generated output");
+  }
+
   private int countOccurrences(Pattern pattern, String textToSearch) {
     int count = 0;
     for (Matcher matcher = pattern.matcher(textToSearch); matcher.find();) {

Reply via email to