Skip to content

camel-netty-http: decode Basic auth credentials as UTF-8 - #29711

Open
Dhairya-007 wants to merge 3 commits into
apache:mainfrom
Dhairya-007:fix/netty-http-basic-auth-utf8
Open

Dhairya-007 wants to merge 3 commits into
apache:mainfrom
Dhairya-007:fix/netty-http-basic-auth-utf8

Conversation

@Dhairya-007

Copy link
Copy Markdown
Contributor

Problem

HttpServerChannelHandler.extractBasicAuthSubject reads the Base64-decoded credentials back with the platform default charset:

String decoded = StringHelper.after(auth, " ");
// the decoded part is base64 encoded, so we need to decode that
ByteBuf buf = NettyConverter.toByteBuffer(decoded.getBytes());
ByteBuf out = Base64.decode(buf);
try {
    String userAndPw = out.toString(Charset.defaultCharset());

RFC 7617 §2.1 says the client encodes user:password with UTF-8, which is what browsers and Camel's own producers send. A netty-http consumer running on a JVM whose default charset is not UTF-8 — -Dfile.encoding, file.encoding=COMPAT, or a JVM before 18 following the platform locale — therefore reconstructs a different password than the one that was sent, as soon as it contains a non-ASCII character, and answers 401 although the credentials are correct.

This is the receiving side of what #29694 fixed on the sending side; @davsclaus spotted it while reviewing that PR.

Fix

  • out.toString(StandardCharsets.UTF_8) for the credentials.
  • decoded.getBytes(StandardCharsets.US_ASCII) for the Base64 text itself, which is ASCII by definition — byte-identical in any ASCII-compatible charset, but it was the same unchecked pattern two lines up.

The now-unused java.nio.charset.Charset import is dropped.

Tests

New HttpServerChannelHandlerBasicAuthTest, which calls the protected static method directly from the same package:

  • shouldDecodeCredentialsAsUtf8 — the header Basic dXNlcjpww6Rzc3dvcmQ=, which is user:pässword encoded as a client sends it, has to come back as pässword. The expected values are written out rather than recomputed from the input, so the test states what the wire format means instead of agreeing with the implementation.
  • shouldDecodeAsciiCredentials — scott:tiger, the ordinary case, unchanged.
  • shouldIgnoreAnotherScheme — a Bearer header yields no principal.

As on #29694: Camel's CI runs Java 21, where the default charset is already UTF-8, so the first test is green there either way. It fails on a JVM started with a non-UTF-8 file.encoding, and it pins the behaviour from now on.

extractBasicAuthSubject read the Base64-decoded credentials back with the
platform default charset:

    String userAndPw = out.toString(Charset.defaultCharset());

RFC 7617 says a client encodes "user:password" with UTF-8, which is what
browsers and Camel's own producers send. A server whose JVM default is not
UTF-8 - -Dfile.encoding, file.encoding=COMPAT, or a JVM before 18 following
the platform locale - therefore reads a different password than the one that
was sent as soon as it contains a non-ASCII character, and the request is
rejected with 401 although the credentials are right.

This is the receiving side of the credential encoding fixed on the sending
side in apache#29694, and was spotted in its review.

The Base64 text itself is also converted to bytes explicitly, as US-ASCII,
which is what Base64 is; that one is byte-identical in any ASCII-compatible
charset.
@github-actions

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR: the UTF-8 fix is correct, thanks for following up on #29694 so quickly. One small, pre-existing buffer leak sits on the line this PR rewrites, so it would be good to fix it here.

I checked the change: out.toString(StandardCharsets.UTF_8) matches RFC 7617 and what Camel's own producers send after #29694, and US-ASCII for the Base64 text is right. Both test vectors are correct (dXNlcjpww6Rzc3dvcmQ= is user:pässword in UTF-8), and JUnit assertions match the rest of camel-netty-http. The two PRs touch disjoint files, so they can merge in any order.

[Important] Pooled buffer leak on malformed input (HttpServerChannelHandler.java:383-384): NettyConverter.toByteBuffer(byte[]) allocates from ByteBufAllocator.DEFAULT (pooled), and Base64.decode(buf) runs before the try. Netty's decoder throws IllegalArgumentException on an invalid Base64 character or a bad length, so a malformed Authorization: Basic ... header sent by any client leaves buf unreleased. Since this PR rewrites that line, could we move the decode inside the try?

ByteBuf buf = NettyConverter.toByteBuffer(decoded.getBytes(StandardCharsets.US_ASCII));
ByteBuf out = null;
try {
    out = Base64.decode(buf);
    String userAndPw = out.toString(StandardCharsets.UTF_8);
    ...
} finally {
    buf.release();
    if (out != null) {
        out.release();
    }
}

(An alternative is java.util.Base64.getDecoder().decode(decoded.trim()) with no ByteBufs at all. Catching the IllegalArgumentException and returning null, so the client gets a 401 instead of an error, would also be reasonable.)

[Suggestion] A third case in HttpServerChannelHandlerBasicAuthTest with a malformed value (e.g. Basic %%%) would pin whichever behaviour you choose.

Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

✅ Generated files are up to date

An earlier CI run reported uncommitted generated changes; the latest run no longer does.

@oscerd oscerd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against the producer/encode side (#29694); the two are the matching halves of the same RFC 7617 fix and they line up on the same dXNlcjpww6Rzc3dvcmQ= vector.

Two charset changes in extractBasicAuthSubject, both right:

  • decoded.getBytes() becomes decoded.getBytes(US_ASCII). decoded is the Base64 text, whose alphabet is ASCII by definition, so US_ASCII is both correct and no longer at the mercy of the default charset.
  • out.toString(Charset.defaultCharset()) becomes out.toString(UTF_8), so a non-ASCII user name or password is read back as the bytes the client actually sent.

The new test covers the three cases that matter: a non-ASCII credential decoded as UTF-8, a plain ASCII credential, and a non-Basic scheme returning null. Reusing the same pässword vector as the encode side is a nice touch.

One consideration, not blocking: a netty-http server on Java 17 with a non-UTF-8 default charset that today authenticates non-ASCII credentials will read those credentials differently after this change (correctly, per the RFC). If #29694 adds an upgrade-guide note for the header-value change, a sentence here would be worth it so the server side is covered too. Java 18 and later already default to UTF-8, so most deployments are unaffected.

Claude Code on behalf of oscerd

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR: thanks for the update; the new commit is the formatter re-wrapping the test's class Javadoc (it matches the patch CI produced exactly), so the buffer leak from my earlier review is still open.

[Important] Pooled buffer leak on malformed input is still there (HttpServerChannelHandler.java:383-384): Base64.decode(buf) still runs before the try, so when a client sends a malformed Authorization: Basic ... value, Netty's decoder throws IllegalArgumentException and the pooled buf from NettyConverter.toByteBuffer(...) is never released. The fix I suggested earlier still applies: move the decode inside the try and release out only when it is not null, or decode with java.util.Base64 so no ByteBufs are involved. Optionally, catch the IllegalArgumentException and return null so the client gets a 401.

[Suggestion] A third case in HttpServerChannelHandlerBasicAuthTest with a malformed value (e.g. Basic %%%) would pin whichever behaviour you choose.

Everything else (the UTF-8 / US-ASCII charsets and the two test vectors) still looks good, and the PR merges cleanly with main.

Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

Base64.decode(buf) ran before the try whose finally released the buffer, so a
client sending a malformed Authorization: Basic value made Netty's decoder
throw IllegalArgumentException and leaked the pooled ByteBuf that
NettyConverter.toByteBuffer had allocated - once per bad request.

Rather than juggle the two buffers, this drops them: java.util.Base64 decodes
straight to a byte[], so there is nothing to release and no leak to get wrong.
A value that is not base64 is now answered with no principal, which means the
request gets 401 instead of an error out of the handler.
@Dhairya-007

Copy link
Copy Markdown
Contributor Author

Thanks — the leak is real and it is on the exact line I was rewriting, so it belongs here. Fixed, and I took your alternative rather than the finally version, because it removes the failure mode instead of handling it:

String encoded = StringHelper.after(auth, " ");
byte[] credentials;
try {
    // the value is base64 encoded, so we need to decode that
    credentials = Base64.getDecoder().decode(encoded.trim());
} catch (IllegalArgumentException e) {
    // a client sent something that is not base64 at all: no principal, so the request is
    // answered with 401 rather than an error
    LOG.debug("Cannot decode the Basic Auth header, it is not valid base64");
    return null;
}
String userAndPw = new String(credentials, StandardCharsets.UTF_8);

With java.util.Base64 there is no ByteBuf on this path at all, so there is nothing to release and no ordering to get wrong — and returning null on a malformed header is what the method already does for every other unparseable case, so the client gets a 401 instead of an exception out of the handler. io.netty.handler.codec.base64.Base64 is no longer imported; ByteBuf and NettyConverter stay, they are still used by writeResponse.

Added the malformed test, and verifying it caught a mistake worth mentioning: my first version also asserted that Basic c2NvdHQ6dGlnZX is rejected. It is not — the JDK decoder does not require the trailing padding, so that value decodes happily to scott:tige. The test now uses Basic %%% and Basic not base64!, both of which really do throw, and says so in a comment.

On @oscerd's upgrade-guide point: #29694 concluded no note was needed, since main requires Java 21 where the default charset is already UTF-8 and only a deployment overriding file.encoding sees a change. The same reasoning covers the server side, so I have left the guide alone — say the word if you would rather have a sentence covering both halves and I will add it to one of the two PRs.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • components/camel-netty-http

ℹ️ Dependent modules were not tested because the total number of affected modules exceeded the threshold (50). Use the test-dependents label to force testing all dependents.


🔬 Scalpel shadow comparison — Scalpel: 40 of 723 tested, 27 compile-only — current: 40 all tested

Maveniverse Scalpel detected 40 affected modules (current approach: 40).

Skip-tests mode would test 40 modules (1 direct + 39 downstream), skip tests for 27 (generated code, meta-modules)

Modules Scalpel would test (40)
  • camel-a2a ← downstream of org.apache.camel:camel-platform-http-vertx
  • camel-chaos ← downstream of org.apache.camel:camel-netty-http
  • camel-cli-connector ← downstream of org.apache.camel:camel-chaos
  • camel-cli-debug ← downstream of org.apache.camel:camel-cli-connector
  • camel-clickup ← downstream of org.apache.camel:camel-netty-http
  • camel-java-joor-dsl ← downstream of org.apache.camel:camel-telegram
  • camel-jbang-mcp ← downstream of org.apache.camel:camel-rest-openapi
  • camel-jbang-plugin-mcp ← downstream of org.apache.camel:camel-jbang-core
  • camel-jbang-plugin-tui ← downstream of org.apache.camel:camel-chaos
  • camel-jbang-plugin-validate ← downstream of org.apache.camel:camel-yaml-dsl-validator
  • camel-jbang-run ← downstream of org.apache.camel:camel-catalog
  • camel-jsonpath ← downstream of org.apache.camel:camel-platform-http-vertx
  • camel-knative-http ← downstream of org.apache.camel:camel-platform-http-vertx
  • camel-launcher-container ← downstream of org.apache.camel:camel-launcher
  • camel-mcp-server ← downstream of org.apache.camel:camel-oauth
  • camel-micrometer-prometheus ← downstream of org.apache.camel:camel-platform-http-main
  • camel-netty-http ← components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerChannelHandler.java, components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/handlers/HttpServerChannelHandlerBasicAuthTest.java
  • camel-oauth ← downstream of org.apache.camel:camel-netty-http
  • camel-observability-services ← downstream of org.apache.camel:camel-micrometer-prometheus
  • camel-openai ← downstream of org.apache.camel:camel-platform-http-vertx
  • camel-openapi-validator ← downstream of org.apache.camel:camel-rest-openapi
  • camel-platform-http-main ← downstream of org.apache.camel:camel-platform-http-vertx
  • camel-platform-http-vertx ← downstream of org.apache.camel:camel-rest-openapi
  • camel-rest-openapi ← downstream of org.apache.camel:camel-netty-http
  • camel-semantic ← downstream of org.apache.camel:camel-xml-io-dsl
  • camel-semantic-openai ← downstream of org.apache.camel:camel-semantic
  • camel-semantic-typesafe-ai ← downstream of org.apache.camel:camel-xml-io-dsl
  • camel-semantic-wolf-defender ← downstream of org.apache.camel:camel-semantic
  • camel-telegram ← downstream of org.apache.camel:camel-netty-http
  • camel-test-main-junit5 ← downstream of org.apache.camel:camel-xml-io-dsl
  • camel-test-main-junit6 ← downstream of org.apache.camel:camel-xml-io-dsl
  • camel-typesafe-ai ← downstream of org.apache.camel:camel-xml-io-dsl
  • camel-webhook ← downstream of org.apache.camel:camel-netty-http
  • camel-whatsapp ← downstream of org.apache.camel:camel-netty-http
  • camel-xml-io-dsl ← downstream of org.apache.camel:camel-telegram
  • camel-xml-jaxb-dsl ← downstream of org.apache.camel:camel-telegram
  • camel-xml-jaxb-dsl-test-definition ← downstream of org.apache.camel:camel-telegram
  • camel-xml-jaxb-dsl-test-spring ← downstream of org.apache.camel:camel-telegram
  • camel-yaml-dsl-validator ← downstream of org.apache.camel:camel-yaml-dsl
  • camel-yaml-dsl-validator-maven-plugin ← downstream of org.apache.camel:camel-yaml-dsl-validator
Modules with tests skipped (27)
  • apache-camel
  • camel-allcomponents
  • camel-catalog
  • camel-catalog-console
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-endpointdsl
  • camel-endpointdsl-support
  • camel-itest
  • camel-jbang-core
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-kamelet-main
  • camel-launcher
  • camel-report-maven-plugin
  • camel-route-parser
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers
  • camel-yaml-dsl-maven-plugin
  • coverage
  • docs
  • dummy-component

ℹ️ Shadow mode — Scalpel observes but does not affect test execution. Learn more


⚙️ View full build and test results

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update. Since my last review, d89d1cb decodes the Basic value with java.util.Base64 instead of Netty's ByteBuf decoder (HttpServerChannelHandler.java:381-399), so there is no pooled buffer left to leak, and a value that is not Base64 now gives no principal, which the handler answers with 401 (lines 203-218). shouldIgnoreAValueThatIsNotBase64 pins that. Both earlier points are covered, and it lines up with #29694 (now on main) on the encode side: UTF-8 both ways.

LGTM once CI is green.

Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants