29 Jul 2026
12 min read
I read a lot of security news and most of it is useless. Not wrong, just useless: a CVE number, a CVSS score, a vendor advisory link, “patch immediately”. You close the tab and you have learned nothing. The interesting part of a vulnerability is almost never the score, its the mechanism. How does a broker end up skipping an ACL check because you gave a destination the right kind of name? What happens when the library whose entire job is to answer yes or no gets equals() backwards?
So I am going to try something. Once a week I will list the vulnerabilities in the Java ecosystem that caught my attention, then pick a couple and dig into them properly: the advisory, the mechanism, and what I think the actual lesson is. Not a feed, there are better places for that.
This week has a theme, and I did not go looking for it. Almost everything that landed between 22 and 28 July was an authorization failure of one kind or another. A broker that forgot to check an ACL. An authorization policy engine with three separate ways to get the wrong answer. A SOAP stack accepting serialized objects from anybody who can reach a port. It was a bad week for saying no.
The week in five
-
CVE-2026-66713, Apache Axis2/Java, CVSS 9.8, 28 July. Unsafe Java deserialization in the Tribes-based clustering component. An unauthenticated attacker with network access to the clustering port sends a crafted serialized object, it gets deserialized in Axis2ChannelListener#messageReceived, and that is remote code execution. Clustering is off by default, which is the only reason this is not a much bigger story. The fix in 2.0.1 is to remove the clustering feature entirely. Not harden it, remove it.
-
CVE-2026-61487, Apache ActiveMQ, 27 July. An authenticated low-privilege user bypasses per-destination write ACLs by sending to a temporary composite destination whose physical name is a comma-separated list of real queues. Rated Important. Affects everything before 5.19.9 and 6.0.0 through 6.2.7, fixed in 5.19.9, 6.2.8 and 6.3.0. Deep dive below, and look at the credit line while you are there.
-
CVE-2026-55771, CVE-2026-55772 and CVE-2026-55773, Cedar-Java, CVSS 8.8 each, 28 July. Three vulnerabilities in the Java binding for the Cedar authorization policy language: a policy injection, a type confusion across the Java to Rust boundary, and an incorrect equality comparison. Affects everything before 2.3.6, 3.1.2 through 3.4.0, and 4.0.0 through 4.8.9. Fixed in 2.3.6, 3.4.1 and 4.9.0. Deep dive below.
-
CVE-2026-66390 and CVE-2026-66391, Apache Wicket, 27 July. The first is an XSS: crafted Link URL strings can break out of the surrounding JavaScript sequence, because the URL is interpolated into a script context without being neutralised for that context. Rated Important, affects 9.0.0 through 9.23.0 and 10.0.0 through 10.9.0, fixed in 10.10.0. The second, filed the same day by the same reporter, is leaked and missing CSP headers. I like that pairing a lot: one bug lets you inject script, the sibling bug is the defence in depth that should have limited the damage not being properly applied. They found the hole and then found that the safety net had a hole too.
-
CVE-2026-59878, Apache ActiveMQ AMQP, 27 July. Improper input validation in the AMQP NIO connector: an unauthenticated remote attacker sends a negative frame size value, NIO threads crash, and the thread pool gets exhausted. Moderate severity, same affected and fixed versions as the ACL bug above, reported by zx (Jace). A negative length field crashing a network protocol parser in 2026 is a comforting reminder that the classics never really go away.
ActiveMQ: the name is not the resource
Some background, because this bug only makes sense if you know two ActiveMQ features that are individually reasonable.
The first is composite destinations. ActiveMQ lets you address more than one destination with a single name by comma separating them, so publishing to A,B,C fans the message out to queues A, B and C. It is a genuinely useful thing for integration work and I have used it plenty.
The second is temporary destinations. These are created on the fly, typically for request and reply patterns where a consumer needs somewhere to receive the answer. Because they are owned by the connection that created them and disappear when it goes away, they are treated differently by the authorization layer. You do not write an ACL entry for a queue that will exist for 200 milliseconds and then never again.
Put the two together and you get the bug. Create a destination that is marked temporary but whose physical name is A,B,C, where A, B and C are real queues you have no write permission on. The broker looks at the destination, sees the temporary flag, and takes the path that skips the per-destination write ACL. Then the composite expansion runs and your message is delivered to all three queues.
The authorization decision was made about the wrapper, and the delivery was performed on the expansion. A composite destination is not one destination, its N destinations wearing a single name, and the check happened while it still looked like one thing.
Why I find it interesting
The generalisable lesson is that you have to authorize the effect and not the label. Any time a system has a naming mechanism that expands, aliases, globs or otherwise resolves one identifier into several resources, the authorization check has to happen after resolution, on the actual set. Checking before is checking a string. This is the same shape as path traversal (the path you validated is not the file you opened), as SSRF via redirect (the host you allowed is not the host you fetched), and as the Tomcat and Reactor Netty redirect bugs from earlier this year where the credential was authorized for one host and then sent to another. The resource you checked has to be the resource you touched, and every layer of indirection between those two moments is a place for them to diverge.
The other detail worth pausing on is the credit line. The advisory credits the discovery to Claude and Ada Logics.
Ada Logics is a security firm that does a lot of open source auditing and fuzzing work, much of it funded through OpenSSF and CNCF programmes. So what that line means in practice is a professional audit team using an AI model as part of their workflow, and the Apache Security Team crediting both in the advisory as a matter of routine. No announcement, no blog post, no controversy. Just a line in an advisory on oss-security.
I find that more significant than any single one of these bugs. Back in February I wrote about the CVE stigma and how AI-assisted discovery was going to change the volume and the culture around vulnerability reporting, and at the time the examples were still notable enough to be news. Five months later it is an unremarkable attribution in a routine Apache advisory. Steve Poole made a similar point recently after seven jackson-databind CVEs landed in a single day from AI assisted analysis, and his line has stayed with me: the absence of CVEs was never evidence of safety, it was evidence of silence.
For a project like ActiveMQ this is fine, it has a real security team and a release process. My worry, and I have said this before, is the enormous middle of the ecosystem that has neither. The rate of finding is going up much faster then the capacity to triage and fix.
Cedar-Java: three ways to get the wrong answer
Cedar is the authorization policy language AWS open sourced. The engine is written in Rust and there are bindings for other languages, of which CedarJava is the Java one. You describe who can do what in Cedar’s policy language, you hand the engine a request, it tells you permit or forbid. It exists specifically so that you do not have to hand-roll authorization logic, which is exactly the kind of code everybody gets wrong.
Three CVEs landed on 28 July, all rated 8.8, all in the Java binding rather then the Rust core.
CVE-2026-55773, policy injection. Improper input handling lets attacker-controlled data end up influencing the policy itself rather then just being data the policy is evaluated against. This is the authorization equivalent of SQL injection and the mitigation is the same as it has been for twenty five years: policies are code, user input is data, and the two must never be joined with string concatenation.
CVE-2026-55772, type confusion. This one is the most interesting technically. Cedar’s serialization format, the one used to pass values from Java across the FFI boundary into the Rust evaluator, reserves two JSON key names: __entity and __extn. They mark a value as an entity reference or an extension value rather then a plain record. CedarJava did not validate that keys in a CedarMap avoided those reserved names. So if your service builds a CedarMap from anything user-controlled, and request headers or resource metadata are the obvious candidates, an attacker who can choose a key name can inject __entity and get the Rust evaluator to interpret a plain record as an entity reference. The conditions are that the integrating service builds a map from user-controlled data and a policy references that value in a condition, which is not exotic.
CVE-2026-55771, incorrect equality comparison. The advisory itself is vague, it says only that under certain circumstances this could lead to incorrect equality comparisons. Secondary reporting attributes it to inverted logic in EntityIdentifier.equals(), returning true when the argument is null and false in cases where it should return true. I have not verified that against the source, so treat the specific mechanism as reported rather then confirmed, but the shape is clear enough.
Why I find it interesting
Start with the obvious one and then the useful one.
The obvious one is the irony, and I do not think it is only irony. Cedar exists so that applications stop writing their own authorization logic, on the entirely correct theory that hand-rolled authorization is where bugs live. And then the binding layer ships a policy injection, a type confusion and a broken equals(). This is not an argument against using a policy engine, you should absolutely use one. It is a reminder that adopting a library moves the risk, it does not delete it, and the place it moves to is the glue you wrote around the library plus the glue the library wrote around its own core.
The useful lesson is about the type confusion, and it is one I think Java developers systematically underrate: a reserved key in a user-controlled map is an injection vector. __entity and __extn are in-band signalling. They are metadata living in the same namespace as the data, distinguished only by a naming convention, and a naming convention is not a boundary. This is precisely the shape of prototype pollution in JavaScript with __proto__, and of every YAML tag and JSON $type deserialization bug we have all been fixing for years. If your serialization format has magic keys and you build objects in that format out of anything that came from a request, you have to filter for the magic keys. The library should have done it here, but the integrating service is the one holding the untrusted input.
The second lesson is that the FFI boundary is a trust boundary and it does not look like one. Java to Rust in the same process feels like a function call, so nobody treats it like a protocol. But it is a protocol, with a wire format and a parser on the other side, and the Rust evaluator trusted the Java side to have produced well-formed input. Everything we know about validating input at the edges applies to internal boundaries too, and internal boundaries are much easier to forget about precisely because they do not have a socket in front of them.
What I take away from this week
Two things, plus a note about timing.
The first is that all three of the serious bugs this week are the same failure at different altitudes: a decision was made about a representation rather then about the thing it represents. ActiveMQ authorized a destination name instead of the queues it expanded into. Cedar interpreted a record as an entity because it carried the right key. Axis2 turned bytes into live objects because they arrived on the clustering port and the port was the only credential. In each case the check was performed against something that stood in for the real resource, and the substitution was attacker-influenced.
The second is the pattern I noticed last week too and which is becoming a genuine trend: deletion as a fix. Axis2’s answer to a deserialization RCE in clustering is to remove clustering in 2.0.1. Logback did the same thing in June, taking out Janino-based conditional configuration entirely rather then patching the denylist for the fourth time. When a capability’s security history reads as a list instead of an incident, taking the feature out is usually the honest answer, even though somebody’s upgrade gets painful.
The note about timing: none of this will reach most Java teams this week. It reaches you when a BOM moves. Camel, Spring Boot, Quarkus and everything else pin these versions transitively, so the practical arrival date for the ActiveMQ and Wicket fixes is whenever the next framework release picks them up, and for anything on an EOL line it is never. If you run ActiveMQ directly, though, 5.19.9 or 6.2.8 is worth doing on your own schedule rather then waiting. An authorization bypass available to any authenticated user is exactly the kind of thing that sits quietly in an environment where you assumed the ACLs were doing the work.
See you next week.
References
06 May 2026
9 min read
Post-Quantum Cryptography has moved from research papers to production standards. NIST finalized FIPS 203 (ML-KEM), FIPS 204 (ML-DSA), and FIPS 205 (SLH-DSA) in August 2024, and the Java ecosystem is catching up fast. JDK 24 shipped with native ML-KEM support via the javax.crypto.KEM API (JEP 496), and JDK 27 will bring PQC directly into TLS with JEP 527.
As Chair of the Apache Camel PMC, I wanted to start preparing Camel for this transition early. Over the past weeks I have been working on the core SSL/TLS infrastructure and building proof-of-concept examples that show Camel can negotiate PQC TLS handshakes across three different JDK versions.
In this post I will walk through what we changed in Camel’s core, why it matters, and how you can try it yourself.
Why PQC matters for integration
If you are building enterprise integrations, chances are you are moving sensitive data between systems over TLS. The “harvest now, decrypt later” threat is real: adversaries can record encrypted traffic today and decrypt it once a sufficiently powerful quantum computer becomes available. For regulated industries, the migration deadline is approaching fast. NSA’s CNSA 2.0 guidance requires PQC for TLS in national security systems by 2033.
For Apache Camel users, this means the framework they use to wire systems together needs to support PQC at the transport layer. That is the direction we are heading, though there is still a good amount of work ahead.
What changed in Camel core
The PQC work started in Camel 4.19.0 and continued into 4.20. It focuses on making PQC TLS configuration as simple as adding a few properties.
Named Groups and Signature Schemes
TLS 1.3 uses named groups to negotiate key exchange algorithms. PQC key exchange requires new named groups like X25519MLKEM768, a hybrid that combines classical X25519 with the post-quantum ML-KEM-768 algorithm. Both run together, so security is maintained even if one is broken.
Before these changes, there was no way to configure named groups through Camel’s SSL configuration. We added namedGroups and signatureSchemes to both SSLContextParameters and the camel.ssl.* configuration properties:
camel.ssl.namedGroups=X25519MLKEM768,x25519
camel.ssl.signatureSchemes=ML-DSA,ECDSA,RSA
We also added include/exclude filters for fine-grained control:
camel.ssl.namedGroupsInclude=X25519MLKEM768,x25519
camel.ssl.namedGroupsExclude=ffdhe2048
Auto-configuration on JDK 25+
On JDK 25 and later (where X25519MLKEM768 is available in the JVM), Camel will automatically reorder the named groups to prefer PQC key exchange. If you have not explicitly configured named groups, Camel sets the ordering to:
X25519MLKEM768, x25519, secp256r1, secp384r1, [JVM defaults]
No configuration required. If your JVM supports it, Camel prefers PQC. You will see a log line confirming it:
INFO SSLContextParameters - Auto-configured PQC named groups: [X25519MLKEM768, x25519, secp256r1, secp384r1, ...]
This only kicks in when you have not set namedGroups or namedGroupsFilter explicitly, so it will not override your choices.
Self-signed certificates and provider selection
For development and testing, we added camel.ssl.selfSigned=true, which generates a self-signed certificate at startup so you can enable HTTPS without providing a keystore. Combined with camel.ssl.trustAllCertificates=true, this gives you a working TLS setup with zero external files.
We also added camel.ssl.provider so you can select a specific JSSE provider. This is what makes PQC work on JDK 21 and 24 through BouncyCastle:
camel.ssl.provider=BCJSSE
Tracking issues
- CAMEL-23154 - Add PQC named groups support to SSLContextParameters
- CAMEL-23158 - Add PQC named groups and signature schemes to SSL configuration properties
- CAMEL-23159 - Add signatureSchemes to SSLContextParameters
Three proof-of-concept examples
To validate that the approach works in practice, I built three self-contained examples that each perform a real PQC TLS 1.3 handshake using X25519MLKEM768. The examples are available in the camel-pqc-tls repository.
Each example creates an SSLServerSocket and an SSLSocket, performs a TLS 1.3 handshake with PQC key exchange, and verifies the result. No external servers, no mock objects. A real handshake that either succeeds or fails.
All three examples use the same camel.ssl.* property-based configuration. The JDK version and provider differ, but the approach is consistent.
JDK 27: Native PQC in TLS
JDK 27 adds PQC named groups directly to SunJSSE via JEP 527. This is the cleanest path: no third-party TLS providers, no security property workarounds. Everything is built into the JDK.
The application bootstrap is minimal:
public class PQCSSLContextApplication {
public static void main(String[] args) throws Exception {
Main main = new Main();
main.run(args);
}
}
The entire PQC configuration lives in application.properties:
camel.server.enabled=true
camel.server.port=8443
camel.server.useGlobalSslContextParameters=true
camel.ssl.enabled=true
camel.ssl.secureSocketProtocol=TLSv1.3
camel.ssl.selfSigned=true
camel.ssl.trustAllCertificates=true
camel.ssl.namedGroups=X25519MLKEM768,x25519
camel.ssl.cipherSuites=TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256
That is it. No keystores to generate, no shell scripts to run. Camel generates a self-signed certificate at startup, configures PQC named groups, and serves HTTPS on port 8443.
The handshake verification in the route uses Groovy to access Camel’s global SSLContext:
def sslCtx = camelContext.getSSLContextParameters().createSSLContext(camelContext)
def serverSocket = sslCtx.getServerSocketFactory().createServerSocket(0)
def clientSocket = sslCtx.getSocketFactory().createSocket("localhost", serverSocket.getLocalPort())
def sslParams = clientSocket.getSSLParameters()
sslParams.setNamedGroups(["X25519MLKEM768"] as String[])
clientSocket.setSSLParameters(sslParams)
clientSocket.startHandshake()
The client offers only X25519MLKEM768 with no classical fallback, so a successful handshake definitively proves PQC was negotiated.
JDK 21 and JDK 24: BouncyCastle JSSE
For JDK 21 and 24, the JDK’s SunJSSE does not yet support PQC named groups in TLS. BouncyCastle’s JSSE provider (BCJSSE) bridges this gap.
The configuration is almost identical to JDK 27, with two additions: camel.ssl.provider=BCJSSE to select the BouncyCastle TLS provider, and secp256r1 in the named groups for ECDSA certificate verification:
camel.server.enabled=true
camel.server.port=8443
camel.server.useGlobalSslContextParameters=true
camel.ssl.enabled=true
camel.ssl.provider=BCJSSE
camel.ssl.selfSigned=true
camel.ssl.trustAllCertificates=true
camel.ssl.secureSocketProtocol=TLSv1.3
camel.ssl.namedGroups=X25519MLKEM768,secp256r1
The bootstrap class still needs to register the BouncyCastle providers before Camel starts, and remove ECDH from jdk.tls.disabledAlgorithms (which BCJSSE interprets broadly, preventing EC credentials from working):
public class PQCSSLContextApplication {
public static void main(String[] args) throws Exception {
String disabled = Security.getProperty("jdk.tls.disabledAlgorithms");
if (disabled != null) {
disabled = disabled.replaceAll(",\\s*ECDH\\b", "");
Security.setProperty("jdk.tls.disabledAlgorithms", disabled);
}
Security.addProvider(new BouncyCastleProvider());
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
Main main = new Main();
main.run(args);
}
}
The route itself uses camelContext.getSSLContextParameters().createSSLContext(camelContext) to get the SSLContext, just like the JDK 27 example. The only difference is that on JDK 21, the standard SSLParameters.setNamedGroups() API does not exist yet, so the verification route uses BouncyCastle’s BCSSLParameters to set named groups on the client socket:
def sslCtx = camelContext.getSSLContextParameters().createSSLContext(camelContext)
def serverSocket = sslCtx.getServerSocketFactory().createServerSocket(0)
// ...
def bcClientParams = new org.bouncycastle.jsse.BCSSLParameters()
bcClientParams.setNamedGroups(["X25519MLKEM768", "secp256r1"] as String[])
((org.bouncycastle.jsse.BCSSLSocket) cs).setParameters(bcClientParams)
cs.startHandshake()
Earlier versions of these examples had to generate certificates programmatically, build keystores in memory, and create the SSLContext manually in Groovy. All of that is now handled by Camel’s camel.ssl.* configuration.
JDK 24 bonus: native ML-KEM
What makes the JDK 24 example unique is the /api/verify-kem endpoint, which exercises JDK 24’s native javax.crypto.KEM API directly. This endpoint runs cross-provider interoperability tests between the JDK’s built-in ML-KEM implementation and BouncyCastle’s, including key re-encoding through standard X.509/PKCS#8 formats.
One detail worth noting: cross-provider key interoperability requires re-encoding keys through standard formats. When you generate an ML-KEM key pair with one provider and pass the key objects directly to another provider’s KEM.getInstance(), you get an InvalidKeyException. The fix is to export the keys via getEncoded() and re-import them through X509EncodedKeySpec and PKCS8EncodedKeySpec:
def bcKf = KeyFactory.getInstance("ML-KEM", "BC")
def reEncodedPub = bcKf.generatePublic(
new X509EncodedKeySpec(jdkKp.getPublic().getEncoded()))
def reEncodedPriv = bcKf.generatePrivate(
new PKCS8EncodedKeySpec(jdkKp.getPrivate().getEncoded()))
This works because both providers use the same standard key encoding formats, even though their internal key representations differ.
Verifying PQC at the network level
Running the examples and seeing pqcVerified: true is one thing. Verifying at the network level that PQC key exchange actually happened is another.
You can use tshark to capture and inspect the TLS handshake:
tshark -i lo -f "tcp port 43567" -Y "tls.handshake" \
-V -o tls.keylog_file:/dev/null 2>/dev/null | \
grep -E "(Handshake Protocol|named_group|key_share|supported_groups)"
In the capture you will see the TLS 1.3 HelloRetryRequest flow. The client’s first ClientHello advertises X25519MLKEM768 in its supported_groups extension. The server agrees on the PQC group and sends a HelloRetryRequest asking the client to provide a key share for X25519MLKEM768. The client’s second ClientHello includes a 1216-byte key share, which is the combined X25519 (32 bytes) + ML-KEM-768 (1184 bytes) public key. That 1184-byte ML-KEM component is the unmistakable fingerprint of a post-quantum key exchange.
Side-by-side comparison
| Aspect |
JDK 27 Native |
JDK 24 + BouncyCastle |
JDK 21 + BouncyCastle |
| TLS provider |
SunJSSE |
BCJSSE 1.83 |
BCJSSE 1.83 |
| Configuration |
camel.ssl.* properties |
camel.ssl.* properties |
camel.ssl.* properties |
| Certificates |
camel.ssl.selfSigned=true |
camel.ssl.selfSigned=true |
camel.ssl.selfSigned=true |
| Native ML-KEM (KEM API) |
Yes |
Yes (JEP 496) |
No |
| REST transport |
HTTPS on 8443 |
HTTPS on 8443 |
HTTPS on 8443 |
| Extra dependencies |
None |
bcprov, bctls |
bcprov, bctls |
Running the examples
All three examples follow the same pattern: select the right JDK, build, and run.
JDK 27
cd pqc-ssl-context
sdk use java 27.ea.11-open
mvn clean compile exec:exec
curl -k https://localhost:8443/api/verify-pqc
JDK 24
cd pqc-kem-jdk24
sdk use java 24.0.1-tem
mvn clean compile exec:exec
curl -k https://localhost:8443/api/verify-pqc
curl -k https://localhost:8443/api/verify-kem
JDK 21
cd pqc-ssl-context-jdk21
sdk use java 21.0.10-tem
mvn clean compile exec:exec
curl -k https://localhost:8443/api/verify-pqc
All three return "pqcVerified": true when the PQC TLS handshake succeeds.
What is not done yet
I want to be clear: this work is not finished. What we have so far is the foundation, and there are real gaps remaining.
The changes so far are in Camel’s core SSL layer. Individual Camel components that use TLS (HTTP, Kafka, JMS, AMQP, and many others) still need to adopt these settings for PQC to actually work end-to-end with each connector. Some components delegate to their own TLS stacks (Netty, Vert.x, the Kafka client library) and will need specific integration work to pass through the PQC named groups. We have started this for Netty, which now has a PQC fallback that auto-applies named groups, but the rest is still ahead of us.
The camel.ssl.selfSigned=true option is useful for demos and development, but production deployments need proper certificates and keystores. The property-based configuration (camel.ssl.keyStore, camel.ssl.trustStore) is already there for that.
The auto-configuration on JDK 25+ is a nice default, but it only helps when the JVM and the remote peer both support PQC named groups. In practice, most peers today will not, and the hybrid approach (X25519MLKEM768 falling back to X25519) handles that gracefully. But we have not tested this against a wide variety of real-world TLS endpoints yet.
The BouncyCastle JSSE path (JDK 21/24) works but requires a bootstrap class to register providers and tweak security properties. We would like to make this smoother, perhaps through auto-detection or a Camel extension, but that is not built yet.
Finally, there is the question of PQC at the application layer (signing, encryption, key encapsulation), not just the transport layer. The camel-pqc component covers some of this, but integrating PQC signatures and key management into Camel’s broader security model is a longer-term effort.
Contributions are welcome if any of this interests you.
References
16 Feb 2026
9 min read
If you’ve ever tried to report a security vulnerability to an open source project, you know the feeling. You find something real, you write a detailed report, you follow responsible disclosure, and then… silence. Or worse, pushback. The maintainer tells you it’s not a real issue. The ticket gets closed. Sometimes you get a reply that feels almost hostile, as if you just insulted someone’s work instead of trying to help.
I’ve been involved in open source for a long time and I’ve seen this pattern play out more times than I’d like to admit. There’s a strange dynamic around CVEs in our industry: reporting a vulnerability should be a normal, healthy part of software development, but instead it often feels like an accusation. And for the maintainer on the receiving end, having a CVE allocated against their project can feel like a mark of shame.
This needs to change. And it needs to change fast, because the world around us is changing faster than our culture can keep up.
The reporting nightmare
Let’s talk about what it actually looks like to report a vulnerability to a project. The experience varies wildly depending on the project, but the friction is surprisingly common even in well-established ones.
First you need to figure out if the project even has a security policy. Some projects have a SECURITY.md file, some have a dedicated email, some have nothing at all. You might end up opening a public GitHub issue because there’s no private channel, which kind of defeats the purpose of responsible disclosure.
Then comes the triage. Many maintainers, especially in the smaller projects, are volunteers. They have day jobs. They maintain software because they care about it, not because they’re paid to handle security reports. So your report might sit for weeks. If the maintainer doesn’t agree with your assessment, you’re stuck in a back-and-forth that can drag on for months. I’ve seen cases where a perfectly valid vulnerability was dismissed simply because the maintainer didn’t consider the attack scenario realistic enough.
And the CVE system itself doesn’t help. It was designed in a world where software came from vendors with support contracts and SLAs. When you apply it to open source, the assumptions break down. There’s no commercial relationship, no obligation to respond, no dedicated security team. But the expectations from downstream users and scanners remain the same: if there’s a CVE, it must be fixed, and it must be fixed now.
The result is a system that creates pressure without providing resources. Maintainers feel attacked, reporters feel ignored, and the actual security of the software suffers.
The shame problem
Here’s something that doesn’t get discussed enough: many projects treat a CVE as a failure rather then a finding. There’s a cultural undercurrent that says having vulnerabilities in your code means you wrote bad code, that it reflects poorly on you as a developer.
This is wrong, and it’s counterproductive.
Every non-trivial piece of software has bugs. Some of those bugs have security implications. This is not a moral failure, its a statistical certainty. The Linux kernel gets CVEs. OpenSSL gets CVEs. The JDK gets CVEs. These are some of the most reviewed, most tested codebases on the planet. If they have vulnerabilities, your project will too.
But the stigma persists. I’ve seen maintainers downplay severity scores, argue that a bug isn’t exploitable when it clearly is, or simply refuse to acknowledge the issue. In some cases the motivation is understandable: a high-severity CVE can trigger automated alerts across thousands of organizations, creating a firestorm of support requests that a volunteer maintainer is simply not equipped to handle. But ignoring the problem doesn’t make it go away.
The Daniel Stenberg situation with curl is a good example of the other side of this coin. He has been vocal about the flood of bogus CVE reports, many of them AI-generated, that waste maintainer time. He estimates that about 20% of all security submissions to curl are now AI-generated noise, and the rate of genuine vulnerabilities has dropped to around 5%. For every real vulnerability, there are four fake ones. Each fake one consumes hours of expert time to disprove. This is a real problem, and it’s getting worse.
But the solution isn’t to build higher walls around vulnerability reporting. The solution is to build better processes and, more importantly, a better culture.
AI is changing the game whether we like it or not
Here’s where things get interesting and, honestly, a bit uncomfortable for our industry.
In the last couple of years AI systems have gone from theoretical vulnerability discovery to actually finding real zero-day vulnerabilities in production software. This isn’t hype. The evidence is concrete and growing.
Google’s Big Sleep project, a collaboration between Project Zero and DeepMind, found an exploitable stack buffer underflow in SQLite in late 2024. The Project Zero team noted that human researchers couldn’t rediscover the same vulnerability using traditional fuzzing even after 150 CPU hours of testing. Since then, Big Sleep has found over 20 vulnerabilities across projects like FFmpeg and ImageMagick, each discovered and reproduced by the AI agent without human intervention.
Anthropic published findings showing Claude discovering previously unknown vulnerabilities, including in GhostScript, where the model took a creative approach by reading through the Git commit history after more traditional analysis methods failed.
In late 2025 and early 2026, AI systems autonomously discovered zero-day vulnerabilities in Node.js and React. Not toy projects. Not contrived examples. Two of the most widely deployed pieces of JavaScript infrastructure in the world. The vulnerabilities were real, the exploits worked, and patches were necessary.
Researchers from the University of Illinois showed that teams of LLM agents working together could exploit zero-day vulnerabilities with meaningful success rates. Trend Micro’s AESIR platform has contributed to the discovery of 21 CVEs across NVIDIA, Tencent, and MLflow since mid-2025.
The trajectory is clear: AI-discovered CVEs jumped from around 300 in 2023 to over 450 in 2024, and exceeded 1,000 in 2025. This is not slowing down.
The FFmpeg wake-up call
The FFmpeg controversy in late 2025 perfectly illustrates the collision between old culture and new technology. Google’s Big Sleep found 13 vulnerabilities in FFmpeg alone. The volunteer maintainers were understandably frustrated: a trillion-dollar company was using AI to find bugs in their code and then dropping reports on them with a 90-day disclosure countdown, without providing patches or funding.
FFmpeg’s maintainers called some of the findings “CVE slop” and asked Google to either fund the project or stop burdening volunteers with security reports. One maintainer described a bug in a LucasArts Smush codec, an issue affecting only early versions of a 1990s game, flagged as a “medium-impact” security vulnerability. Nick Wellnhofer resigned as maintainer of libxml2, citing the unsustainable workload of addressing security reports without compensation.
The maintainers have a point. But the uncomfortable truth is that those vulnerabilities still exist in the code. The fact that finding them is now cheap and fast doesn’t make them less real.
We need a new culture
So where does this leave us? I think we need to rethink our relationship with security vulnerabilities from the ground up. Here what I believe needs to change.
Vulnerabilities are not failures
We need to stop treating CVEs as marks of shame. A vulnerability report should be treated like a bug report: a normal part of the software lifecycle. The projects that handle CVEs well, with transparency, clear communication, and timely fixes, should be seen as more trustworthy, not less.
The current system wasn’t designed for the open source world. We need better processes for triaging reports, especially now that AI tools can generate them at scale. The OpenSSF and OWASP are working on this, but progress is slow. We need clear guidelines for what constitutes a valid report, better tooling for maintainers to handle volume, and a way to distinguish between genuine findings and noise.
Funding must follow expectations
If the industry expects open source maintainers to handle security reports with the same rigor as commercial vendors, then funding needs to follow. You can’t demand enterprise-grade security response from volunteers working on their spare time. Organizations that depend on open source software need to invest in the projects they rely on. This means direct funding, dedicated security resources, or at minimum, contributing patches alongside vulnerability reports.
Security education needs an update
Most developers learn about security as a set of rules: don’t use eval, sanitize your inputs, use parameterized queries. This is necessary but not sufficient. We need to teach developers that vulnerabilities are inevitable, that finding them is good, and that the process of fixing them is a skill worth developing. Security should be part of the development culture, not an external audit that happens once a year.
Prepare for the AI flood
AI-powered vulnerability discovery is here and it’s only going to accelerate. Bruce Schneier has noted that the latest models can analyze substantial codebases and produce candidate vulnerabilities in hours or minutes, fundamentally altering the economics of vulnerability discovery. Multi-agent systems where specialized LLMs collaborate on code analysis, exploit development, and verification are outperforming single-model approaches.
This means every project, regardless of size, will face an increasing volume of vulnerability reports. We need to build the infrastructure, both technical and cultural, to handle this. That includes better automated triage, clearer severity standards, and a shared understanding that a rising CVE count doesn’t mean software is getting worse. It means we’re getting better at finding problems.
Conclusion
The security landscape is shifting under our feet. AI tools are finding real vulnerabilities in real software at a pace that human researchers can’t match. Our vulnerability reporting and handling processes, built for a slower era, are showing cracks everywhere.
The answer isn’t to dismiss the findings or shoot the messenger. It’s to grow up as an industry. Treat vulnerabilities as the normal engineering challenge they are. Fund the maintainers who keep critical infrastructure running. Reform the systems that create perverse incentives. And prepare for a world where the volume of discovered vulnerabilities will only increase.
We’ve been treating CVEs as something to be ashamed of. It’s time to start treating them as something to be proud of fixing.
References
15 Oct 2025
4 min read
In the rapidly evolving landscape of AI-powered applications, the ability to process and understand documents has become increasingly crucial. Whether you’re dealing with PDFs, Word documents, or PowerPoint presentations, extracting meaningful insights from unstructured data is a challenge many developers face daily.
In this post, we’ll explore how Apache Camel’s new AI components enable developers to build sophisticated RAG (Retrieval Augmented Generation) pipelines with minimal code. We’ll combine the power of Docling for document conversion with LangChain4j for AI orchestration, all orchestrated through Camel’s YAML DSL.
The Challenge: Document Intelligence at Scale
Companies are drowning in documents. Legal firms process contracts, healthcare providers manage medical records, and financial institutions analyze reports. The traditional approach of manual document review simply doesn’t scale.
So this a possible space where we could apply RAG and Apache Camel. The steps:
- Convert documents from any format to structured text
- Extract key insights and summaries
- Answer questions about document content
- Process documents in real-time as they arrive
This is where the combination of Docling and LangChain4j shines, and Apache Camel provides the perfect integration layer to bring them together.
Meet the Components
Camel-Docling: Enterprise Document Conversion
The camel-docling component integrates IBM’s Docling library, an AI-powered document parser that can handle various formats including PDF, Word, PowerPoint, and more. What makes Docling special is its ability to preserve document structure while converting to clean Markdown, HTML, or JSON.
Key features:
- Multiple Operations: Convert to Markdown, HTML, JSON, or extract structured data
- Flexible Deployment: Works with both CLI and API (docling-serve) modes
- Content Control: Return content directly in the message body or as file paths
- OCR Support: Handle scanned documents with optical character recognition
Camel-LangChain4j: AI Orchestration Made Simple
The camel-langchain4j-chat component provides seamless integration with Large Language Models through the LangChain4j framework. It supports various LLM providers including OpenAI, Ollama, and more.
Perfect for:
- Document analysis and summarization
- Question-answering systems
- Content generation
- RAG implementations
Building a RAG Pipeline with YAML
Let’s walk through a complete example that demonstrates the power of combining these components. Our goal is to create a system that automatically processes documents, analyzes them with AI, and generates comprehensive reports: a classic example.
Architecture Overview
The flow is straightforward:
- Watch a directory for new documents
- Convert documents to Markdown using Docling
- Send the converted content to an LLM for analysis
- Generate a comprehensive analysis report
- Clean up processed files
All of this is defined declaratively in YAML, making it easy to understand and modify.
Setting Up the Infrastructure
First, we need our services running. Thanks to camel infra command, this is pretty simple:
# Start Docling (if camel infra supports it)
$ jbang -Dcamel.jbang.version=4.16.0-SNAPSHOT camel@apache/camel infra run docling
# Start Ollama (if camel infra supports it)
$ jbang -Dcamel.jbang.version=4.16.0-SNAPSHOT camel@apache/camel infra run ollama
Or we could use docker
# Start Docling-Serve
$ docker run -d -p 5001:5001 --name docling-serve ghcr.io/docling-project/docling-serve:latest
# Start Ollama
$ docker run -d -p 11434:11434 --name ollama ollama/ollama:latest
# Pull orca-mini model
$ docker exec -it ollama ollama pull orca-mini
We could also use docker-compose:
$ docker compose up -d
$ docker exec -it ollama ollama pull orca-mini
Configuring the Chat Model
We use a Groovy script bean to configure our LangChain4j chat model:
- beans:
- name: chatModel
type: "#class:dev.langchain4j.model.ollama.OllamaChatModel"
scriptLanguage: groovy
script: |
import dev.langchain4j.model.ollama.OllamaChatModel
import static java.time.Duration.ofSeconds
return OllamaChatModel.builder()
.baseUrl("{{ollama.base.url}}")
.modelName("{{ollama.model.name}}")
.temperature(0.3)
.timeout(ofSeconds(120))
.build()
Notice how we use property placeholders ({{ollama.base.url}}) which Camel automatically resolves. This makes the configuration flexible and environment-agnostic.
The Main RAG Route
Here’s where the magic happens. The route watches for documents, processes them through Docling, and analyzes them with our LLM:
- route:
id: document-analysis-workflow
from:
uri: file:{{documents.directory}}
parameters:
include: ".*\\.(pdf|docx|pptx|html|md)"
noop: true
idempotent: true
steps:
- log: "Processing document: ${header.CamelFileName}"
# Convert GenericFile to file path
- setBody:
simple: "${body.file.absolutePath}"
# Convert to Markdown
- to:
uri: docling:CONVERT_TO_MARKDOWN
parameters:
useDoclingServe: true
doclingServeUrl: "{{docling.serve.url}}"
contentInBody: true
# Prepare AI prompt
- setBody:
simple: |
You are a helpful document analysis assistant. Please analyze
the following document and provide:
1. A brief summary (2-3 sentences)
2. Key topics and main points
3. Any important findings or conclusions
Document content:
${exchangeProperty.convertedMarkdown}
# Get AI analysis
- to:
uri: langchain4j-chat:analysis
parameters:
chatModel: "#chatModel"
Interactive Q&A API
We also provide an HTTP endpoint for asking questions about documents:
- route:
id: document-qa-api
from:
uri: platform-http:/api/ask
steps:
# Find latest document
# Convert with Docling
# Prepare RAG prompt with user question
# Get answer from LLM
This enables interactive workflows:
$ curl -X POST http://localhost:8080/api/ask \
-d "What are the main topics in this document?"
Future Enhancements
Possible developments could be:
- Vector Storage Integration: Combine with camel-langchain4j-embeddings to store document chunks in vector databases for more sophisticated retrieval.
- Multi-Model Workflows: Use different models for different tasks - fast models for classification, powerful models for analysis.
- Streaming Responses: For long documents, stream LLM responses back to the client as they’re generated.
- Custom Tools: Integrate camel-langchain4j-tools to give the LLM access to external data sources.
Try It Yourself
The complete example is available in the Apache Camel repository under camel-jbang-examples/docling-langchain4j-rag. To run it:
$ jbang -Dcamel.jbang.version=4.16.0-SNAPSHOT camel@apache/camel run \
--fresh \
--dep=camel:docling \
--dep=camel:langchain4j-chat \
--dep=camel:platform-http \
--dep=dev.langchain4j:langchain4j:1.6.0 \
--dep=dev.langchain4j:langchain4j-ollama:1.6.0 \
--properties=application.properties \
docling-langchain4j-rag.yaml
Don’t forget to copy the sample.md into the documents directory!
Watch the logs as your document is processed, analyzed, and cleaned up automatically!
Conclusion
The combination of Apache Camel’s integration capabilities, Docling’s document conversion power, and LangChain4j’s AI orchestration creates a compelling platform for building intelligent document processing systems.
What makes this especially powerful is the declarative nature of the solution. The entire workflow is defined in ~350 lines of readable YAML, making it easy to understand, modify, and extend.
We’d love to hear about what you build with these components. Share your experiences on the Apache Camel mailing list or join us on Zulip chat!
Stay tuned for more examples combining Camel’s growing AI component ecosystem. The future of integration is intelligent, and we’re just getting started.
Happy integrating!
20 Dec 2022
4 min read
Starting from Camel 3.19.0 we have four cloud services supported for loading properties as secrets:
- AWS Secret Manager
- Google Cloud Secret Manager
- Azure Key Vault
- Hashicorp Vault
One of the problems we faced in the development was related to finding a way to automatically refresh the secret value on the secrets update.
The main players in the cloud game are providing solutions based on their services:
AWS provides multiple ways to be notified about secret updates and secret rotations through AWS Cloudtrail or AWS Cloud events, GCP leverages Google Pubsub to deliver messages with events related to secret,
while Azure provides multiple ways of getting notified about events related to a vault in the Azure Key Vault service, mostly by using Azure Eventgrid as an intermediate service.
Hashicorp Vault as of today doesn’t provide an API to get secrets notification.
Enabling Automatic Camel context reloading after Secrets Refresh
AWS Secrets Manager
The automatic context reloading could be achieved through Camel’s main properties. In particular, the authentication to AWS service (Cloudtrail) could be set through the default credentials provider or through access key/secret key/region credentials. The camel’s main properties are:
camel.vault.aws.refreshEnabled=true
camel.vault.aws.refreshPeriod=60000
camel.vault.aws.secrets=Secret
camel.main.context-reload-enabled = true
where camel.vault.aws.refreshEnabled will enable the automatic context to reload, camel.vault.aws.refreshPeriod is the interval of time between two different checks for update events, and camel.vault.aws.secrets is a regex representing the secrets we want to track for updates.
The property camel.vault.aws.secrets is not mandatory: if not specified the task responsible for checking updates events will take into account the properties with an aws: prefix.
The mechanism behind this feature, for what is related to AWS Secrets Manager, involves the AWS Cloudtrail service. The task will search for Secrets in camel properties associated with AWS prefixes and look for events in the Cloudtrail entries related to them. Once the task will find an update operation it will trigger the context reloading.
At the following URL, we provide a simple example through camel-jbang: AWS Secrets Manager Example
Google Secret Manager
The automatic context reloading could be achieved through Camel’s main properties. In particular, the authentication to Google service (Pubsub) could be set through the default google instance or through the service account key file. The camel’s main properties are:
camel.vault.gcp.projectId= projectId
camel.vault.gcp.refreshEnabled=true
camel.vault.gcp.refreshPeriod=60000
camel.vault.gcp.secrets=hello*
camel.vault.gcp.subscriptionName=subscriptionName
camel.main.context-reload-enabled = true
where camel.vault.gcp.refreshEnabled will enable the automatic context reloading, camel.vault.gcp.refreshPeriod is the interval of time between two different checks for update events, and camel.vault.gcp.secrets is a regex representing the secrets we want to track for updates.
The property camel.vault.gcp.secrets is not mandatory: if not specified the task responsible for checking updates events will take into account the properties with a gcp: prefix.
The camel.vault.gcp.subscriptionName is the subscription name created about the Google PubSub topic associated with the tracked secrets.
This mechanism while making use of the notification system related to Google Secret Manager: through this feature, every secret could be associated with one up to ten Google Pubsub Topics. These topics will receive events related to the life cycle of the secret.
At the following URL, we provide a simple example through camel-jbang: Google Secret Manager Example
Azure Key Vault
The automatic context reloading could be achieved through Camel’s main properties. In particular, the authentication to Azure service (Storage Blob) could be set through client id/client secret/tenant id. The camel’s main properties are:
camel.vault.azure.refreshEnabled=true
camel.vault.azure.refreshPeriod=60000
camel.vault.azure.secrets=Secret
camel.vault.azure.eventhubConnectionString=eventhub_conn_string
camel.vault.azure.blobAccountName=blob_account_name
camel.vault.azure.blobContainerName=blob_container_name
camel.vault.azure.blobAccessKey=blob_access_key
camel.main.context-reload-enabled = true
where camel.vault.azure.refreshEnabled will enable the automatic context to reload, camel.vault.azure.refreshPeriod is the interval of time between two different checks for update events, and camel.vault.azure.secrets is a regex representing the secrets we want to track for updates.
The property camel.vault.azure.eventhubConnectionString is the eventhub connection string to get notifications from, camel.vault.azure.blobAccountName, camel.vault.azure.blobContainerName, and camel.vault.azure.blobAccessKey are the Azure Storage Blob parameters for the checkpoint store needed by Azure Eventhub.
The property camel.vault.azure.secrets is not mandatory: if not specified the task responsible for checking updates events will take into account the properties with an azure: prefix.
At the following URL, we provide a simple example through camel-jbang: Azure Key Vault Example
A real example
While working on the feature I tried to set up a real use-case scenario. In my case, I’ve been trying to update a database administrator password while a running camel integration was querying the database.
This is well explained in this example the reader could run through camel-jbang: MySQL Database Password Refresh Example
Future
In the next Camel development for what concerns the secret updates feature, we would like to provide the ability to select a different kinds of tasks/policies to trigger a context reloading. For example, we would like to support the secret rotations events coming from AWS Services supporting the rotation. This is in our roadmap.
If you find this feature useful and you feel there is something to improve, don’t hesitate to contact us via the usual channels: Camel Support
Thanks for your attention and see you online.