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

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions