View Javadoc
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 }