This is an automated email from the ASF dual-hosted git repository. rvesse pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/jena.git
commit df92b65808219ce0db9bf38a83d42be42009282a Author: Rob Vesse <[email protected]> AuthorDate: Wed Sep 30 11:40:30 2026 +0100 afn:sprintf() bounds output The afn:sprintf() function provides direct access to the Java String.format() method from within SPARQL queries. Badly formed format strings could lead to generating excessive output from small inputs. The implementation now configures a maximum output size (default 4096 bytes) to prevent this. --- .../jena/sparql/expr/nodevalue/XSDFuncOp.java | 56 +++++++++++++++++++++- .../apache/jena/sparql/function/MathLimits.java | 23 ++++++++- .../org/apache/jena/sparql/expr/TestFunctions.java | 22 +++++++++ .../function/library/TestFnFunctionsNumeric.java | 1 - .../sparql/function/library/TestMathLimits.java | 21 ++++++++ 5 files changed, 120 insertions(+), 3 deletions(-) diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/XSDFuncOp.java b/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/XSDFuncOp.java index 0e5410d4a5..df7de11a60 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/XSDFuncOp.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/XSDFuncOp.java @@ -27,10 +27,13 @@ import static org.apache.jena.sparql.expr.nodevalue.NumericType.OP_DOUBLE; import static org.apache.jena.sparql.expr.nodevalue.NumericType.OP_FLOAT; import static org.apache.jena.sparql.expr.nodevalue.NumericType.OP_INTEGER; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; import java.math.MathContext; import java.math.RoundingMode; +import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.Normalizer; @@ -44,6 +47,7 @@ import javax.xml.datatype.DatatypeConstants; import javax.xml.datatype.Duration; import javax.xml.datatype.XMLGregorianCalendar; +import org.apache.commons.io.output.ThresholdingOutputStream; import org.apache.jena.atlas.lib.IRILib; import org.apache.jena.atlas.lib.Lib; import org.apache.jena.atlas.lib.StrUtils; @@ -72,6 +76,26 @@ public class XSDFuncOp // F&O 3.1: section 4.2 (end intro) // https://www.w3.org/TR/xpath-functions/#op.numeric section 4.2 private static final int DIVIDE_PRECISION = 24; + + /** + * The default maximum output length (in bytes) for {@link #MAX_SPRINTF_OUTPUT_LENGTH} + * <p> + * Currently this is 4096 bytes, i.e. 4 kilobytes, as this allows for formatting a reasonable amount of content, and + * in particular allows for string literals of a modest length to be formatted. + * </p> + */ + protected static final int DEFAULT_MAX_SPRINTF_OUTPUT_LENGTH = 4096; + + /** + * Maximum permitted length (in bytes) for output generated by {@link #javaSprintf(NodeValue, List)}. + * <p> + * This prevents format strings that use large width specifiers from generating very large outputs from small + * inputs. + * </p> + */ + protected static int MAX_SPRINTF_OUTPUT_LENGTH = DEFAULT_MAX_SPRINTF_OUTPUT_LENGTH; + + // -------------------------------- // Numeric operations // http://www.w3.org/TR/xpath-functions/#op.numeric @@ -567,13 +591,43 @@ public class XSDFuncOp } } - return NodeValue.makeString(String.format(formatForOutput,objVals.toArray())); + return NodeValue.makeString(boundedFormat(formatForOutput,objVals.toArray())); } catch (IndexOutOfBoundsException ex) { throw new ExprEvalException("IndexOutOfBounds", ex); } } + /** + * Generates formatted output bounding the maximum output length by {@link #MAX_SPRINTF_OUTPUT_LENGTH}, this + * prevents a badly formed format string generating excessive output. + * <p> + * This uses the {@link ThresholdingOutputStream} from Apache Commons IO to enforce that the maximum length is not + * exceeded and throwing an {@link ExprEvalException} as soon as it would be which aborts further formatting. + * </p> + * @param formatStr Format string + * @param args Format arguments + * @return Formatted string + * @throws ExprEvalException Thrown if the format string is invalid or too much output is generated + */ + private static String boundedFormat(String formatStr, Object[] args) { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (ThresholdingOutputStream boundedOutput + = new ThresholdingOutputStream(MAX_SPRINTF_OUTPUT_LENGTH, + stream -> { throw new ExprEvalException("Formatted output too large");}, + stream -> output)) { + Formatter formatter = + new Formatter(boundedOutput, StandardCharsets.UTF_8, Locale.getDefault(Locale.Category.FORMAT)); + formatter.format(formatStr, args); + formatter.flush(); + return output.toString(StandardCharsets.UTF_8); + } catch (IllegalFormatException e) { + throw new ExprEvalException("Invalid format string", e); + } catch (IOException e) { + throw new ExprEvalException("IO error applying format"); + } + } + public static NodeValue strlen(NodeValue nvString) { Node n = NodeValueOps.checkAndGetStringLiteral("strlen", nvString); String str = n.getLiteralLexicalForm(); diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/function/MathLimits.java b/jena-arq/src/main/java/org/apache/jena/sparql/function/MathLimits.java index efe497d722..4f7608873a 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/function/MathLimits.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/function/MathLimits.java @@ -1,3 +1,24 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + package org.apache.jena.sparql.function; import org.apache.jena.sparql.expr.ExprEvalException; @@ -67,7 +88,7 @@ public class MathLimits { * Calculates how many digits raising {@code a} to the power of {@code b} would require. If the number of digits is * very large then throw an expression evaluation error to prevent execution. * <p> - * This is based upon the following mathematical formula, for {@code a^b} then Number of Digits is + * This is based upon the following mathematical formula, for {@code a^b} then number of digits is * {@code floor(b * log10(a)) + 1}. * </p> * diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestFunctions.java b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestFunctions.java index 9fe5e99838..1ced6aceee 100644 --- a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestFunctions.java +++ b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestFunctions.java @@ -34,6 +34,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Strings; import org.apache.jena.atlas.lib.DateTimeUtils; import org.apache.jena.datatypes.xsd.XSDDatatype; @@ -85,6 +86,27 @@ public class TestFunctions @Test public void exprSprintf_10() { test("afn:sprintf('this number is equal to %.5f', '1.23456789'^^xsd:double)",NodeValue.makeString("this number is equal to "+String.format("%.5f",1.23456789))); } @Test public void exprSprintf_11() { test("afn:sprintf('%.0f != %s', '12.23456789'^^xsd:double,'15')",NodeValue.makeString("12 != 15")); } @Test public void exprSprintf_12() { test("afn:sprintf('(%.0f,%s,%d) %4$tm %4$te,%4$tY', '12.23456789'^^xsd:double,'12',11,'2016-03-17'^^xsd:date)",NodeValue.makeString("(12,12,11) 03 17,2016")); } + @Test public void exprSprintf_13() { test("afn:sprintf('%20d', 1)",NodeValue.makeString(" 1")); } + @Test public void exprSprintf_14() { test("afn:sprintf('%020d', 1)",NodeValue.makeString("00000000000000000001")); } + @Test public void exprSprintf_15() { test("afn:sprintf('%200d', 1)",NodeValue.makeString(StringUtils.repeat(' ', 199) + "1")); } + + public static Stream<Arguments> badSprintf() { + return Stream.of( + Arguments.of("afn:sprintf('%2000000000d', 1)", "too large"), + Arguments.of("afn:sprintf('%1000d %1000d %1000d %1000d %1000d', 1, 2, 3, 4, 5)", "too large"), + Arguments.of("afn:sprintf('%f', 17)", "invalid format string") + ); + } + + @ParameterizedTest + @MethodSource("badSprintf") + public void exprSprintf_bad(String expr, String expectedMessage) { + // Given and When + ExprEvalException e = testEvalException(expr); + + // Then + assertTrue(Strings.CI.contains(e.getMessage(), expectedMessage)); + } // Timezone tests diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestFnFunctionsNumeric.java b/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestFnFunctionsNumeric.java index 821d512daf..51e453588d 100644 --- a/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestFnFunctionsNumeric.java +++ b/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestFnFunctionsNumeric.java @@ -62,7 +62,6 @@ public class TestFnFunctionsNumeric { // counter-intuitive -- would fail if float/double not translated to decimal @Test public void exprRoundHalfEven_08() { test("fn:round-half-to-even('150.015'^^xsd:float, 2)", NodeValue.makeFloat((float)150.01)); } - public static Stream<Arguments> outOfRangeInputs() { return Stream.of(Arguments.of("math:pow(2,2000000000)"), Arguments.of("math:pow(2000000000, 16)"), diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestMathLimits.java b/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestMathLimits.java index 9b058aa6a9..c33f02eb1d 100644 --- a/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestMathLimits.java +++ b/jena-arq/src/test/java/org/apache/jena/sparql/function/library/TestMathLimits.java @@ -1,3 +1,24 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + package org.apache.jena.sparql.function.library; import org.apache.jena.sparql.expr.ExprEvalException;
