Skip to content

CAMEL-25704: camel-undertow - refuse a request target or header value with control characters - #27944

Merged
davsclaus merged 3 commits into
apache:mainfrom
oscerd:fix/CAMEL-25704
Oct 18, 2026
Merged

davsclaus merged 3 commits into
apache:mainfrom
oscerd:fix/CAMEL-25704

Conversation

@oscerd

@oscerd oscerd commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

CAMEL-25704: camel-undertow - refuse a request target or header value with control characters

What

The undertow producer writes the path, query and header values of the outgoing HTTP request to the socket one
character at a time (io.undertow.client.http.HttpRequestConduit does buffer.put((byte) string.charAt(i))), with no
validation. So:

  • a carriage return or line feed in the request path/query or in a header value broke it into a further line;
  • a character above 0xFF was narrowed to a single, different byte (for example U+010A was written as a line feed).

The producer decodes a percent-encoded CamelHttpPath / CamelHttpQuery before building the request target
(URISupport.pathAndQueryOf uses the decoded URI.getPath() / URI.getQuery()), so a percent-encoded control
character in those headers became a real control character at that point.

Change (camel-undertow only)

  • New helper UndertowHelper.checkWriteable(what, value): refuses a value that holds a control character, DEL, or a
    character above 0xFF. SP and HTAB, which do not change the request framing, are allowed.
  • UndertowProducer.process validates the request target (after pathAndQueryOf) and fails the exchange with an
    IllegalArgumentException if it is not writeable, instead of handing it to the client. The toHttpRequest call is
    wrapped so a rejected header value fails the exchange the same way (rather than escaping the async callback).
  • DefaultUndertowHttpBinding.toHttpRequest validates every outgoing header value with the same helper (Undertow
    validates header names but not values), including the Content-Type header it puts directly.
  • The Host header is also validated: when preserveHostHeader is set the producer writes the caller's own Host
    header verbatim, so it is run through checkWriteable before the put.

The decoding of CamelHttpPath / CamelHttpQuery is deliberately kept: the rest producer double-encodes its query
parameters and relies on the undertow producer decoding once (CAMEL-15410, RestUndertowProducerEncodingTest).
Changing the producer to send the raw, still-encoded target would regress that round trip, so this change keeps the
existing decoding and only refuses characters that cannot be written as they are.

Tests

  • UndertowProducerEncodedUriTest: a percent-encoded CR/LF in CamelHttpQuery or CamelHttpPath, a character above
    0xFF in the query (the narrowing case), a CR/LF in a header value, and a CR/LF in a preserved Host header all make
    the exchange fail; an ordinary query is still sent, and a header value with a tab and a Latin-1 character is
    accepted (pinning the allowed set).
  • Existing RestUndertowProducerEncodingTest keeps passing (the rest round trip is unchanged).
  • Full camel-undertow module suite is green (232 run, 0 failures, 1 skipped).

Docs

  • Upgrade-guide note in camel-4x-upgrade-guide-4_24.adoc.

Relates to CAMEL-25704.

Claude Code on behalf of oscerd

🤖 Generated with Claude Code

@oscerd
oscerd requested review from Croway and davsclaus October 16, 2026 10:48
@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.

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • components/camel-undertow
  • docs

ℹ️ 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: 46 of 706 tested, 26 compile-only — current: 46 all tested

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

Skip-tests mode would test 46 modules (2 direct + 45 downstream), skip tests for 26 (generated code, meta-modules)

Modules Scalpel would test (46)
  • camel-a2a ← downstream of org.apache.camel:camel-vertx-http
  • camel-ai-observability ← downstream of org.apache.camel:camel-telemetry
  • camel-consul ← downstream of org.apache.camel:camel-undertow
  • camel-cxf-rest ← downstream of org.apache.camel:camel-undertow
  • camel-cxf-soap ← downstream of org.apache.camel:camel-undertow
  • camel-cxf-spring-rest ← downstream of org.apache.camel:camel-undertow
  • camel-cxf-spring-soap ← downstream of org.apache.camel:camel-undertow
  • camel-cxf-spring-transport ← downstream of org.apache.camel:camel-cxf-transport
  • camel-cxf-transport ← downstream of org.apache.camel:camel-undertow
  • 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-catalog
  • camel-jbang-plugin-validate ← downstream of org.apache.camel:camel-jbang-core
  • camel-jsonpath ← downstream of org.apache.camel:camel-platform-http-vertx
  • camel-knative-http ← downstream of org.apache.camel:camel-platform-http-vertx
  • camel-langchain4j-agent ← downstream of org.apache.camel:camel-telemetry
  • camel-langchain4j-chat ← downstream of org.apache.camel:camel-telemetry
  • camel-langchain4j-embeddings ← downstream of org.apache.camel:camel-telemetry
  • camel-langchain4j-embeddingstore ← downstream of org.apache.camel:camel-langchain4j-embeddings
  • camel-launcher-container ← downstream of org.apache.camel:camel-launcher
  • camel-lra ← downstream of org.apache.camel:camel-undertow
  • camel-mcp-server ← downstream of org.apache.camel:camel-openai
  • camel-micrometer-observability ← downstream of org.apache.camel:camel-undertow
  • camel-micrometer-prometheus ← downstream of org.apache.camel:camel-platform-http-main
  • camel-oauth ← downstream of org.apache.camel:camel-undertow
  • camel-observability-services ← downstream of org.apache.camel:camel-opentelemetry2
  • camel-openai ← downstream of org.apache.camel:camel-telemetry
  • camel-openapi-validator ← downstream of org.apache.camel:camel-rest-openapi
  • camel-opentelemetry ← downstream of org.apache.camel:camel-undertow
  • camel-opentelemetry2 ← downstream of org.apache.camel:camel-undertow
  • camel-platform-http-main ← downstream of org.apache.camel:camel-platform-http-vertx
  • camel-platform-http-vertx ← downstream of org.apache.camel:camel-vertx-http
  • camel-rest-openapi ← downstream of org.apache.camel:camel-undertow
  • camel-rest-postman ← downstream of org.apache.camel:camel-vertx-http
  • camel-semantic-openai ← downstream of org.apache.camel:camel-openai
  • camel-slack ← downstream of org.apache.camel:camel-undertow
  • camel-soap ← downstream of org.apache.camel:camel-cxf-soap
  • camel-spring-ai-chat ← downstream of org.apache.camel:camel-telemetry
  • camel-telemetry ← downstream of org.apache.camel:camel-undertow
  • camel-telemetry-dev ← downstream of org.apache.camel:camel-undertow
  • camel-undertow ← components/camel-undertow/src/main/java/org/apache/camel/component/undertow/DefaultUndertowHttpBinding.java, components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowHelper.java, components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowProducer.java, components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowProducerEncodedUriTest.java
  • camel-undertow-spring-security ← downstream of org.apache.camel:camel-undertow
  • camel-vertx-http ← downstream of org.apache.camel:camel-undertow
  • camel-yaml-dsl-validator ← downstream of org.apache.camel:camel-catalog
  • camel-yaml-dsl-validator-maven-plugin ← downstream of org.apache.camel:camel-yaml-dsl-validator
  • camel-zookeeper ← downstream of org.apache.camel:camel-undertow
Modules with tests skipped (26)
  • 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
  • dummy-component

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

Build reactor — dependencies compiled but only changed modules were tested (2 modules, 47.8s total)

Total reactor time: 47.8s

Module Duration Status
Camel :: Undertow 33.7s SUCCESS
Camel :: Docs 14.1s SUCCESS

Top 20 slowest modules:

  • Camel :: Undertow (33.7s)
  • Camel :: Docs (14.1s)

⚙️ View full build and test results

@gnodet-bot gnodet-bot 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.

Good security fix — the checkWriteable helper and its integration are clean and well-tested.

Two observations worth addressing:

1. Host header bypasses checkWriteable when preserveHostHeader=true

When endpoint.isPreserveHostHeader() is true, the caller-supplied host header value is written directly to requestHeaders without validation (UndertowProducer.java, lines 138-140):

final String host = message.getHeader(UndertowConstants.HOST_STRING, String.class);
if (endpoint.isPreserveHostHeader()) {
    requestHeaders.put(Headers.HOST, Optional.ofNullable(host).orElseGet(uri::getAuthority));

A CR/LF in the incoming Host header bypasses the fix entirely. Calling UndertowHelper.checkWriteable("Host header", host) before the requestHeaders.put call would close this gap.

2. Missing test for the char-narrowing attack vector (ch > 0xFF → LF)

The PR description and Javadoc explicitly call out U+010A → LF narrowing as a key motivation for the fix, but no test exercises the ch > 0xFF branch of checkWriteable (line 174 in UndertowHelper.java). All three rejection tests use %0D%0A or literal \r\n, which hit the ch < 0x20 guard. A test passing U+010A directly in a query or header value would verify the narrowing branch is caught. See the inline comment on the test file for a suggested addition.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@oscerd

oscerd commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Both points are addressed in 333f977:

  1. Host header — when preserveHostHeader is set, the producer wrote the caller's own Host header verbatim outside the validated header-copy loop. It now goes through UndertowHelper.checkWriteable("Host header", ...) before the put, so a CR/LF there fails the exchange. I also validated the Content-Type header, which toHttpRequest puts the same verbatim way. The cookie values come from the component's cookie store (not the inbound message) and their names already go through HttpString, so I left those as-is — happy to extend if you prefer.
  2. Narrowing coverage — added rejectsNarrowingCharacterInQuery (U+010A in CamelHttpQuery) exercising the ch > 0xFF branch, plus rejectsCrlfInPreservedHostHeader for the Host path above.

Full camel-undertow suite is green (234 run, 0 failures, 1 skipped); UndertowHttpProxyPreserveHostTest still passes, so a legitimate preserved Host header is unaffected.

Claude Code on behalf of oscerd

@gnodet-bot gnodet-bot 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.

Re-reviewed after the latest commits — all prior findings are addressed. ✅

  • Host header bypass (preserveHostHeader=true): checkWriteable("Host header", hostHeader) is now called before the put, correctly closing the injection vector.
  • Narrowing-character test: rejectsNarrowingCharacterInQuery sends U+010A (>0xFF) and confirms the first branch of checkWriteable fires before the byte is narrowed.
  • Content-Type propagation: checkWriteable is called inside toHttpRequest, which is wrapped in the IllegalArgumentException catch block in the producer — the exception path is correct.
  • rejectsCrlfInPreservedHostHeader: validates the fix end-to-end with CRLF injection into Host.

The dismissed items (cookie values, header key injection, consumer-side, IAE propagation) were all correctly scoped as pre-existing or already handled by the call stack. No further concerns.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@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 @oscerd, this looks good. The Undertow 2.4.4 client conduit writes each char with a plain byte cast and does no validation, so validating the decoded request target and every header value (including the preserved Host and Content-Type) is the right fix, and it keeps the CAMEL-15410 rest round trip intact. Allowing HTAB and Latin-1 obs-text while refusing controls, DEL and anything above 0xFF is a sensible line, and the upgrade-guide note covers the behaviour change. CI is green.

One optional suggestion inline.

Claude Code on behalf of Claus Ibsen

oscerd and others added 3 commits October 18, 2026 12:18
… with control characters

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Andrea Cosentino <ancosen@gmail.com>
…ers too

The producer writes the Host header (the caller's own when preserveHostHeader
is set) and the Content-Type header verbatim, outside the header-copy loop, so
run them through UndertowHelper.checkWriteable as well. Adds tests for the
above-0xFF narrowing branch and for a CR/LF in a preserved Host header.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Andrea Cosentino <ancosen@gmail.com>
…der-value characters

Pins the allowed set: a header value with a tab and a Latin-1 (obs-text)
character is sent, not refused.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Andrea Cosentino <ancosen@gmail.com>

@gnodet-bot gnodet-bot 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.

Previous findings are all addressed. The new commit adds sendsAHeaderWithTabAndLatin1 — a positive test pinning the allowed boundary: a header value with a tab (exempted by ch != '\t') and a Latin-1 obs-text character (é, 0xE9 ≤ 0xFF) is correctly not refused by checkWriteable. The test reaches the validation path through toHttpRequest's header loop, so it is not vacuous. No new issues.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@gnodet gnodet added the lts label Oct 18, 2026

@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 @oscerd. Re-reviewed from 333f977 (the commit approved earlier) to the current head. Apart from the rebase onto main, the only change is the new sendsAHeaderWithTabAndLatin1 test; UndertowHelper, UndertowProducer and DefaultUndertowHttpBinding are byte-for-byte the same as what was approved.

The optional point from the earlier review is addressed: the test sends MyHeader: café<TAB>au lait, which goes through the header loop in toHttpRequest (the default UndertowHeaderFilterStrategy does not filter MyHeader), so it really exercises checkWriteable and pins SP, HTAB and Latin-1 as allowed. Because throwExceptionOnFailure is on by default, assertFalse(out.isFailed()) also confirms the Undertow server accepted the request. The earlier gnodet-bot points (the preserved Host header and coverage for the narrowing branch) are still fixed, the upgrade-guide note is unchanged, and CI is green.

Good to merge.

Claude Code on behalf of Claus Ibsen

@davsclaus davsclaus added this to the 4.24.0 milestone Oct 18, 2026
@davsclaus davsclaus added the bug Something isn't working label Oct 18, 2026
@davsclaus
davsclaus merged commit dad7b54 into apache:main Oct 18, 2026
7 checks passed
@davsclaus davsclaus added port/camel-4.18.x Bug needs porting to camel-4.18.x port/camel-4.22.x Bug needs porting to camel-4.22.x labels Oct 18, 2026
@davsclaus

Copy link
Copy Markdown
Contributor

Added port/camel-4.22.x and port/camel-4.18.x, matching the 4.22.x and 4.18.x fix versions on CAMEL-25704. UndertowHelper and UndertowProducer on both branches still build the request without the control-character check that this PR adds.

Claude Code on behalf of Claus Ibsen

@gnodet

gnodet commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

🔄 Backport Bot

Conflicts (🤖 agent dispatched to resolve):

  • ⚠️ camel-4.22.x — Cherry-pick conflict in: docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
  • ⚠️ camel-4.18.x — Cherry-pick conflict in: docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc

ℹ️ If you push additional commits after /port, use /port again to update the port PRs.

@gnodet

gnodet commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

✅ Backport to camel-4.18.x created: #28064

@gnodet

gnodet commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

✅ Backport to camel-4.22.x created: #28065

@gnodet gnodet removed the port/camel-4.22.x Bug needs porting to camel-4.22.x label Oct 18, 2026
@oscerd
oscerd deleted the fix/CAMEL-25704 branch October 19, 2026 07:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working components docs lts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants