Skip to content

Silent data leak across rows with ES|QL IP_PREFIX command #141628

@strawgate

Description

@strawgate

Elasticsearch Version

Latest

Installed Plugins

No response

Java Version

bundled

OS Version

latest

Problem Description

The IP_PREFIX function has a bug in makePrefix() where the byte at index fullBytes is not cleared when remainingBits == 0 (i.e., when the prefix length is a multiple of 8). Due to scratch buffer reuse across rows, this causes data from previously processed rows to leak into subsequent results.

private static void makePrefix(BytesRef ip, BytesRef scratch, int fullBytes, int remainingBits) {
    // Copy the first full bytes
    System.arraycopy(ip.bytes, ip.offset, scratch.bytes, 0, fullBytes);

    // Copy the last byte ignoring the trailing bits
    if (remainingBits > 0) {
        byte lastByteMask = (byte) (0xFF << (8 - remainingBits));
        scratch.bytes[fullBytes] = (byte) (ip.bytes[ip.offset + fullBytes] & lastByteMask);
    }

    // Copy the last empty bytes - BUG: starts at fullBytes + 1, not fullBytes
    if (fullBytes < 16) {
        Arrays.fill(scratch.bytes, fullBytes + 1, 16, (byte) 0);
    }
}

When remainingBits == 0:

  1. array copy copies bytes 0 to fullBytes - 1
  2. The if (remainingBits > 0) block is skipped, so byte fullBytes is NOT set
  3. Arrays.fill starts at fullBytes + 1, leaving byte fullBytes untouched
  4. Byte fullBytes retains its value from the previous row's computation

Some rows with v6_prefix=72 and ip=::1 will show incorrect prefix values like ::ff:0:0:0 instead of ::, because byte 9 retains 0xFF from a previously processed row with v6_prefix=80.

Expected Behavior
All rows with ip=::1 and v6_prefix=72 should return prefix :: (first 72 bits, which are all zeros).

Fix:
Change line 181 from:

Arrays.fill(scratch.bytes, fullBytes + 1, 16, (byte) 0);

to:

Arrays.fill(scratch.bytes, fullBytes, 16, (byte) 0);

Or add explicit zeroing when remainingBits == 0:

if (remainingBits > 0) {
    byte lastByteMask = (byte) (0xFF << (8 - remainingBits));
    scratch.bytes[fullBytes] = (byte) (ip.bytes[ip.offset + fullBytes] & lastByteMask);
} else if (fullBytes < 16) {
    scratch.bytes[fullBytes] = 0;
}

Steps to Reproduce

PUT ip-prefix-test/_doc/1
{"ip": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "v6_prefix": 80}

PUT ip-prefix-test/_doc/2  
{"ip": "::1", "v6_prefix": 72}

PUT ip-prefix-test/_doc/3
{"ip": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "v6_prefix": 80}

PUT ip-prefix-test/_doc/4 
{"ip": "::1", "v6_prefix": 72}
FROM ip-prefix-test
| EVAL prefix = IP_PREFIX(ip::ip, 32, v6_prefix::int)
| KEEP ip, v6_prefix, prefix

Results:

Image

Delete the doc 1 and doc 3 and run the query again:

Image

Logs (if relevant)

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions