Dan Mclaughlin created CODEC-348:
------------------------------------

             Summary: Base64 STRICT decoding in 1.23.0 rejects valid input when 
the codec has a line length (breaking change from 1.22.1)
                 Key: CODEC-348
                 URL: https://issues.apache.org/jira/browse/CODEC-348
             Project: Commons Codec
          Issue Type: Bug
    Affects Versions: 1.23.0
         Environment: OpenJDK 21.0.12
            Reporter: Dan Mclaughlin


Component: {{org.apache.commons.codec.binary.Base64}} / {{BaseNCodec}}
Works in: 1.22.1 and earlier (STRICT since 1.15)

h2. Summary

Commit {{8b506a1c}} ("Base32 and Base64 STRICT decoding now require the 
encoder's canonical alphabet, padding, and line separators, and validate 
streams through EOF") changed what {{CodecPolicy.STRICT}} means for decoding. A 
STRICT {{Base64}} instance constructed with a line length now accepts only 
byte-for-byte what that instance would encode, including a trailing line 
separator. Previously valid input that decoded correctly in 1.22.1 now throws 
{{IllegalArgumentException}}.

This is a behavior change in a minor release that breaks existing callers who 
use one STRICT instance for both encoding and decoding. Shibboleth's 
{{Base64Support}} (used by the Shibboleth IdP, SP integrations and OpenSAML for 
all SAML/XML-DSig base64) is one such caller: with 1.23.0 it can no longer 
decode its own encoded output. This is tracked downstream as Shibboleth 
[JSSH-84|https://shibboleth.atlassian.net/browse/JSSH-84] (priority Highest), 
where it breaks SAML message, signature and certificate decoding; in our 
deployment it broke SAML metadata signature validation. Because the IdP does 
not control which commons-codec version deployers' builds resolve, every 
Shibboleth deployment whose dependency management moves to 1.23.0 is affected.

h2. Steps to reproduce

Attached {{CodecOnlyRepro.java}} (commons-codec only):

{code:java}
Base64 strict76 = new Base64(76, new byte[] {'\n'}, false, CodecPolicy.STRICT);
String exact = strict76.encodeToString(data);

strict76.decode(exact);                          // exact encoder output
strict76.decode(exact.trim());                   // same, trailing '\n' removed
strict76.decode(singleLineEncodingOf(data));     // same bytes, no line breaks
strict76.decode(exact.replace("\n", "\r\n"));    // same bytes, CRLF separators
{code}

h2. Expected

All four decode to the original bytes, as in 1.22.1. Each input is valid base64 
for the same data, with canonical alphabet, padding and trailing bits.

h2. Actual (1.23.0)

||Input||20 bytes||256 bytes||
|Exact encoder output|OK|OK|
|Encoder output, trailing {{\n}} trimmed|Missing or incomplete final line 
separator|Missing or incomplete final line separator|
|Single line, no separators|Missing or incomplete final line separator|Invalid 
line separator or line length|
|76-char lines, CRLF|Unexpected character or data after padding|Invalid line 
separator or line length|

In 1.22.1 every case returns the correct bytes.

h2. Why this is a compatibility problem

* Through 1.22.1, {{BaseNCodec.isStrictDecoding()}} documented STRICT narrowly: 
decoding raises {{IllegalArgumentException}} "if trailing bits are not part of 
a valid encoding." Callers chose STRICT for that guarantee, not for 
line-structure validation.
* {{CodecPolicy.STRICT}}'s Javadoc is unchanged in 1.23.0 ("Data that causes a 
codec to fail should throw an exception"), so nothing at the API level signals 
the broader meaning.
* The changelog entry lists it under fixed bugs, and the release notes describe 
1.23.0 as a "feature and maintenance release," so downstream consumers and 
automated update tools treat it as a safe minor bump.
* Requiring a trailing separator even for input shorter than one line is 
stricter than RFC 2045 and RFC 4648 practice, and rejects the common pattern of 
trimming encoded output.
* In CODEC-346 (resolved 2026-10-03), a request to make default decoding 
stricter was declined because "changing the default would introduce another 
compatibility break." Changing what STRICT itself accepts is the same kind of 
break for callers who already opted into STRICT.

h2. Requests (any one would resolve it)

# Make line-structure validation opt-in (for example a builder option such as 
{{setStrictLineValidation(true)}}), keeping 1.22.x STRICT semantics as the 
default.
# Apply line-structure validation only when explicitly requested, and continue 
to accept input without separators (or with a missing final separator) for 
instances that have a line length.
# If the new behavior stays, document it as a breaking change in the release 
notes and in the {{CodecPolicy.STRICT}} and {{isStrictDecoding()}} Javadoc, and 
state that an instance with a line length should not be used to decode 
arbitrary input.

h2. Workaround for affected users

Decode with an instance that has no line length, after removing whitespace:

{code:java}
new Base64(0, new byte[] {'\n'}, false, 
CodecPolicy.STRICT).decode(input.replaceAll("\\s+", ""));
{code}

This decodes all the inputs above in both 1.22.1 and 1.23.0, and still rejects 
non-canonical trailing bits.

h2. Attachments

{{commons-codec-1.23-strict-repro}} project: {{CodecOnlyRepro.java}}, 
{{run.sh}}, {{run-output.txt}} (output for 1.22.1 and 1.23.0). Also includes 
the Shibboleth {{Base64Support}} and OpenSAML reproductions showing downstream 
impact.

Related: Shibboleth [JSSH-84|https://shibboleth.atlassian.net/browse/JSSH-84] 
(downstream impact), CODEC-346 (compatibility rationale), commit {{8b506a1c}} 
(the change).



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to