CVE-2023-36480 (Java, published 2023-08-04T15:15Z)
Description: value: The Aerospike Java client is a Java application that implements a network protocol to communicate with an Aerospike server. Prior to versions 7.0.0, 6.2.0, 5.2.0, and 4.5.0 some of the messages received from the server contain Java objects that the client deserializes when it encounters them without further validation. Attackers that manage to trick clients into communicating with a malicious server can include especially crafted objects in its responses that, once deserialized by the client, force it to execute arbitrary code. This can be abused to take control of the machine the client is running on. Versions 7.0.0, 6.2.0, 5.2.0, and 4.5.0 contain a patch for this issue.
Severity: nan (CVSS v3 base score: 9.8)
Weakness: CWE-502 \u2014 Deserialization of Untrusted Data
The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.
Repository: https://github.com/aerospike/aerospike-client-java
Files changed:
client/src/com/aerospike/client/Bin.java
client/src/com/aerospike/client/Value.java
client/src/com/aerospike/client/command/Buffer.java
client/src/com/aerospike/client/command/ParticleType.java
client/src/com/aerospike/client/lua/LuaInstance.java
client/src/com/aerospike/client/lua/LuaUnpacker.java
client/src/com/aerospike/client/util/Packer.java
client/src/com/aerospike/client/util/Unpacker.java
examples/src/com/aerospike/examples/Main.java
examples/src/com/aerospike/examples/OperateMap.java
test/src/com/aerospike/test/SuiteSync.java
test/src/com/aerospike/test/sync/basic/TestAdd.java
Commit message: CVE-2023-36480 CLIENT-2252 Remove all code that used java runtime serialization/deserialization.

Deserialization of java runtime serialized objects has been identified as a security risk by
CodeQL team members @atorralba (Tony Torralba) and @joefarebrother (Joseph Farebrother).

All existing database objects that are serialized using this serialization format will need to
be converted to a safer format (Aerospike native types, protobuf, msgpack, json, xml ...) using
a previous client version.


Vulnerable code:
	/**
	 * Create bin with an object value. This is the slowest of the Bin constructors because the type
	 * must be determined using multiple "instanceof" checks. If the object type is unrecognized,
	 * the default java serializer is used.
	 * <p>
	 * To disable this constructor, set {@link com.aerospike.client.Value#DisableSerializer} to true.
	 *
	 * @param name		bin name, current limit is 15 characters.
	 * @param value		bin value
	 */
	public Bin(String name, Object value) {
		this.name = name;
		this.value = Value.get(value);
	}

	/**
	 * Create bin with a blob value.  The value will be java serialized.
	 * This method is faster than the bin object constructor because the blob is converted
	 * directly instead of using multiple "instanceof" type checks with a blob default.
	 * <p>
	 * To disable this method, set {@link com.aerospike.client.Value#DisableSerializer} to true.
	 *
	 * @param name		bin name, current limit is 15 characters.
	 * @param value		bin value
	 */
	public static Bin asBlob(String name, Object value) {
		return new Bin(name, Value.getAsBlob(value));
	}

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
	/**
	 * Should default object serializer be disabled. If true, an exception will be thrown when
	 * a default object serialization is attempted. Default object serialization is triggered
	 * when a bin constructed by {@link com.aerospike.client.Bin#Bin(String, Object)} or
	 * {@link com.aerospike.client.Bin#asBlob(String, Object)} is used in a write command
	 * with an unrecognized object type.
	 */
	public static boolean DisableSerializer = false;

	/**
	 * Should default object deserializer be disabled. If true, an exception will be thrown when
	 * a default object deserialization is attempted. Default object serialization is triggered
	 * when serialized data is read/parsed from the server. DisableDeserializer is separate from
	 * DisableSerializer because there may be cases when no new serialization is allowed, but
	 * existing serialized objects need to be supported.
	 */
	public static boolean DisableDeserializer = false;

	/**
	 * Get blob or null value instance.
	 */
	public static Value getAsBlob(Object value) {
		return (value == null)? NullValue.INSTANCE : new BlobValue(value);
	}

		return new BlobValue(value);
	/**
	 * Blob value.
	 */
	public static final class BlobValue extends Value {
		private final Object object;
		private byte[] bytes;

		public BlobValue(Object object) {
			this.object = object;
		}

		@Override
		public int estimateSize() throws AerospikeException.Serialize {
			bytes = serialize(object);
			return bytes.length;
		}

		public static byte[] serialize(Object val) {
			if (DisableSerializer) {
				throw new AerospikeException("Object serializer has been disabled");
			}

			try (ByteArrayOutputStream bstream = new ByteArrayOutputStream()) {
				try (ObjectOutputStream ostream = new ObjectOutputStream(bstream)) {
					ostream.writeObject(val);
				}
				return bstream.toByteArray();
			}
			catch (Throwable e) {
				throw new AerospikeException.Serialize(e);
			}
		}

		@Override
		public int write(byte[] buffer, int offset) {
			System.arraycopy(bytes, 0, buffer, offset, bytes.length);
			return bytes.length;
		}

		@Override
		public void pack(Packer packer) {
			packer.packBlob(object);
		}

		@Override
		public void validateKeyType() {
			throw new AerospikeException(ResultCode.PARAMETER_ERROR, "Invalid key type: jblob");
		}

		@Override
		public int getType() {
			return ParticleType.JBLOB;
		}

		@Override
		public Object getObject() {
			return object;
		}

		@Override
		public LuaValue getLuaValue(LuaInstance instance) {
			return LuaString.valueOf(bytes);
		}

		@Override
		public String toString() {
			return Buffer.bytesToHexString(bytes);
		}

		@Override
		public boolean equals(Object other) {
			return (other != null &&
				this.getClass().equals(other.getClass()) &&
				this.object.equals(((BlobValue)other).object));
		}

		@Override
		public int hashCode() {
			return object.hashCode();
		}
	}

import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
		case ParticleType.JBLOB:
			return Buffer.bytesToObject(buf, offset, len);

	public static Object bytesToObject(byte[] buf, int offset, int length) {
		if (length <= 0) {
			return null;
		}

		if (Value.DisableDeserializer) {
			throw new AerospikeException.Serialize("Object deserializer has been disabled");
		}

		try (ByteArrayInputStream bastream = new ByteArrayInputStream(buf, offset, length)) {
			try (ObjectInputStream oistream = new ObjectInputStream(bastream)) {
				return oistream.readObject();
			}
		}
		catch (Throwable e) {
			throw new AerospikeException.Serialize(e);
		}
	}

 * Copyright 2012-2021 Aerospike, Inc.
 * Bin type for byte[] values.
	// Server particle types. Unsupported types are commented out.
	//public static final int TIMESTAMP = 5;
	//public static final int DIGEST = 6;
	public static final int JBLOB = 7;
	//public static final int CSHARP_BLOB = 8;
	//public static final int PYTHON_BLOB = 9;
	//public static final int RUBY_BLOB = 10;
	//public static final int PHP_BLOB = 11;
	//public static final int ERLANG_BLOB = 12;
 * Copyright 2012-2021 Aerospike, Inc.
		case ParticleType.JBLOB:
			Object object = Buffer.bytesToObject(buf, offset, len);
			return new LuaJavaBlob(object);

/*
 * Copyright 2012-2021 Aerospike, Inc.
 *
 * Portions may be licensed to Aerospike, Inc. under one or more contributor
 * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.aerospike.client.lua;

import org.luaj.vm2.LuaUserdata;

public final class LuaJavaBlob extends LuaUserdata implements LuaData {

	public LuaJavaBlob(Object object) {
		super(object);
	}

	public Object luaToObject() {
		return m_instance;
	}
}
 * Copyright 2012-2021 Aerospike, Inc.
	@Override
	protected LuaValue getJavaBlob(Object value) {
		return new LuaJavaBlob(value);
	}

import com.aerospike.client.Value.BlobValue;
	public void packBlob(Object val) {
		byte[] bytes = BlobValue.serialize(val);
		packByteArrayBegin(bytes.length + 1);
		packByte(ParticleType.JBLOB);
		packByteArray(bytes, 0, bytes.length);
	}

		packBlob(obj);
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
		case ParticleType.JBLOB:
			if (Value.DisableDeserializer) {
				throw new AerospikeException.Serialize("Object deserializer has been disabled");
			}

			try (ByteArrayInputStream bastream = new ByteArrayInputStream(buffer, offset, count)) {
				try (ObjectInputStream oistream = new ObjectInputStream(bastream)) {
					val = getJavaBlob(oistream.readObject());
				}
			}
			catch (Throwable e) {
				throw new AerospikeException.Serialize(e);
			}
			break;

	protected abstract T getJavaBlob(Object value);
		@Override
		protected Object getJavaBlob(Object value) {
			return value;
		}

		"Serialize",
		l1.add(Value.get(new GregorianCalendar(2018, 1, 1).getTime()));
		l2.add(Value.get(new GregorianCalendar(2018, 1, 2).getTime()));
		l3.add(Value.get(new GregorianCalendar(2018, 2, 1).getTime()));
		l4.add(Value.get(new GregorianCalendar(2018, 2, 2).getTime()));
		l5.add(Value.get(new GregorianCalendar(2018, 2, 5).getTime()));
		end.add(Value.get(new GregorianCalendar(2018, 2, 2).getTime()));
/*
 * Copyright 2012-2023 Aerospike, Inc.
 *
 * Portions may be licensed to Aerospike, Inc. under one or more contributor
 * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.aerospike.examples;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import com.aerospike.client.AerospikeClient;
import com.aerospike.client.Bin;
import com.aerospike.client.Key;
import com.aerospike.client.Record;
import com.aerospike.client.Value;

public class Serialize extends Example {

	public Serialize(Console console) {
		super(console);
	}

	/**
	 * Write complex objects using serializer.
	 */
	@Override
	public void runExample(AerospikeClient client, Parameters params) throws Exception {
		testArray(client, params);
		testList(client, params);
		testComplex(client, params);
	}

	/**
	 * Write array of integers using standard java serializer..
	 */
	public void testArray(AerospikeClient client, Parameters params) throws Exception {
		Key key = new Key(params.namespace, params.set, "serialarraykey");

		// Delete record if it already exists.
		client.delete(params.writePolicy, key);

		console.info("Initialize array");

		int[] array = new int[10000];

		for (int i = 0; i < 10000; i++) {
			array[i] = i * i;
		}

		Bin bin = new Bin("serialbin", array);

		// Do a test that pushes this complex object through the serializer
		console.info("Write array using serializer.");
		client.put(params.writePolicy, key, bin);

		console.info("Read array using serializer.");
		Record record = client.get(params.policy, key, bin.name);

		if (record == null) {
			throw new Exception(String.format(
				"Failed to get: namespace=%s set=%s key=%s",
				key.namespace, key.setName, key.userKey));
		}

		int[] received;

		try {
			received = (int[])record.getValue(bin.name);
		}
		catch (Exception e) {
			throw new Exception(String.format(
				"Failed to parse returned value: namespace=%s set=%s key=%s bin=%s",
				key.namespace, key.setName, key.userKey, bin.name));
		}

		if (received.length != 10000) {
			throw new Exception(String.format(
				"Array length mismatch: Expected=%d Received=%d", 10000, received.length));
		}

		for (int i = 0; i < 10000; i++) {
			if (received[i] != i * i) {
				throw new Exception(String.format(
					"Mismatch: index=%d expected=%d received=%d", i, i*i, received[i]));
			}
		}

		console.info("Read array successful.");
	}

	/**
	 * Write list using standard java serializer.
	 */
	public void testList(AerospikeClient client, Parameters params) throws Exception {
		Key key = new Key(params.namespace, params.set, "seriallistkey");

		// Delete record if it already exists.
		client.delete(params.writePolicy, key);

		console.info("Initialize list");

		ArrayList<String> list = new ArrayList<String>();
		list.add("string1");
		list.add("string2");
		list.add("string3");

		Bin bin = new Bin("serialbin", (Object)list);

		console.info("Write list using serializer.");
		client.put(params.writePolicy, key, bin);

		console.info("Read list using serializer.");
		Record record = client.get(params.policy, key, bin.name);

		if (record == null) {
			throw new Exception(String.format(
				"Failed to get: namespace=%s set=%s key=%s",
				key.namespace, key.setName, key.userKey));
		}

		List<?> received;

		try {
			received = (List<?>) record.getValue(bin.name);
		}
		catch (Exception e) {
			throw new Exception(String.format(
				"Failed to parse returned value: namespace=%s set=%s key=%s bin=%s",
				key.namespace, key.setName, key.userKey, bin.name));
		}

		if (received.size() != 3) {
			throw new Exception(String.format(
				"Array length mismatch: Expected=%d Received=%d", 3, received.size()));
		}

		for (int i = 0; i < received.size(); i++) {
			String expected = "string" + (i + 1);
			if (! received.get(i).equals(expected)) {
				Object obj = received.get(i);
				throw new Exception(String.format(
					"Mismatch: index=%d expected=%s received=%s", i, expected, obj));
			}
		}

		console.info("Read list successful.");
	}

	/**
	 * Write complex object using standard java serializer.
	 */
	public void testComplex(AerospikeClient client, Parameters params) throws Exception {
		Key key = new Key(params.namespace, params.set, "serialcomplexkey");

		// Delete record if it already exists.
		client.delete(params.writePolicy, key);

		console.info("Initialize complex object");

		ArrayList<Object> inner = new ArrayList<Object>();
		inner.add("string2");
		inner.add(8);

		HashMap<Object,Object> innerMap = new HashMap<Object,Object>();
		innerMap.put("a", 1);
		innerMap.put(2, "b");
		innerMap.put("list", inner);

		ArrayList<Object> list = new ArrayList<Object>();
		list.add("string1");
		list.add(4);
		list.add(inner);
		list.add(innerMap);

		Bin bin = new Bin("complexbin", new Value.BlobValue(list));

		console.info("Write complex object using serializer.");
		client.put(params.writePolicy, key, bin);

		console.info("Read complex object using serializer.");
		Record record = client.get(params.policy, key, bin.name);

		if (record == null) {
			throw new Exception(String.format(
				"Failed to get: namespace=%s set=%s key=%s",
				key.namespace, key.setName, key.userKey));
		}

		Object received;

		try {
			received = (List<?>) record.getValue(bin.name);
		}
		catch (Exception e) {
			throw new Exception(String.format(
				"Failed to parse returned value: namespace=%s set=%s key=%s bin=%s",
				key.namespace, key.setName, key.userKey, bin.name));
		}

		if (received != null && received.equals(list)) {
			console.info("Data matched: namespace=%s set=%s key=%s bin=%s value=%s",
				key.namespace, key.setName, key.userKey, bin.name, received);
		}
		else {
			console.error("Data mismatch");
			console.error("Expected " + list);
			console.error("Received " + received);
		}
		console.info("Read complex object successful.");
	}
}
import com.aerospike.test.sync.basic.TestSerialize;
	TestSerialize.class,
		Bin bin = new Bin(binName, (Long)null);
/*
 * Copyright 2012-2023 Aerospike, Inc.
 *
 * Portions may be licensed to Aerospike, Inc. under one or more contributor
 * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.aerospike.test.sync.basic;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.junit.Test;

import com.aerospike.client.Bin;
import com.aerospike.client.Key;
import com.aerospike.client.Record;
import com.aerospike.client.Value;
import com.aerospike.test.sync.TestSync;

public class TestSerialize extends TestSync {
	private static final String binName = "serialbin";

	@Test
	public void serializeArray() {
		Key key = new Key(args.namespace, args.set, "serialarraykey");

		// Delete record if it already exists.
		client.delete(null, key);

		int[] array = new int[10000];

		for (int i = 0; i < 10000; i++) {
			array[i] = i * i;
		}

		Bin bin = new Bin(binName, array);

		// Do a test that pushes this complex object through the serializer
		client.put(null, key, bin);

		Record record = client.get(null, key, bin.name);
		assertRecordFound(key, record);

		int[] received = null;

		try {
			received = (int[])record.getValue(bin.name);
		}
		catch (Exception e) {
			fail("Failed to parse returned value: namespace=" + key.namespace + " set=" + key.setName +
				 " key=" + key.userKey + " bin=" + bin.name);
		}

		assertNotNull(received);
		assertEquals(10000, received.length);

		for (int i = 0; i < 10000; i++) {
			if (received[i] != i * i) {
				fail("Mismatch: index=" + i + " expected=" + (i*i) + " received=" + received[i]);
			}
		}
	}

	@Test
	public void serializeList() {
		Key key = new Key(args.namespace, args.set, "seriallistkey");

		// Delete record if it already exists.
		client.delete(null, key);

		ArrayList<String> list = new ArrayList<String>();
		list.add("string1");
		list.add("string2");
		list.add("string3");

		Bin bin = new Bin(binName, (Object)list);

		client.put(null, key, bin);

		Record record = client.get(null, key, bin.name);
		assertRecordFound(key, record);

		List<?> received = null;

		try {
			received = (List<?>) record.getValue(bin.name);
		}
		catch (Exception e) {
			fail("Failed to parse returned value: namespace=" + key.namespace + " set=" + key.setName +
				 " key=" + key.userKey + " bin=" + bin.name);
		}

		assertNotNull(received);
		assertEquals(3, received.size());
		int max = received.size();

		for (int i = 0; i < max; i++) {
			String expected = "string" + (i + 1);
			if (! received.get(i).equals(expected)) {
				Object obj = received.get(i);
				fail("Mismatch: index=" + i + " expected=" + expected + " received=" + obj);
			}
		}
	}

	@Test
	public void serializeComplex() {
		Key key = new Key(args.namespace, args.set, "serialcomplexkey");

		// Delete record if it already exists.
		client.delete(null, key);

		ArrayList<Object> inner = new ArrayList<Object>();
		inner.add("string2");
		inner.add(8);

		HashMap<Object,Object> innerMap = new HashMap<Object,Object>();
		innerMap.put("a", 1);
		innerMap.put(2, "b");
		innerMap.put("list", inner);

		ArrayList<Object> list = new ArrayList<Object>();
		list.add("string1");
		list.add(4);
		list.add(inner);
		list.add(innerMap);

		Bin bin = new Bin("complexbin", new Value.BlobValue(list));

		client.put(null, key, bin);

		Record record = client.get(null, key, bin.name);
		assertRecordFound(key, record);

		Object received = null;

		try {
			received = (List<?>) record.getValue(bin.name);
		}
		catch (Exception e) {
			fail("Failed to parse returned value: namespace=" + key.namespace + " set=" + key.setName +
				 " key=" + key.userKey + " bin=" + bin.name);
		}

		if (received == null || ! received.equals(list)) {
			fail("Data mismatch" + System.lineSeparator() +
				"Expected " + list + System.lineSeparator() +
				"Received " + received);
		}
	}
}

Fixed code:
		throw new AerospikeException("Unsupported type: " + value.getClass().getName());
 * Copyright 2012-2023 Aerospike, Inc.
 * Server particle types.
 * Copyright 2012-2023 Aerospike, Inc.
 * Copyright 2012-2023 Aerospike, Inc.
		throw new AerospikeException("Unsupported type: " + obj.getClass().getName());
		l1.add(Value.get(new GregorianCalendar(2018, 1, 1).getTimeInMillis()));
		l2.add(Value.get(new GregorianCalendar(2018, 1, 2).getTimeInMillis()));
		l3.add(Value.get(new GregorianCalendar(2018, 2, 1).getTimeInMillis()));
		l4.add(Value.get(new GregorianCalendar(2018, 2, 2).getTimeInMillis()));
		l5.add(Value.get(new GregorianCalendar(2018, 2, 5).getTimeInMillis()));
		end.add(Value.get(new GregorianCalendar(2018, 2, 2).getTimeInMillis()));
import com.aerospike.client.Value;
		Bin bin = new Bin(binName, Value.get((Object)null));