Skip to content

Commit a1152c3

Browse files
committed
A serialized Range can't store a bad cached hashCode.
1 parent d1e7111 commit a1152c3

2 files changed

Lines changed: 93 additions & 1 deletion

File tree

src/main/java/org/apache/commons/lang3/Range.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
*/
1717
package org.apache.commons.lang3;
1818

19+
import java.io.IOException;
20+
import java.io.InvalidObjectException;
21+
import java.io.ObjectInputStream;
1922
import java.io.Serializable;
2023
import java.util.Comparator;
2124
import java.util.Objects;
@@ -235,7 +238,7 @@ public static <T> Range<T> of(final T fromInclusive, final T toInclusive, final
235238
this.minimum = element2;
236239
this.maximum = element1;
237240
}
238-
this.hashCode = Objects.hash(minimum, maximum);
241+
this.hashCode = hash(minimum, maximum);
239242
}
240243

241244
/**
@@ -379,6 +382,10 @@ public T getMinimum() {
379382
return minimum;
380383
}
381384

385+
private int hash(final T value1, final T value2) {
386+
return Objects.hash(minimum, maximum);
387+
}
388+
382389
/**
383390
* Gets a suitable hash code for the range.
384391
*
@@ -527,6 +534,15 @@ public boolean isStartedBy(final T element) {
527534
return comparator.compare(element, minimum) == 0;
528535
}
529536

537+
private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
538+
in.defaultReadObject();
539+
// Reject streams whose cached hashCode does not match the canonical hash of the deserialized minimum/maximum: a crafted stream cannot supply a forged
540+
// value.
541+
if (hashCode != hash(minimum, maximum)) {
542+
throw new InvalidObjectException("Range hashCode does not match minimum/maximum.");
543+
}
544+
}
545+
530546
/**
531547
* Gets the range as a {@link String}.
532548
*
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.commons.lang3;
19+
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
22+
import static org.junit.jupiter.api.Assertions.assertThrows;
23+
import static org.junit.jupiter.api.Assertions.fail;
24+
25+
import java.io.InvalidObjectException;
26+
27+
import org.apache.commons.lang3.reflect.FieldUtils;
28+
import org.junit.jupiter.api.Test;
29+
30+
/**
31+
* Tests that a serialized Range can't store a bad cached hashCode.
32+
*/
33+
class RangeReadObjectTest {
34+
35+
private static byte[] intToBytes(final int v) {
36+
return new byte[] { (byte) (v >>> 24), (byte) (v >>> 16), (byte) (v >>> 8), (byte) v };
37+
}
38+
39+
private static byte[] replaceLastInt(final byte[] src, final int from, final int to) {
40+
final byte[] fromB = intToBytes(from);
41+
final byte[] toB = intToBytes(to);
42+
final byte[] out = src.clone();
43+
for (int i = out.length - 4; i >= 0; i--) {
44+
if (out[i] == fromB[0] && out[i + 1] == fromB[1] && out[i + 2] == fromB[2] && out[i + 3] == fromB[3]) {
45+
out[i] = toB[0];
46+
out[i + 1] = toB[1];
47+
out[i + 2] = toB[2];
48+
out[i + 3] = toB[3];
49+
return out;
50+
}
51+
}
52+
fail("No legitimate int in stream, serialization must keep hashCode in default field set");
53+
return null;
54+
}
55+
56+
@Test
57+
void testBadHashCodeRejected() throws Exception {
58+
final Range<Integer> range = Range.of(1, 100);
59+
final byte[] bytes = SerializationUtils.serialize(range);
60+
// Locate the legitimate hashCode int in the serialized stream and overwrite it.
61+
final int hashCode = (Integer) FieldUtils.readDeclaredField(range, "hashCode", true);
62+
final byte[] edited = replaceLastInt(bytes, hashCode, 0xDEADBEEF);
63+
final SerializationException ex = assertThrows(SerializationException.class, () -> SerializationUtils.deserialize(edited),
64+
"Bad hashCode in stream must be rejected with InvalidObjectException");
65+
assertInstanceOf(InvalidObjectException.class, ex.getCause());
66+
assertEquals("java.io.InvalidObjectException: Range hashCode does not match minimum/maximum.", ex.getMessage());
67+
}
68+
69+
@Test
70+
void testRoundTripPreservesCorrectHashCode() throws Exception {
71+
final Range<String> range = Range.of("apple", "mango");
72+
final Range<String> roundtrip = SerializationUtils.roundtrip(range);
73+
assertEquals(range.hashCode(), roundtrip.hashCode(), "Round-trip serialization must preserve the correct hashCode");
74+
assertEquals(range, roundtrip);
75+
}
76+
}

0 commit comments

Comments
 (0)