Skip to content

CAMEL-25137: Fix ArrayIndexOutOfBoundsException in MLLP consumer when message starts with segment delimiter - #27136

Open
tmielke wants to merge 1 commit into
apache:mainfrom
tmielke:fix/CAMEL-25137
Open

tmielke wants to merge 1 commit into
apache:mainfrom
tmielke:fix/CAMEL-25137

Conversation

@tmielke

@tmielke tmielke commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Description

When MllpTcpServerConsumer receives an MLLP payload whose first byte is a segment delimiter (0x0D / \r), populateHl7DataHeaders crashes with ArrayIndexOutOfBoundsException: Index -1 because the parsing loop accesses hl7MessageBytes[i - 1] at i == 0.

This is a latent defect in the original 2018 implementation — the default configuration (hl7Headers=true, validatePayload=false) leaves this path unguarded.

Changes

  • MllpTcpServerConsumer.java — two fixes:
    • Added i > 0 guard before hl7MessageBytes[i - 1] to prevent the out-of-bounds access
    • Changed if (-1 == endOfMSH) to if (endOfMSH <= 0) so a zero-length MSH segment (delimiter at byte 0) is treated as invalid rather than falling through to the header parsing branch
  • MllpTcpServerConsumerPopulateHl7DataHeadersTest.java — new unit test with two test cases:
    • Message starting with segment delimiter (reproduces the reported crash)
    • Message where fieldSeparator == SEGMENT_DELIMITER (edge case where byte[3] is also 0x0D)

Test plan

  • New unit tests pass (mvn test -Dtest=MllpTcpServerConsumerPopulateHl7DataHeadersTest -pl components/camel-mllp)
  • Full MLLP test suite passes with no regressions (348 tests, 0 failures)

Target

  • I checked that the commit is targeting the correct branch (Camel 4 uses the main branch)

Tracking

  • If this is a large change, bug fix, or code improvement, I checked there is a JIRA issue filed for the change (usually before you start working on it).

Apache Camel coding standards and style

  • I checked that each commit in the pull request has a meaningful subject line and body.

  • I have run mvn clean install -DskipTests locally from root folder and I have committed all auto-generated changes.

AI-assisted contributions

  • If this PR includes AI-generated code, commits have proper co-authorship attribution (e.g., Co-authored-by trailers) and the PR description identifies the AI tool used.

… message starts with segment delimiter

Guard against i == 0 before accessing hl7MessageBytes[i - 1] in
populateHl7DataHeaders, and treat endOfMSH == 0 as invalid (same as -1)
so a zero-length MSH does not fall through to the header parsing branch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

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

Correct fix, and a good one to make since MllpTcpServerConsumer parses untrusted network input — a malformed payload should never crash header population.

The two changes are consistent. When the first byte is a segment delimiter, the loop enters the SEGMENT_DELIMITER branch at i == 0; the i > 0 guard short-circuits before hl7MessageBytes[i - 1], so the ArrayIndexOutOfBoundsException: Index -1 is avoided, fieldSeparatorIndexes is left untouched, and endOfMSH is set to 0. The follow-on check then becomes endOfMSH <= 0, which folds the new zero-length-MSH case (delimiter at byte 0 → endOfMSH == 0) in with the existing not-found case (-1), so both go to the "unable to find the end of the MSH segment" warning and header parsing runs only for a real, non-empty MSH (endOfMSH > 0). Empty input and a one-byte delimiter-only message both land in the warn branch now instead of throwing.

The new MllpTcpServerConsumerPopulateHl7DataHeadersTest covers the reported crash (leading delimiter) and the fieldSeparator == SEGMENT_DELIMITER edge, and the full MLLP suite passes.

LGTM. This is a fork PR so CI has not run yet; I'll hold the formal approval until the workflow is authorized and the checks are green.

This review was generated with AI assistance and reviewed/issued by the human operator. 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.

Thanks @tmielke, this is a clean, minimal fix for a long-standing edge case (the loop dates back to the 2018 rewrite).

Verified locally (PR head 73ed411):

  • MllpTcpServerConsumerPopulateHl7DataHeadersTest and MllpTcpServerConsumerMessageHeadersTest pass.
  • With only the production change reverted, testMessageStartingWithSegmentDelimiter fails with ArrayIndexOutOfBoundsException: Index -1, so the test does reproduce the reported bug.
  • No formatter/impsort changes after the build.

What changes for users: before this fix, the AIOOBE was caught by the outer catch in processMessage and reported as the misleading "Unexpected exception creating Unit of Work". The exchange never reached the route and no ACK was sent. Now the message reaches the route without HL7 headers, a WARN is logged, and auto-ack goes through Hl7Util.generateAcknowledgementPayload, which already rejects this payload with a controlled MllpAcknowledgementGenerationException (the MSH has fewer than 8 fields). That is the graceful handling the JIRA asks for.

Minor, non-blocking notes on the test are inline. On its own, the i > 0 guard would already stop the crash: with endOfMSH == 0 the else branch finds no field indexes and sets no headers. The endOfMSH <= 0 change is still worthwhile because it logs the WARN for this malformed case.

CI workflows are waiting for maintainer approval (action_required), so there are no check results yet.

This is a rules-and-conventions review. It does not replace specialized AI review tools or static analysis such as SonarCloud.

This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

Claude Code on behalf of davsclaus


@Override
protected void doPreSetup() throws Exception {
MllpComponent mllpComponent = createCamelContext().getComponent("mllp", MllpComponent.class);

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.

Nit: createCamelContext() here builds a second CamelContext that is never started or stopped. The consumer and endpoint then belong to that orphan context, while the exchanges in the tests are created on the managed context. It works, but it is a bit surprising. You could build the consumer lazily in the test methods from context.getEndpoint("mllp://localhost:0", MllpEndpoint.class), or drop CamelTestSupport and use a plain DefaultCamelContext for this pure unit test.

}

@Test
void testFieldSeparatorEqualsSegmentDelimiter() {

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.

Note: this case passes on the code before the fix as well. When fieldSeparator == SEGMENT_DELIMITER, the else if branch is never reached and endOfMSH stays -1, so it does not exercise either of the two changed lines. It is fine to keep as a guard against regressions. You could say so in the comment, or add a case that hits endOfMSH == 0 without the i - 1 access.

@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

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • components/camel-mllp

🔬 Scalpel shadow comparison — Scalpel: 9 of 697 tested, 27 compile-only — current: 9 all tested

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

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

Modules Scalpel would test (9)
  • camel-jbang-mcp ← downstream of org.apache.camel:camel-catalog
  • camel-jbang-plugin-mcp ← downstream of org.apache.camel:camel-jbang-core
  • camel-jbang-plugin-route-parser ← downstream of org.apache.camel:camel-route-parser
  • camel-jbang-plugin-tui ← downstream of org.apache.camel:camel-catalog
  • camel-jbang-plugin-validate ← downstream of org.apache.camel:camel-yaml-dsl-validator
  • camel-launcher-container ← downstream of org.apache.camel:camel-launcher
  • camel-mllp ← components/camel-mllp/src/main/java/org/apache/camel/component/mllp/MllpTcpServerConsumer.java, components/camel-mllp/src/test/java/org/apache/camel/component/mllp/MllpTcpServerConsumerPopulateHl7DataHeadersTest.java
  • 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
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

⚠️ Some tests are disabled on GitHub Actions (@DisabledIfSystemProperty(named = "ci.env.name")) and require manual verification:

  • components/camel-mllp: 1 test(s) disabled on GitHub Actions
All tested modules (36 modules, 5m 10s total)

Total reactor time: 5m 10s

Module Duration Status
Camel :: Launcher 51.1s SUCCESS
Camel :: JBang :: MCP 41.2s SUCCESS
Camel :: JBang :: Plugin :: TUI 34.1s SUCCESS
Camel :: Component DSL 27.1s SUCCESS
Camel :: Catalog :: Camel Catalog 21.7s SUCCESS
Camel :: YAML DSL 17.7s SUCCESS
Camel :: JBang :: Plugin :: Kubernetes 15.0s SUCCESS
Camel :: Docs 14.4s SUCCESS
Camel :: Kamelet Main 12.4s SUCCESS
Camel :: YAML DSL :: Validator 9.9s SUCCESS
Camel :: YAML DSL :: Deserializers 8.1s SUCCESS
Camel :: Catalog :: Camel Route Parser 7.7s SUCCESS
Camel :: JBang :: Plugin :: Testing 7.3s SUCCESS
Camel :: Catalog :: Camel Report Maven Plugin 6.3s SUCCESS
Camel :: All Components Sync point 5.0s SUCCESS
Camel :: JBang :: Plugin :: Validate 5.0s SUCCESS
Camel :: YAML DSL :: Validator Maven Plugin 3.5s SUCCESS
Camel :: YAML DSL :: Maven Plugins 3.4s SUCCESS
Camel :: Catalog :: Suggest (deprecated) 3.1s SUCCESS
Camel :: Catalog :: Maven 2.9s SUCCESS
Camel :: Coverage 1.7s SUCCESS
Camel :: Assembly 1.6s SUCCESS
Camel :: JBang :: Plugin :: Edit 1.5s SUCCESS
Camel :: JBang :: Plugin :: Generate 1.2s SUCCESS
Camel :: Catalog :: Dummy Component 1.0s SUCCESS
Camel :: JBang :: Integration tests 1.0s SUCCESS
Camel :: JBang :: Main 1.0s SUCCESS
Camel :: Catalog :: Console 0.8s SUCCESS
Camel :: JBang :: Plugin :: MCP 0.8s SUCCESS
Camel :: Launcher :: Container 0.7s SUCCESS
Camel :: Endpoint DSL :: Support 0.7s SUCCESS
Camel :: JBang :: Plugin :: Route Parser 0.5s SUCCESS
Camel :: Endpoint DSL n/a
Camel :: Integration Tests n/a
Camel :: JBang :: Core n/a
Camel :: MLLP n/a

Top 20 slowest modules:

  • Camel :: Launcher (51.1s)
  • Camel :: JBang :: MCP (41.2s)
  • Camel :: JBang :: Plugin :: TUI (34.1s)
  • Camel :: Component DSL (27.1s)
  • Camel :: Catalog :: Camel Catalog (21.7s)
  • Camel :: YAML DSL (17.7s)
  • Camel :: JBang :: Plugin :: Kubernetes (15.0s)
  • Camel :: Docs (14.4s)
  • Camel :: Kamelet Main (12.4s)
  • Camel :: YAML DSL :: Validator (9.9s)
  • Camel :: YAML DSL :: Deserializers (8.1s)
  • Camel :: Catalog :: Camel Route Parser (7.7s)
  • Camel :: JBang :: Plugin :: Testing (7.3s)
  • Camel :: Catalog :: Camel Report Maven Plugin (6.3s)
  • Camel :: All Components Sync point (5.0s)
  • Camel :: JBang :: Plugin :: Validate (5.0s)
  • Camel :: YAML DSL :: Validator Maven Plugin (3.5s)
  • Camel :: YAML DSL :: Maven Plugins (3.4s)
  • Camel :: Catalog :: Suggest (deprecated) (3.1s)
  • Camel :: Catalog :: Maven (2.9s)

⚙️ View full build and test results

@allthingssecurity allthingssecurity 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 @tmielke. I checked this independently and it looks good to me.

  • With main's MllpTcpServerConsumer, testMessageStartingWithSegmentDelimiter fails with ArrayIndexOutOfBoundsException: Index -1. With the PR it passes. The whole camel-mllp suite passes (350 tests, 3 skipped).
  • End to end over a socket, \rMSH|^~\&|ADT|... now reaches the route (WARN "unable to find the end of the MSH segment"). The exchange then fails with the controlled MllpAcknowledgementGenerationException from auto-ack instead of the AIOOBE. On main the route never saw the message.

One related thing I found while testing. It is not caused by this PR and could be a separate JIRA:

Hl7Util.generateAcknowledgementPayload checks fieldSeparatorIndexes.size() < 8, but then reads get(8) and get(9) (its own message says "10 are required"). So an MSH with 8 or 9 field-separator positions still fails with an unchecked exception. MSH|^~\&|A|B|C|D|20160902123950| gives IndexOutOfBoundsException: Index: 8, Size: 8, and MSH|^~\&|A|B|C|D|20160902123950|| gives Index: 9, Size: 9. On the consumer side this escapes the catch (MllpAcknowledgementGenerationException) in MllpTcpServerConsumer, and the connection is closed without an ACK. Changing the guard to < 10 would turn it into the same controlled exception as here. I am happy to open the JIRA if you agree.

Claude Code on behalf of allthingssecurity

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