Skip to content

Commit d194ad0

Browse files
authored
HADOOP-19079. HttpExceptionUtils to verify that loaded class is really an exception before instantiation (apache#6557)
Security hardening + Adds new interceptAndValidateMessageContains() method in LambdaTestUtils to verify a list of strings can all be found in the toString() value of a raised exception Contributed by PJ Fanning
1 parent 81b0597 commit d194ad0

File tree

5 files changed

+174
-56
lines changed

5 files changed

+174
-56
lines changed

hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/HttpExceptionUtils.java

+13-4
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
import java.io.IOException;
2727
import java.io.InputStream;
2828
import java.io.Writer;
29-
import java.lang.reflect.Constructor;
29+
import java.lang.invoke.MethodHandle;
30+
import java.lang.invoke.MethodHandles;
31+
import java.lang.invoke.MethodType;
3032
import java.net.HttpURLConnection;
3133
import java.util.Collections;
3234
import java.util.LinkedHashMap;
@@ -54,6 +56,10 @@ public class HttpExceptionUtils {
5456

5557
private static final String ENTER = System.getProperty("line.separator");
5658

59+
private static final MethodHandles.Lookup PUBLIC_LOOKUP = MethodHandles.publicLookup();
60+
private static final MethodType EXCEPTION_CONSTRUCTOR_TYPE =
61+
MethodType.methodType(void.class, String.class);
62+
5763
/**
5864
* Creates a HTTP servlet response serializing the exception in it as JSON.
5965
*
@@ -150,9 +156,12 @@ public static void validateResponse(HttpURLConnection conn,
150156
try {
151157
ClassLoader cl = HttpExceptionUtils.class.getClassLoader();
152158
Class klass = cl.loadClass(exClass);
153-
Constructor constr = klass.getConstructor(String.class);
154-
toThrow = (Exception) constr.newInstance(exMsg);
155-
} catch (Exception ex) {
159+
Preconditions.checkState(Exception.class.isAssignableFrom(klass),
160+
"Class [%s] is not a subclass of Exception", klass);
161+
MethodHandle methodHandle = PUBLIC_LOOKUP.findConstructor(
162+
klass, EXCEPTION_CONSTRUCTOR_TYPE);
163+
toThrow = (Exception) methodHandle.invoke(exMsg);
164+
} catch (Throwable t) {
156165
toThrow = new IOException(String.format(
157166
"HTTP status [%d], exception [%s], message [%s], URL [%s]",
158167
conn.getResponseCode(), exClass, exMsg, conn.getURL()));

hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/impl/prefetch/TestBlockCache.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public void testArgChecks() throws Exception {
5454
() -> cache.put(42, null, null, null));
5555

5656

57-
intercept(NullPointerException.class, null,
57+
intercept(NullPointerException.class,
5858
() -> new SingleFilePerBlockCache(null, 2, null));
5959

6060
}

hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/test/LambdaTestUtils.java

+111-6
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,25 @@
1818

1919
package org.apache.hadoop.test;
2020

21-
import org.apache.hadoop.util.Preconditions;
2221
import org.junit.Assert;
2322
import org.slf4j.Logger;
2423
import org.slf4j.LoggerFactory;
2524

2625
import org.apache.hadoop.security.UserGroupInformation;
26+
import org.apache.hadoop.util.Preconditions;
2727
import org.apache.hadoop.util.Time;
2828

2929
import java.io.IOException;
3030
import java.security.PrivilegedExceptionAction;
31+
import java.util.Collection;
3132
import java.util.Optional;
3233
import java.util.concurrent.Callable;
3334
import java.util.concurrent.CancellationException;
3435
import java.util.concurrent.ExecutionException;
3536
import java.util.concurrent.Future;
3637
import java.util.concurrent.TimeUnit;
3738
import java.util.concurrent.TimeoutException;
39+
import java.util.stream.Collectors;
3840

3941
/**
4042
* Class containing methods and associated classes to make the most of Lambda
@@ -476,7 +478,7 @@ public static <T, E extends Throwable> E intercept(
476478
* <i>or a subclass</i>.
477479
* @param contained string which must be in the {@code toString()} value
478480
* of the exception
479-
* @param message any message tho include in exception/log messages
481+
* @param message any message to include in exception/log messages
480482
* @param eval expression to eval
481483
* @param <T> return type of expression
482484
* @param <E> exception class
@@ -528,7 +530,7 @@ public static <E extends Throwable> E intercept(
528530
throws Exception {
529531
return intercept(clazz, contained,
530532
"Expecting " + clazz.getName()
531-
+ (contained != null? (" with text " + contained) : "")
533+
+ (contained != null ? (" with text " + contained) : "")
532534
+ " but got ",
533535
() -> {
534536
eval.call();
@@ -543,7 +545,7 @@ public static <E extends Throwable> E intercept(
543545
* <i>or a subclass</i>.
544546
* @param contained string which must be in the {@code toString()} value
545547
* of the exception
546-
* @param message any message tho include in exception/log messages
548+
* @param message any message to include in exception/log messages
547549
* @param eval expression to eval
548550
* @param <E> exception class
549551
* @return the caught exception if it was of the expected type
@@ -563,6 +565,105 @@ public static <E extends Throwable> E intercept(
563565
});
564566
}
565567

568+
/**
569+
* Intercept an exception; throw an {@code AssertionError} if one not raised.
570+
* The caught exception is rethrown if it is of the wrong class or
571+
* does not contain the text defined in {@code contained}.
572+
* <p>
573+
* Example: expect deleting a nonexistent file to raise a
574+
* {@code FileNotFoundException} with the {@code toString()} value
575+
* containing the text {@code "missing"}.
576+
* <pre>
577+
* FileNotFoundException ioe = interceptAndValidateMessageContains(
578+
* FileNotFoundException.class,
579+
* "missing",
580+
* "path should not be found",
581+
* () -> {
582+
* filesystem.delete(new Path("/missing"), false);
583+
* });
584+
* </pre>
585+
*
586+
* @param clazz class of exception; the raised exception must be this class
587+
* <i>or a subclass</i>.
588+
* @param contains strings which must be in the {@code toString()} value
589+
* of the exception (order does not matter)
590+
* @param eval expression to eval
591+
* @param <T> return type of expression
592+
* @param <E> exception class
593+
* @return the caught exception if it was of the expected type and contents
594+
* @throws Exception any other exception raised
595+
* @throws AssertionError if the evaluation call didn't raise an exception.
596+
* The error includes the {@code toString()} value of the result, if this
597+
* can be determined.
598+
* @see GenericTestUtils#assertExceptionContains(String, Throwable)
599+
*/
600+
public static <T, E extends Throwable> E interceptAndValidateMessageContains(
601+
Class<E> clazz,
602+
Collection<String> contains,
603+
VoidCallable eval)
604+
throws Exception {
605+
String message = "Expecting " + clazz.getName()
606+
+ (contains.isEmpty() ? "" : (" with text values " + toString(contains)))
607+
+ " but got ";
608+
return interceptAndValidateMessageContains(clazz, contains, message, eval);
609+
}
610+
611+
/**
612+
* Intercept an exception; throw an {@code AssertionError} if one not raised.
613+
* The caught exception is rethrown if it is of the wrong class or
614+
* does not contain the text defined in {@code contained}.
615+
* <p>
616+
* Example: expect deleting a nonexistent file to raise a
617+
* {@code FileNotFoundException} with the {@code toString()} value
618+
* containing the text {@code "missing"}.
619+
* <pre>
620+
* FileNotFoundException ioe = interceptAndValidateMessageContains(
621+
* FileNotFoundException.class,
622+
* "missing",
623+
* "path should not be found",
624+
* () -> {
625+
* filesystem.delete(new Path("/missing"), false);
626+
* });
627+
* </pre>
628+
*
629+
* @param clazz class of exception; the raised exception must be this class
630+
* <i>or a subclass</i>.
631+
* @param contains strings which must be in the {@code toString()} value
632+
* of the exception (order does not matter)
633+
* @param message any message to include in exception/log messages
634+
* @param eval expression to eval
635+
* @param <T> return type of expression
636+
* @param <E> exception class
637+
* @return the caught exception if it was of the expected type and contents
638+
* @throws Exception any other exception raised
639+
* @throws AssertionError if the evaluation call didn't raise an exception.
640+
* The error includes the {@code toString()} value of the result, if this
641+
* can be determined.
642+
* @see GenericTestUtils#assertExceptionContains(String, Throwable)
643+
*/
644+
public static <T, E extends Throwable> E interceptAndValidateMessageContains(
645+
Class<E> clazz,
646+
Collection<String> contains,
647+
String message,
648+
VoidCallable eval)
649+
throws Exception {
650+
E ex;
651+
try {
652+
eval.call();
653+
throw new AssertionError(message);
654+
} catch (Throwable e) {
655+
if (!clazz.isAssignableFrom(e.getClass())) {
656+
throw e;
657+
} else {
658+
ex = (E) e;
659+
}
660+
}
661+
for (String contained : contains) {
662+
GenericTestUtils.assertExceptionContains(contained, ex, message);
663+
}
664+
return ex;
665+
}
666+
566667
/**
567668
* Robust string converter for exception messages; if the {@code toString()}
568669
* method throws an exception then that exception is caught and logged,
@@ -607,7 +708,6 @@ public static <T> void assertOptionalEquals(String message,
607708
* Assert that an optional value matches an expected one;
608709
* checks include null and empty on the actual value.
609710
* @param message message text
610-
* @param expected expected value
611711
* @param actual actual optional value
612712
* @param <T> type
613713
*/
@@ -641,7 +741,6 @@ public static <T> T eval(Callable<T> closure) {
641741
* Invoke a callable; wrap all checked exceptions with an
642742
* AssertionError.
643743
* @param closure closure to execute
644-
* @return the value of the closure
645744
* @throws AssertionError if the operation raised an IOE or
646745
* other checked exception.
647746
*/
@@ -823,6 +922,11 @@ public static <E extends Throwable> E verifyCause(
823922
}
824923
}
825924

925+
private static String toString(Collection<String> strings) {
926+
return strings.stream()
927+
.collect(Collectors.joining(",", "[", "]"));
928+
}
929+
826930
/**
827931
* Returns {@code TimeoutException} on a timeout. If
828932
* there was a inner class passed in, includes it as the
@@ -1037,3 +1141,4 @@ public Void run() throws Exception {
10371141
}
10381142
}
10391143
}
1144+

hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestHttpExceptionUtils.java

+49-40
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.apache.hadoop.util;
1919

2020
import com.fasterxml.jackson.databind.ObjectMapper;
21+
import org.apache.hadoop.test.LambdaTestUtils;
2122
import org.junit.Assert;
2223
import org.junit.Test;
2324
import org.mockito.Mockito;
@@ -31,6 +32,7 @@
3132
import java.io.PrintWriter;
3233
import java.io.StringWriter;
3334
import java.net.HttpURLConnection;
35+
import java.nio.charset.StandardCharsets;
3436
import java.util.Arrays;
3537
import java.util.HashMap;
3638
import java.util.Map;
@@ -82,40 +84,34 @@ public void testCreateJerseyException() throws IOException {
8284
@Test
8385
public void testValidateResponseOK() throws IOException {
8486
HttpURLConnection conn = Mockito.mock(HttpURLConnection.class);
85-
Mockito.when(conn.getResponseCode()).thenReturn(
86-
HttpURLConnection.HTTP_CREATED);
87+
Mockito.when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_CREATED);
8788
HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_CREATED);
8889
}
8990

90-
@Test(expected = IOException.class)
91-
public void testValidateResponseFailNoErrorMessage() throws IOException {
91+
@Test
92+
public void testValidateResponseFailNoErrorMessage() throws Exception {
9293
HttpURLConnection conn = Mockito.mock(HttpURLConnection.class);
93-
Mockito.when(conn.getResponseCode()).thenReturn(
94-
HttpURLConnection.HTTP_BAD_REQUEST);
95-
HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_CREATED);
94+
Mockito.when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_BAD_REQUEST);
95+
LambdaTestUtils.intercept(IOException.class,
96+
() -> HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_CREATED));
9697
}
9798

9899
@Test
99-
public void testValidateResponseNonJsonErrorMessage() throws IOException {
100+
public void testValidateResponseNonJsonErrorMessage() throws Exception {
100101
String msg = "stream";
101-
InputStream is = new ByteArrayInputStream(msg.getBytes());
102+
InputStream is = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8));
102103
HttpURLConnection conn = Mockito.mock(HttpURLConnection.class);
103104
Mockito.when(conn.getErrorStream()).thenReturn(is);
104105
Mockito.when(conn.getResponseMessage()).thenReturn("msg");
105-
Mockito.when(conn.getResponseCode()).thenReturn(
106-
HttpURLConnection.HTTP_BAD_REQUEST);
107-
try {
108-
HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_CREATED);
109-
Assert.fail();
110-
} catch (IOException ex) {
111-
Assert.assertTrue(ex.getMessage().contains("msg"));
112-
Assert.assertTrue(ex.getMessage().contains("" +
113-
HttpURLConnection.HTTP_BAD_REQUEST));
114-
}
106+
Mockito.when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_BAD_REQUEST);
107+
LambdaTestUtils.interceptAndValidateMessageContains(IOException.class,
108+
Arrays.asList(Integer.toString(HttpURLConnection.HTTP_BAD_REQUEST), "msg",
109+
"com.fasterxml.jackson.core.JsonParseException"),
110+
() -> HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_CREATED));
115111
}
116112

117113
@Test
118-
public void testValidateResponseJsonErrorKnownException() throws IOException {
114+
public void testValidateResponseJsonErrorKnownException() throws Exception {
119115
Map<String, Object> json = new HashMap<String, Object>();
120116
json.put(HttpExceptionUtils.ERROR_EXCEPTION_JSON, IllegalStateException.class.getSimpleName());
121117
json.put(HttpExceptionUtils.ERROR_CLASSNAME_JSON, IllegalStateException.class.getName());
@@ -124,23 +120,19 @@ public void testValidateResponseJsonErrorKnownException() throws IOException {
124120
response.put(HttpExceptionUtils.ERROR_JSON, json);
125121
ObjectMapper jsonMapper = new ObjectMapper();
126122
String msg = jsonMapper.writeValueAsString(response);
127-
InputStream is = new ByteArrayInputStream(msg.getBytes());
123+
InputStream is = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8));
128124
HttpURLConnection conn = Mockito.mock(HttpURLConnection.class);
129125
Mockito.when(conn.getErrorStream()).thenReturn(is);
130126
Mockito.when(conn.getResponseMessage()).thenReturn("msg");
131-
Mockito.when(conn.getResponseCode()).thenReturn(
132-
HttpURLConnection.HTTP_BAD_REQUEST);
133-
try {
134-
HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_CREATED);
135-
Assert.fail();
136-
} catch (IllegalStateException ex) {
137-
Assert.assertEquals("EX", ex.getMessage());
138-
}
127+
Mockito.when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_BAD_REQUEST);
128+
LambdaTestUtils.intercept(IllegalStateException.class,
129+
"EX",
130+
() -> HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_CREATED));
139131
}
140132

141133
@Test
142134
public void testValidateResponseJsonErrorUnknownException()
143-
throws IOException {
135+
throws Exception {
144136
Map<String, Object> json = new HashMap<String, Object>();
145137
json.put(HttpExceptionUtils.ERROR_EXCEPTION_JSON, "FooException");
146138
json.put(HttpExceptionUtils.ERROR_CLASSNAME_JSON, "foo.FooException");
@@ -149,19 +141,36 @@ public void testValidateResponseJsonErrorUnknownException()
149141
response.put(HttpExceptionUtils.ERROR_JSON, json);
150142
ObjectMapper jsonMapper = new ObjectMapper();
151143
String msg = jsonMapper.writeValueAsString(response);
152-
InputStream is = new ByteArrayInputStream(msg.getBytes());
144+
InputStream is = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8));
153145
HttpURLConnection conn = Mockito.mock(HttpURLConnection.class);
154146
Mockito.when(conn.getErrorStream()).thenReturn(is);
155147
Mockito.when(conn.getResponseMessage()).thenReturn("msg");
156-
Mockito.when(conn.getResponseCode()).thenReturn(
157-
HttpURLConnection.HTTP_BAD_REQUEST);
158-
try {
159-
HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_CREATED);
160-
Assert.fail();
161-
} catch (IOException ex) {
162-
Assert.assertTrue(ex.getMessage().contains("EX"));
163-
Assert.assertTrue(ex.getMessage().contains("foo.FooException"));
164-
}
148+
Mockito.when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_BAD_REQUEST);
149+
LambdaTestUtils.interceptAndValidateMessageContains(IOException.class,
150+
Arrays.asList(Integer.toString(HttpURLConnection.HTTP_BAD_REQUEST),
151+
"foo.FooException", "EX"),
152+
() -> HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_CREATED));
165153
}
166154

155+
@Test
156+
public void testValidateResponseJsonErrorNonException() throws Exception {
157+
Map<String, Object> json = new HashMap<String, Object>();
158+
json.put(HttpExceptionUtils.ERROR_EXCEPTION_JSON, "invalid");
159+
// test case where the exception classname is not a valid exception class
160+
json.put(HttpExceptionUtils.ERROR_CLASSNAME_JSON, String.class.getName());
161+
json.put(HttpExceptionUtils.ERROR_MESSAGE_JSON, "EX");
162+
Map<String, Object> response = new HashMap<String, Object>();
163+
response.put(HttpExceptionUtils.ERROR_JSON, json);
164+
ObjectMapper jsonMapper = new ObjectMapper();
165+
String msg = jsonMapper.writeValueAsString(response);
166+
InputStream is = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8));
167+
HttpURLConnection conn = Mockito.mock(HttpURLConnection.class);
168+
Mockito.when(conn.getErrorStream()).thenReturn(is);
169+
Mockito.when(conn.getResponseMessage()).thenReturn("msg");
170+
Mockito.when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_BAD_REQUEST);
171+
LambdaTestUtils.interceptAndValidateMessageContains(IOException.class,
172+
Arrays.asList(Integer.toString(HttpURLConnection.HTTP_BAD_REQUEST),
173+
"java.lang.String", "EX"),
174+
() -> HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_CREATED));
175+
}
167176
}

0 commit comments

Comments
 (0)