Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
private static final String FILE_HEADER = "/*\n * Autogenerated by Avro\n *\n * DO NOT EDIT DIRECTLY\n */\n";

public SpecificCompiler(Protocol protocol) {
this();

Check warning on line 151 in lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java

View workflow job for this annotation

GitHub Actions / Java Test (ubuntu-latest)

[this-escape] possible 'this' escape before subclass is fully initialized
// enqueue all types
for (Schema s : protocol.getTypes()) {
enqueue(s);
Expand All @@ -161,7 +161,7 @@
}

public SpecificCompiler(Collection<Schema> schemas) {
this();

Check warning on line 164 in lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java

View workflow job for this annotation

GitHub Actions / Java Test (ubuntu-latest)

[this-escape] possible 'this' escape before subclass is fully initialized
for (Schema schema : schemas) {
enqueue(schema);
}
Expand All @@ -182,7 +182,7 @@
this.templateDir = System.getProperty("org.apache.avro.specific.templates",
"/org/apache/avro/compiler/specific/templates/java/classic/");
initializeVelocity();
initializeSpecificData();

Check warning on line 185 in lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java

View workflow job for this annotation

GitHub Actions / Java Test (ubuntu-latest)

[this-escape] previous possible 'this' escape happens here via invocation

Check warning on line 185 in lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java

View workflow job for this annotation

GitHub Actions / Java Test (ubuntu-latest)

[this-escape] previous possible 'this' escape happens here via invocation
}

/**
Expand Down Expand Up @@ -427,7 +427,7 @@
}

private void initializeSpecificData() {
addLogicalTypeConversions(specificData);

Check warning on line 430 in lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java

View workflow job for this annotation

GitHub Actions / Java Test (ubuntu-latest)

[this-escape] previous possible 'this' escape happens here via invocation

Check warning on line 430 in lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java

View workflow job for this annotation

GitHub Actions / Java Test (ubuntu-latest)

[this-escape] previous possible 'this' escape happens here via invocation
specificData.addLogicalTypeConversion(new Conversions.DecimalConversion());
}

Expand Down Expand Up @@ -940,7 +940,7 @@
// with error(s)
return "void";
}
default:

Check warning on line 943 in lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java

View workflow job for this annotation

GitHub Actions / Java Test (ubuntu-latest)

[fallthrough] possible fall-through into case

Check warning on line 943 in lang/java/compiler/src/main/java/org/apache/avro/compiler/specific/SpecificCompiler.java

View workflow job for this annotation

GitHub Actions / Java Test (ubuntu-24.04-arm)

[fallthrough] possible fall-through into case
return javaType(schema, false);
}
}
Expand Down Expand Up @@ -1109,10 +1109,20 @@
}

/**
* 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;");
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,76 @@ void docsAreEscaped_avro4053() {
}
}

@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();) {
Expand Down
Loading