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.time;
19
20 import static org.junit.jupiter.api.Assertions.assertThrows;
21
22 import java.io.ByteArrayInputStream;
23 import java.io.ByteArrayOutputStream;
24 import java.io.IOException;
25 import java.io.InvalidObjectException;
26 import java.io.ObjectInputStream;
27 import java.io.ObjectOutputStream;
28 import java.io.ObjectStreamClass;
29 import java.io.Serializable;
30 import java.util.Locale;
31 import java.util.TimeZone;
32
33 import org.junit.jupiter.api.Test;
34
35 /**
36 * Tests that a deserialized {@link FastDateParser} rejects null {@code pattern} and null {@code timeZone} fields.
37 *
38 * <p>
39 * The two null-checks were introduced in {@link FastDateParser#readObject(ObjectInputStream)}:
40 * </p>
41 * <ul>
42 * <li>{@code if (pattern == null) throw new InvalidObjectException("pattern null");}</li>
43 * <li>{@code if (timeZone == null) throw new InvalidObjectException("timeZone null");}</li>
44 * </ul>
45 *
46 * <p>
47 * Because neither null value can reach {@code readObject} through the normal public API, the tests forge a malicious serialization stream. A
48 * {@link FastDateParserForge} helper carries the same non-transient field set as {@link FastDateParser} (same names, same types, same {@code serialVersionUID})
49 * but allows null values. A custom {@link ObjectOutputStream} sub-class rewrites the class-descriptor name to {@code FastDateParser} so the stream is accepted
50 * by {@link ObjectInputStream} as a {@link FastDateParser} payload; {@code defaultReadObject} then assigns the forged values to the actual
51 * {@link FastDateParser} fields, triggering the null checks.
52 * </p>
53 */
54 class FastDateParserReadObjectTest {
55
56 /**
57 * Forge carrier: same non-transient fields as {@link FastDateParser}, in the same alphabetical order used by Java default serialization ({@code century},
58 * {@code locale}, {@code pattern}, {@code startYear}, {@code timeZone}), with the same {@code serialVersionUID}. Allows null for {@code pattern} and
59 * {@code timeZone}.
60 */
61 private static final class FastDateParserForge implements Serializable {
62
63 /** Must match {@link FastDateParser#serialVersionUID}. */
64 private static final long serialVersionUID = 3L;
65 // Fields must match FastDateParser's non-transient fields by name and type.
66 private final int century;
67 private final Locale locale;
68 private final String pattern;
69 private final int startYear;
70 private final TimeZone timeZone;
71
72 FastDateParserForge(final String pattern, final TimeZone timeZone, final Locale locale, final int century, final int startYear) {
73 this.pattern = pattern;
74 this.timeZone = timeZone;
75 this.locale = locale;
76 this.century = century;
77 this.startYear = startYear;
78 }
79 }
80
81 /**
82 * Deserializes {@code bytes} and returns the resulting object.
83 *
84 * @param bytes serialized form
85 * @return The deserialized object
86 * @throws IOException Thrown if an I/O error occurs.
87 * @throws ClassNotFoundException Thrown if the class of the serialized object cannot be found.
88 */
89 private static Object deserialize(final byte[] bytes) throws IOException, ClassNotFoundException {
90 try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
91 return ois.readObject();
92 }
93 }
94
95 /**
96 * Serializes a {@link FastDateParserForge} but rewrites the class descriptor so that the resulting stream is treated as a {@link FastDateParser} during
97 * deserialization.
98 *
99 * @param forge The forge instance to serialize
100 * @return A byte array whose class descriptor names {@link FastDateParser}
101 * @throws IOException Thrown if an I/O error occurs
102 */
103 private static byte[] forgeStream(final FastDateParserForge forge) throws IOException {
104 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
105 try (ObjectOutputStream oos = new ObjectOutputStream(baos) {
106
107 @Override
108 protected void writeClassDescriptor(final ObjectStreamClass desc) throws IOException {
109 if (desc.getName().equals(FastDateParserForge.class.getName())) {
110 // Spoof the class descriptor so the stream deserializes as FastDateParser.
111 super.writeClassDescriptor(ObjectStreamClass.lookup(FastDateParser.class));
112 } else {
113 super.writeClassDescriptor(desc);
114 }
115 }
116 }) {
117 oos.writeObject(forge);
118 }
119 return baos.toByteArray();
120 }
121
122 /**
123 * Tests that a forged stream whose {@code pattern} field is {@code null} is rejected with {@link InvalidObjectException}.
124 */
125 @Test
126 void testNullPatternRejected() throws IOException {
127 final FastDateParserForge forge = new FastDateParserForge(null, // pattern = null (the evil value under test)
128 TimeZone.getTimeZone("GMT"), Locale.US, 1900, 0);
129 final byte[] forgedBytes = forgeStream(forge);
130 assertThrows(InvalidObjectException.class, () -> deserialize(forgedBytes), "A null pattern must be rejected with InvalidObjectException");
131 }
132
133 /**
134 * Tests that a forged stream whose {@code timeZone} field is {@code null} is rejected with {@link InvalidObjectException}.
135 */
136 @Test
137 void testNullTimeZoneRejected() throws IOException {
138 final FastDateParserForge forge = new FastDateParserForge("yyyy-MM-dd", null, // timeZone = null (the evil value under test)
139 Locale.US, 1900, 0);
140 final byte[] forgedBytes = forgeStream(forge);
141 assertThrows(InvalidObjectException.class, () -> deserialize(forgedBytes), "A null timeZone must be rejected with InvalidObjectException");
142 }
143 }