This is an automated email from the ASF dual-hosted git repository. robertlazarski pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git
commit fd6366326d5d2fd662353bb16bf6cb4c658505d1 Author: Robert Lazarski <[email protected]> AuthorDate: Thu Sep 3 05:24:55 2026 -1000 Escape schema values before they reach generated source Defaults, fixed values, enumeration facets and the numeric range facets were copied out of the schema into generated Java and C inside string literals with no escaping, so a value carrying a quote closed its literal and the rest became code in a class-level initializer -- running when the bean class loads, on the build machine and in the shipped application. Escape them where they enter the template model, as the neighbouring pattern facet already was. The quote-comma- quote the compiler splices into a QName enumeration on purpose is preserved by escaping the halves either side of it. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> --- .../apache/axis2/schema/SourceLiteralEscaper.java | 109 +++++++++++++++++++++ .../apache/axis2/schema/writer/CStructWriter.java | 10 +- .../apache/axis2/schema/writer/JavaBeanWriter.java | 25 +++-- .../axis2/schema/SourceLiteralEscaperTest.java | 99 +++++++++++++++++++ src/site/markdown/release-notes/2.0.2.md | 9 ++ 5 files changed, 243 insertions(+), 9 deletions(-) diff --git a/modules/adb-codegen/src/org/apache/axis2/schema/SourceLiteralEscaper.java b/modules/adb-codegen/src/org/apache/axis2/schema/SourceLiteralEscaper.java new file mode 100644 index 0000000000..953a302122 --- /dev/null +++ b/modules/adb-codegen/src/org/apache/axis2/schema/SourceLiteralEscaper.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.axis2.schema; + +/** + * Escapes a schema-derived string so that it stays a string when the templates + * splice it into generated Java or C source. + * <p> + * Default and fixed values, enumeration facets and the numeric range facets are + * copied out of the schema and emitted inside string literals -- a field + * initializer such as {@code ConverterUtil.convertToInt("<value>")}, or a + * {@code ConverterUtil.compare(param, "<value>")} call. The schema is written by + * whoever authored the contract, so an unescaped quote closes the literal and the + * rest of the value becomes code in a class-level initializer, which runs when the + * bean class loads: on the machine that compiles the generated sources, and again + * wherever the built application runs. Nobody reads thousands of lines of generated + * stubs. + * <p> + * Escaping the backslash also disposes of the {@code \\uXXXX} route, because Java + * treats {@code \\u} as a unicode escape only when preceded by an even number of + * backslashes. + * <p> + * The escapes used -- backslash, quote, the named control escapes and three-digit + * octal -- mean the same thing in Java and in C, so one escaper serves both writers. + * {@code SchemaCompiler} has escaped the pattern facet this way for years; these are + * the neighbouring values that were missed. + */ +public final class SourceLiteralEscaper { + + /** + * The literal break {@code SchemaCompiler} splices into a QName enumeration on + * purpose, so that one facet value becomes two arguments to + * {@code ConverterUtil.convertToQName}. It has to survive escaping. + */ + private static final String QNAME_ARGUMENT_SPLICE = "\", \""; + + private SourceLiteralEscaper() { + } + + /** + * @param value a schema-derived string, may be null + * @return the value, safe to place inside a Java or C string literal + */ + public static String escape(String value) { + if (value == null) { + return null; + } + StringBuilder escaped = new StringBuilder(value.length() + 8); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + switch (c) { + case '\\': escaped.append("\\\\"); break; + case '"': escaped.append("\\\""); break; + case '\n': escaped.append("\\n"); break; + case '\r': escaped.append("\\r"); break; + case '\t': escaped.append("\\t"); break; + case '\b': escaped.append("\\b"); break; + case '\f': escaped.append("\\f"); break; + default: + if (c < 0x20 || c == 0x7F) { + // Three-digit octal, which both languages read the same way. + escaped.append('\\'); + escaped.append((char) ('0' + ((c >> 6) & 0x7))); + escaped.append((char) ('0' + ((c >> 3) & 0x7))); + escaped.append((char) ('0' + (c & 0x7))); + } else { + escaped.append(c); + } + } + } + return escaped.toString(); + } + + /** + * Escapes an enumeration facet value, leaving the deliberate QName argument + * split intact by escaping the text either side of it. + * + * @param value the registered facet value, may be null + * @return the value, safe to place inside a string literal + */ + public static String escapeEnumFacet(String value) { + if (value == null) { + return null; + } + int splice = value.indexOf(QNAME_ARGUMENT_SPLICE); + if (splice < 0) { + return escape(value); + } + return escape(value.substring(0, splice)) + + QNAME_ARGUMENT_SPLICE + + escape(value.substring(splice + QNAME_ARGUMENT_SPLICE.length())); + } +} diff --git a/modules/adb-codegen/src/org/apache/axis2/schema/writer/CStructWriter.java b/modules/adb-codegen/src/org/apache/axis2/schema/writer/CStructWriter.java index 1c3b93228e..be33187456 100644 --- a/modules/adb-codegen/src/org/apache/axis2/schema/writer/CStructWriter.java +++ b/modules/adb-codegen/src/org/apache/axis2/schema/writer/CStructWriter.java @@ -19,6 +19,7 @@ package org.apache.axis2.schema.writer; +import org.apache.axis2.schema.SourceLiteralEscaper; import org.apache.axis2.schema.BeanWriterMetaInfoHolder; import org.apache.axis2.schema.CompilerOptions; import org.apache.axis2.schema.SchemaCompilationException; @@ -647,8 +648,11 @@ public class CStructWriter implements BeanWriter { if (metainf.isDefaultValueAvailable(name)){ QName schemaQName = metainf.getSchemaQNameForQName(name); if (baseTypeMap.containsKey(schemaQName)){ + // Emitted inside a string literal in a field initializer, so + // the schema's text must not be able to close it. XSLTUtils.addAttribute(model, "defaultValue", - metainf.getDefaultValueForQName(name), property); + SourceLiteralEscaper.escape( + metainf.getDefaultValueForQName(name)), property); } } @@ -889,7 +893,9 @@ public class CStructWriter implements BeanWriter { int id = 0; for (String attribValue : metainf.getEnumFacet()) { Element enumFacet = XSLTUtils.addChildElement(model, "enumFacet", property); - XSLTUtils.addAttribute(model, "value", attribValue, enumFacet); + // Keeps the deliberate QName argument split intact. + XSLTUtils.addAttribute(model, "value", + SourceLiteralEscaper.escapeEnumFacet(attribValue), enumFacet); if (validJava) { XSLTUtils.addAttribute(model, "id", attribValue.toUpperCase(), enumFacet); } else { diff --git a/modules/adb-codegen/src/org/apache/axis2/schema/writer/JavaBeanWriter.java b/modules/adb-codegen/src/org/apache/axis2/schema/writer/JavaBeanWriter.java index cdc652a706..61415abc4b 100644 --- a/modules/adb-codegen/src/org/apache/axis2/schema/writer/JavaBeanWriter.java +++ b/modules/adb-codegen/src/org/apache/axis2/schema/writer/JavaBeanWriter.java @@ -21,6 +21,7 @@ package org.apache.axis2.schema.writer; import org.apache.axiom.om.OMAttribute; import org.apache.axiom.om.OMElement; +import org.apache.axis2.schema.SourceLiteralEscaper; import org.apache.axis2.schema.BeanWriterMetaInfoHolder; import org.apache.axis2.schema.CompilerOptions; import org.apache.axis2.schema.SchemaCompilationException; @@ -834,8 +835,11 @@ public class JavaBeanWriter implements BeanWriter { if (metainf.isDefaultValueAvailable(name)){ QName schemaQName = metainf.getSchemaQNameForQName(name); if (baseTypeMap.containsKey(schemaQName)){ + // Emitted inside a string literal in a field initializer, so + // the schema's text must not be able to close it. XSLTUtils.addAttribute(model, "defaultValue", - metainf.getDefaultValueForQName(name), property); + SourceLiteralEscaper.escape( + metainf.getDefaultValueForQName(name)), property); } } @@ -1012,23 +1016,28 @@ public class JavaBeanWriter implements BeanWriter { } if (metainf.isRestrictionBaseType(name) && metainf.getTotalDigitsFacet() != null) { - XSLTUtils.addAttribute(model, "totalDigitsFacet", metainf.getTotalDigitsFacet() + "", property); + XSLTUtils.addAttribute(model, "totalDigitsFacet", + SourceLiteralEscaper.escape(metainf.getTotalDigitsFacet() + ""), property); } if (metainf.isRestrictionBaseType(name) && metainf.getMaxExclusiveFacet() != null) { - XSLTUtils.addAttribute(model, "maxExFacet", metainf.getMaxExclusiveFacet() + "", property); + XSLTUtils.addAttribute(model, "maxExFacet", + SourceLiteralEscaper.escape(metainf.getMaxExclusiveFacet() + ""), property); } if (metainf.isRestrictionBaseType(name) && metainf.getMinExclusiveFacet() != null) { - XSLTUtils.addAttribute(model, "minExFacet", metainf.getMinExclusiveFacet() + "", property); + XSLTUtils.addAttribute(model, "minExFacet", + SourceLiteralEscaper.escape(metainf.getMinExclusiveFacet() + ""), property); } if (metainf.isRestrictionBaseType(name) && metainf.getMaxInclusiveFacet() != null) { - XSLTUtils.addAttribute(model, "maxInFacet", metainf.getMaxInclusiveFacet() + "", property); + XSLTUtils.addAttribute(model, "maxInFacet", + SourceLiteralEscaper.escape(metainf.getMaxInclusiveFacet() + ""), property); } if (metainf.isRestrictionBaseType(name) && metainf.getMinInclusiveFacet() != null) { - XSLTUtils.addAttribute(model, "minInFacet", metainf.getMinInclusiveFacet() + "", property); + XSLTUtils.addAttribute(model, "minInFacet", + SourceLiteralEscaper.escape(metainf.getMinInclusiveFacet() + ""), property); } if (!metainf.getEnumFacet().isEmpty()) { @@ -1044,7 +1053,9 @@ public class JavaBeanWriter implements BeanWriter { int id = 0; for (String attribValue : metainf.getEnumFacet()) { Element enumFacet = XSLTUtils.addChildElement(model, "enumFacet", property); - XSLTUtils.addAttribute(model, "value", attribValue, enumFacet); + // Keeps the deliberate QName argument split intact. + XSLTUtils.addAttribute(model, "value", + SourceLiteralEscaper.escapeEnumFacet(attribValue), enumFacet); if (validJava) { XSLTUtils.addAttribute(model, "id", attribValue, enumFacet); } else { diff --git a/modules/adb-codegen/test/org/apache/axis2/schema/SourceLiteralEscaperTest.java b/modules/adb-codegen/test/org/apache/axis2/schema/SourceLiteralEscaperTest.java new file mode 100644 index 0000000000..f765213c71 --- /dev/null +++ b/modules/adb-codegen/test/org/apache/axis2/schema/SourceLiteralEscaperTest.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.axis2.schema; + +import junit.framework.TestCase; + +/** + * Schema-derived defaults, enumeration facets and range facets are spliced into + * generated source inside string literals. A value able to close its own literal + * becomes code in a class-level initializer, which runs when the generated bean + * class loads -- on the machine compiling the sources, and wherever the built + * application runs. + */ +public class SourceLiteralEscaperTest extends TestCase { + + /** The injection from the finding: close the literal, then run something. */ + public void testAValueCannotCloseItsLiteral() { + String hostile = "x\"),Runtime.getRuntime().exec(\"calc\");//"; + String escaped = SourceLiteralEscaper.escape(hostile); + assertFalse("no bare quote may survive", hasBareQuote(escaped)); + assertTrue("the text is preserved, just escaped", escaped.contains("Runtime")); + } + + /** + * Escaping the backslash closes the unicode-escape route too: Java reads a + * backslash-u sequence as a unicode escape only after an even number of + * backslashes, so doubling the backslash makes it inert. + */ + public void testUnicodeEscapesAreNeutralised() { + assertEquals("\\\\u0022", SourceLiteralEscaper.escape("\\u0022")); + } + + public void testControlCharactersBecomeEscapes() { + assertEquals("a\\nb", SourceLiteralEscaper.escape("a\nb")); + assertEquals("a\\rb", SourceLiteralEscaper.escape("a\rb")); + assertEquals("a\\tb", SourceLiteralEscaper.escape("a\tb")); + // Three-digit octal, read the same way by Java and by C. + String withSoh = "a" + ((char) 1) + "b"; + assertEquals("a\\001b", SourceLiteralEscaper.escape(withSoh)); + } + + public void testOrdinaryValuesAreUnchanged() { + assertEquals("42", SourceLiteralEscaper.escape("42")); + assertEquals("2026-09-03T00:00:00Z", + SourceLiteralEscaper.escape("2026-09-03T00:00:00Z")); + assertEquals("plain text", SourceLiteralEscaper.escape("plain text")); + assertNull(SourceLiteralEscaper.escape(null)); + } + + /** + * SchemaCompiler splices a quote-comma-quote into a QName enumeration on + * purpose, so one facet becomes two arguments. Escaping it away would break + * QName enums, so the halves are escaped and the split itself is left alone. + */ + public void testTheDeliberateQNameSpliceSurvives() { + String registered = "ns:local\", \"http://example.com/ns"; + assertEquals("ns:local\", \"http://example.com/ns", + SourceLiteralEscaper.escapeEnumFacet(registered)); + } + + /** A hostile value either side of that split is still escaped. */ + public void testHostileHalvesOfASpliceAreStillEscaped() { + String hostile = "a\"),x(\"\", \"b\"),y(\""; + String escaped = SourceLiteralEscaper.escapeEnumFacet(hostile); + int splices = 0; + int at = escaped.indexOf("\", \""); + while (at >= 0) { + splices++; + at = escaped.indexOf("\", \"", at + 1); + } + assertEquals("exactly the one deliberate split remains", 1, splices); + } + + /** A quote not preceded by a backslash. */ + private boolean hasBareQuote(String value) { + for (int i = 0; i < value.length(); i++) { + if (value.charAt(i) == '"' && (i == 0 || value.charAt(i - 1) != '\\')) { + return true; + } + } + return false; + } +} diff --git a/src/site/markdown/release-notes/2.0.2.md b/src/site/markdown/release-notes/2.0.2.md index ebbc12a64c..df2b962b59 100644 --- a/src/site/markdown/release-notes/2.0.2.md +++ b/src/site/markdown/release-notes/2.0.2.md @@ -82,6 +82,15 @@ in `SECURITY.md`. `.xsd` and `.wsdl` names that stay inside META-INF are served now, enforced inside the shared helper so all three callers inherit it. +- **Schema values are escaped before they reach generated source.** Element and + attribute default and fixed values, enumeration facets and the numeric range facets + were copied out of the schema into generated Java and C *inside string literals* + with no escaping, so a value carrying a quote closed its literal and the remainder + became code in a class-level initializer -- which runs when the generated bean class + loads, on the build machine and in the shipped application. All of these are escaped + now, as the pattern facet already was. QName enumerations are unaffected: the + argument split the compiler inserts deliberately is preserved. + - **The schema compiler no longer trusts the schema it is compiling.** `XSD2Java` and the `axis2-xsd2java-maven-plugin` exist to consume contracts written elsewhere, but parsed them with no XXE hardening and dereferenced
