This is an automated email from the ASF dual-hosted git repository. reta pushed a commit to branch 4.1.x-fixes in repository https://gitbox.apache.org/repos/asf/cxf.git
commit 1c37ef9f6c1d99fe5285862505f0b63562b7949b Author: Javid Khan <[email protected]> AuthorDate: Fri Jun 19 02:01:31 2026 +0530 validate hex digits when decoding quoted-printable attachments (#3223) * validate hex digits when decoding quoted-printable attachments * use HexFormat to decode quoted-printable hex pairs * use StandardCharsets.US_ASCII in quoted-printable decoder test (cherry picked from commit 24dfd0bcb71d4695552dc4bea41b89aa6e47b6f6) --- .../attachment/QuotedPrintableDecoderStream.java | 31 ++++------ .../QuotedPrintableDecoderStreamTest.java | 67 ++++++++++++++++++++++ 2 files changed, 78 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/org/apache/cxf/attachment/QuotedPrintableDecoderStream.java b/core/src/main/java/org/apache/cxf/attachment/QuotedPrintableDecoderStream.java index f58e1a14061..6e4569d8ce0 100644 --- a/core/src/main/java/org/apache/cxf/attachment/QuotedPrintableDecoderStream.java +++ b/core/src/main/java/org/apache/cxf/attachment/QuotedPrintableDecoderStream.java @@ -20,25 +20,9 @@ package org.apache.cxf.attachment; import java.io.IOException; import java.io.InputStream; +import java.util.HexFormat; public class QuotedPrintableDecoderStream extends InputStream { - private static final byte[] ENCODING_TABLE = { - (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', - (byte)'9', (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F' - }; - - /* - * set up the decoding table. - */ - private static final byte[] DECODING_TABLE = new byte[128]; - - static { - // initialize the decoding table - for (int i = 0; i < ENCODING_TABLE.length; i++) { - DECODING_TABLE[ENCODING_TABLE[i]] = (byte)i; - } - } - private int deferredWhitespace; private int cachedCharacter = -1; private final InputStream in; @@ -66,9 +50,16 @@ public class QuotedPrintableDecoderStream extends InputStream { return read(); } // this is a hex pair we need to convert back to a single byte. - b[0] = DECODING_TABLE[b[0]]; - b[1] = DECODING_TABLE[b[1]]; - return (b[0] << 4) | b[1]; + return (decodeHexDigit(b[0]) << 4) | decodeHexDigit(b[1]); + } + + private static int decodeHexDigit(byte b) throws IOException { + // mask to an unsigned value first so a high-bit byte is treated as a codepoint rather than indexing negatively + try { + return HexFormat.fromHexDigit(b & 0xff); + } catch (NumberFormatException e) { + throw new IOException("Invalid quoted printable encoding", e); + } } @Override diff --git a/core/src/test/java/org/apache/cxf/attachment/QuotedPrintableDecoderStreamTest.java b/core/src/test/java/org/apache/cxf/attachment/QuotedPrintableDecoderStreamTest.java new file mode 100644 index 00000000000..9e2ea016bbc --- /dev/null +++ b/core/src/test/java/org/apache/cxf/attachment/QuotedPrintableDecoderStreamTest.java @@ -0,0 +1,67 @@ +/** + * 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.cxf.attachment; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertThrows; + +public class QuotedPrintableDecoderStreamTest { + + private static byte[] decode(byte[] encoded) throws IOException { + InputStream in = new QuotedPrintableDecoderStream(new ByteArrayInputStream(encoded)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int b; + while ((b = in.read()) != -1) { + out.write(b); + } + return out.toByteArray(); + } + + @Test + public void testDecodesHexPair() throws Exception { + assertArrayEquals("hello world".getBytes(StandardCharsets.US_ASCII), + decode("hello=20world".getBytes(StandardCharsets.US_ASCII))); + } + + @Test + public void testDecodesLowerCaseHex() throws Exception { + // a lower case hex pair must decode to the same byte as the upper case form + assertArrayEquals(new byte[] {(byte)0xe2}, decode("=e2".getBytes(StandardCharsets.US_ASCII))); + assertArrayEquals(new byte[] {(byte)0xe2}, decode("=E2".getBytes(StandardCharsets.US_ASCII))); + } + + @Test + public void testRejectsNonHexCharacters() { + assertThrows(IOException.class, () -> decode("=GG".getBytes(StandardCharsets.US_ASCII))); + } + + @Test + public void testRejectsHighBitByteAfterMarker() { + // a byte >= 0x80 following the '=' marker must not index the decode table negatively + assertThrows(IOException.class, () -> decode(new byte[] {'=', (byte)0xff, 'A'})); + } +}
