Blog post

Code Security for Conversational AI: Uncovering a Zip Slip in EDDI

Paul Gerste photo

Paul Gerste

Vulnerability Researcher

Date

From time to time, our Vulnerability Researchers enjoy playing Capture the Flag (CTF) competitions. These are great opportunities to sharpen our skills, learn new techniques, and connect with the security community. Some CTF challenges even come with source code and are a great way to battle-test our code analysis engine!

Earlier this year, we played KalmarCTF with team FluxFingers and encountered an interesting challenge called Red wEDDIng, which was solved by 8 out of 287 teams. The challenge consisted of an instance of EDDI, an open-source prompt and conversation management middleware for conversational AI APIs. The instance was running the latest version available at the time, so the task was to find one or more 0-day vulnerabilities that allow reading the flag file from the challenge server.

Since EDDI is open-source and written in Java, it was a perfect opportunity to use SonarQube to scan its code for vulnerabilities. While SonarQube is a tool for developers and is best integrated within the software development life cycle (SDLC), the underlying code analysis engine is able to find vulnerabilities regardless of the context. In this case, it allowed us to benchmark our engine against a real-world code base, and it helped us to be the first team to solve the challenge!

CVE-2025-32779: Zip Slip in Bot Import

The code scan finished quickly, and SonarQube raised a Zip Slip vulnerability. This was a 0-day vulnerability at the time and was later assigned CVE-2025-32779. You can view the issue on SonarQube Cloud (no account required) to follow along and explore the code. Let's dive in:

As we can see, EDDI unpacks a ZIP archive and writes the contained files into a target extraction directory. This is a classic example of a Zip Slip vulnerability: The file path is constructed from the ZIP entry's file name, which can contain arbitrary characters, including a path traversal sequence such as ../../../. The attacker can therefore control the resulting path that the ZIP entry's content is written to. 

We immediately investigated the finding to confirm if it was exploitable in the realm of the competition. Indeed, the functionality containing the issue was exposed to anyone connecting to the server! When sending a ZIP archive to the /backup/import HTTP endpoint, the server unpacks it to a temporary directory and uses the contained files to configure a bot.

To confirm that the Zip Slip vulnerability is indeed exploitable by an attacker, we created a simple ZIP file that contains a file with a path traversal sequence in its name. After sending it to the vulnerable endpoint, we observed the file being written outside of the temporary extraction directory, confirming exploitability. This verifies that SonarQube's finding is indeed a serious vulnerability.


What to Overwrite?

The Zip Slip vulnerability allows an attacker to write files on the filesystem of the instance, only limited by the file permissions. At this point, we needed to think about how this file write primitive can be used by an attacker to execute code on the server. For this, we started to investigate which files can be written on the server using the Zip Slip vulnerability.

Using find / -writable, we enumerated all files and directories writable by the user that the EDDI application was running as. To our surprise, we would have been able to overwrite quite a lot, including executable files in the /opt/jboss/ directory, which contained the JBoss application server that was hosting the EDDI application:


/opt
/opt/jboss
/opt/jboss/container
/opt/jboss/container/java
/opt/jboss/container/java/proxy
/opt/jboss/container/java/proxy/proxy-options
/opt/jboss/container/java/proxy/parse-proxy-url.sh
/opt/jboss/container/java/proxy/translate-no-proxy.sh
/opt/jboss/container/java/s2i
/opt/jboss/container/java/s2i/s2i-core-hooks
/opt/jboss/container/java/s2i/maven-overrides
/opt/jboss/container/java/s2i/maven-s2i-overrides
/opt/jboss/container/java/jvm
[...]


The first attempt was to overwrite one of these files and cause it to be executed by restarting the server in our debugging setup. However, there was no way to restart the server in the real challenge setup, and even crashing it did not lead to a restart but rendered the instance unavailable.

We investigated further and noticed the /deployments/ folder that contained the Java app deployed to the JBoss server. We knew that some application servers would hot-reload applications when they detect file changes, so we tried overwriting the application's main JAR file, but this also didn't work.

We also noticed that the dependency JARs contained inside the app's main JAR were copied to /deployments/lib/main/. Since we had a local debugging setup, we started to trace the application process using strace to see if it would read one of those dependency JAR files when interacting with the application. And indeed, we noticed some file reads of JAR files:


1206081 statx(AT_FDCWD</deployments>, "/deployments/lib/main/io.netty.netty-transport-4.1.118.Final.jar", AT_STATX_SYNC_AS_STAT, STATX_ALL, {stx_mask=STATX_ALL|STATX_MNT_ID, stx_attributes=0, stx_mode=S_IFREG|0644, stx_size=521428, ...}) = 0
1206081 openat(AT_FDCWD</deployments>, "/deployments/lib/main/io.netty.netty-transport-4.1.118.Final.jar", O_RDONLY) = 25</deployments/lib/main/io.netty.netty-transport-4.1.118.Final.jar>
1206081 statx(AT_FDCWD</deployments>, "/deployments/lib/main/io.netty.netty-common-4.1.118.Final.jar", AT_STATX_SYNC_AS_STAT, STATX_ALL, {stx_mask=STATX_ALL|STATX_MNT_ID, stx_attributes=0, stx_mode=S_IFREG|0644, stx_size=719225, ...}) = 0
1206081 openat(AT_FDCWD</deployments>, "/deployments/lib/main/io.netty.netty-common-4.1.118.Final.jar", O_RDONLY) = 13</deployments/lib/main/io.netty.netty-common-4.1.118.Final.jar>
1206084 statx(AT_FDCWD</deployments>, "/deployments/lib/main/io.vertx.vertx-core-4.5.13.jar", AT_STATX_SYNC_AS_STAT, STATX_ALL, {stx_mask=STATX_ALL|STATX_MNT_ID, stx_attributes=0, stx_mode=S_IFREG|0644, stx_size=1668417, ...}) = 0
[...]


We quickly prepared a payload that would overwrite that JAR file, uploaded it, and waited for our payload to execute. However, we just got NoClassDefFoundError and ClassNotFoundException in the logs:


java.lang.NoClassDefFoundError: io/netty/handler/codec/http/DefaultLastHttpContent
      at io.netty.handler.codec.http.HttpObjectDecoder.decode(HttpObjectDecoder.java:444)
      [...]
      at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
      at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: java.lang.ClassNotFoundException: io.netty.handler.codec.http.DefaultLastHttpContent
      at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
      ... 24 more


Lazy-Loaded Classes

First, we were confused, but then realized that EDDI was not loading the whole JAR file at once. It was instead lazy-loading a single class from that JAR file. The class was apparently first used when triggering a certain functionality in EDDI, which is why it had not been loaded before.

With this key observation, we had everything we needed to solve the challenge, at least in theory. After trying to build a JAR file that contained a class with the same fully qualified name as the class from the ClassNotFoundException, we realized that the JAR file was now missing many other classes that EDDI needed to function properly.

During the CTF, we extracted the original JAR, replaced only the target class, and packed everything back into a JAR. This eventually worked after overcoming other minor obstacles, and got us the flag! We were the first team to solve the challenge, and finding the vulnerable feature with only a quick SonarQube scan definitely helped with that.

Patch

After the CTF, we reported the vulnerability to EDDI's maintainers in case they weren't already aware of it. They fixed it in version 5.5.0 by validating that the destination path is inside the extraction directory during ZIP extraction:


File destFile = new File(targetDir, entry.getName());
String destFilePath = destFile.getCanonicalPath();

// Ensure the resolved destination path starts with the target directory path
if (!destFilePath.startsWith(targetDirPath + File.separator)) {
    throw new IOException("Entry is outside of the target dir: " + entry.getName());
}

Timeline

DateAction
2025-03-11We report the vulnerability to the EDDI maintainers
2025-04-07The maintainers commit a fix
2025-04-12The maintainers release version 5.5.0, which contains the fix
2025-04-12The maintainers release an advisory

Summary

In this blog post, we saw a great example of how SonarQube detects real-world vulnerabilities. CTF competitions target a technical security audience, and the challenges aim to be difficult. It is great to see that using SonarQube is an advantage in these scenarios as well, making it easy to find and understand vulnerabilities in the code of an application.

The Zip Slip vulnerability, a special case of Path Traversal, shows once again that path-related issues are still very common. With Zip Slips, the attacker-controlled file name does not directly result from a user input, but from a user-uploaded file, which can make it less obvious. We also learned that Java classes can be lazy-loaded during runtime and how attackers can exploit this behavior.

Finally, we would like to thank the EDDI maintainers for fixing the issue we reported. Kudos also to the Kalmarunionen team for creating a fun and challenging CTF!

Related Blog Posts

Get new blogs delivered directly to your inbox!

Stay up-to-date with the latest Sonar content. Subscribe now to receive the latest blog articles. 

I do not wish to receive promotional emails about upcoming SonarQube updates, new releases, news and events.

By submitting this form, you agree to the storing and processing of your personal data as described in the Privacy Policy and Cookie Policy. You can withdraw your consent by unsubscribing at any time.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

  • Follow SonarSource on Twitter
  • Follow SonarSource on Linkedin
language switcher
日本語 (Japanese)
  • 法的文書
  • トラスト センター

© 2008-2024 SonarSource SA.無断複写·転載を禁じます。SONAR、SONARSOURCE、SONARLINT、SONARQUBE、およびCLEAN AS YOU CODEは、SonarSource SAの商標です。