Skip to content

Commit a559414

Browse files
author
Vladimir Kotal
committed
restrict XML decoding of History object
1 parent 7f142e0 commit a559414

File tree

4 files changed

+137
-3
lines changed

4 files changed

+137
-3
lines changed

opengrok-indexer/src/main/java/org/opengrok/indexer/history/FileHistoryCache.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@
3232
import java.io.BufferedOutputStream;
3333
import java.io.BufferedReader;
3434
import java.io.BufferedWriter;
35+
import java.io.ByteArrayInputStream;
3536
import java.io.File;
3637
import java.io.FileInputStream;
3738
import java.io.FileOutputStream;
3839
import java.io.FileReader;
3940
import java.io.IOException;
41+
import java.io.InputStream;
4042
import java.io.OutputStreamWriter;
4143
import java.io.Writer;
4244
import java.nio.file.NoSuchFileException;
@@ -236,13 +238,24 @@ private static File getCachedFile(File file) throws HistoryException,
236238
return new File(TandemPath.join(sb.toString(), ".gz"));
237239
}
238240

241+
private static XMLDecoder getDecoder(InputStream in) {
242+
return new XMLDecoder(in, null, null, new HistoryClassLoader());
243+
}
244+
245+
// for testing
246+
static History readCache(String xmlconfig) {
247+
final ByteArrayInputStream in = new ByteArrayInputStream(xmlconfig.getBytes());
248+
try (XMLDecoder d = getDecoder(in)) {
249+
return (History) d.readObject();
250+
}
251+
}
252+
239253
/**
240254
* Read history from a file.
241255
*/
242-
private static History readCache(File file) throws IOException {
256+
static History readCache(File file) throws IOException {
243257
try (FileInputStream in = new FileInputStream(file);
244-
XMLDecoder d = new XMLDecoder(new GZIPInputStream(
245-
new BufferedInputStream(in)))) {
258+
XMLDecoder d = getDecoder(new GZIPInputStream(new BufferedInputStream(in)))) {
246259
return (History) d.readObject();
247260
}
248261
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* CDDL HEADER START
3+
*
4+
* The contents of this file are subject to the terms of the
5+
* Common Development and Distribution License (the "License").
6+
* You may not use this file except in compliance with the License.
7+
*
8+
* See LICENSE.txt included in this distribution for the specific
9+
* language governing permissions and limitations under the License.
10+
*
11+
* When distributing Covered Code, include this CDDL HEADER in each
12+
* file and include the License file at LICENSE.txt.
13+
* If applicable, add the following below this CDDL HEADER, with the
14+
* fields enclosed by brackets "[]" replaced with your own identifying
15+
* information: Portions Copyright [yyyy] [name of copyright owner]
16+
*
17+
* CDDL HEADER END
18+
*/
19+
20+
/*
21+
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
22+
*/
23+
package org.opengrok.indexer.history;
24+
25+
import java.beans.XMLDecoder;
26+
import java.util.ArrayList;
27+
import java.util.Collections;
28+
import java.util.Date;
29+
import java.util.HashMap;
30+
import java.util.Set;
31+
import java.util.TreeSet;
32+
import java.util.stream.Collectors;
33+
34+
/**
35+
* Temporary hack to prevent {@link XMLDecoder} to deserialize other than allowed classes. This tries to prevent
36+
* calling of methods on {@link ProcessBuilder} or {@link Runtime} (or similar) which could be used for code execution.
37+
*/
38+
public class HistoryClassLoader extends ClassLoader {
39+
40+
private static final Set<String> allowedClasses = Set.of(
41+
ArrayList.class,
42+
Collections.class,
43+
Date.class,
44+
HashMap.class,
45+
History.class,
46+
HistoryEntry.class,
47+
RepositoryInfo.class,
48+
String.class,
49+
TreeSet.class,
50+
XMLDecoder.class
51+
).stream().map(Class::getName).collect(Collectors.toSet());
52+
53+
@Override
54+
public Class<?> loadClass(final String name) throws ClassNotFoundException {
55+
if (!allowedClasses.contains(name)) {
56+
throw new IllegalAccessError(name + " is not allowed to be used in configuration");
57+
}
58+
59+
return getClass().getClassLoader().loadClass(name);
60+
}
61+
}

opengrok-indexer/src/test/java/org/opengrok/indexer/history/FileHistoryCacheTest.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@
2828
import static org.junit.jupiter.api.Assertions.assertFalse;
2929
import static org.junit.jupiter.api.Assertions.assertNotNull;
3030
import static org.junit.jupiter.api.Assertions.assertNull;
31+
import static org.junit.jupiter.api.Assertions.assertThrows;
3132
import static org.junit.jupiter.api.Assertions.assertTrue;
3233
import static org.opengrok.indexer.condition.RepositoryInstalled.Type.MERCURIAL;
3334
import static org.opengrok.indexer.condition.RepositoryInstalled.Type.SCCS;
3435
import static org.opengrok.indexer.condition.RepositoryInstalled.Type.SUBVERSION;
3536
import static org.opengrok.indexer.history.MercurialRepositoryTest.runHgCommand;
3637

3738
import java.io.File;
39+
import java.io.IOException;
3840
import java.nio.file.Paths;
3941
import java.util.Date;
4042
import java.util.Iterator;
@@ -47,6 +49,8 @@
4749
import org.junit.jupiter.api.Test;
4850
import org.junit.jupiter.api.condition.EnabledOnOs;
4951
import org.junit.jupiter.api.condition.OS;
52+
import org.junit.jupiter.params.ParameterizedTest;
53+
import org.junit.jupiter.params.provider.ValueSource;
5054
import org.opengrok.indexer.condition.EnabledForRepository;
5155
import org.opengrok.indexer.configuration.Filter;
5256
import org.opengrok.indexer.configuration.IgnoredNames;
@@ -827,4 +831,60 @@ public void testStoreAndTryToGetIgnored() throws Exception {
827831
retrievedHistory = cache.get(makefile, repo, true);
828832
assertNotNull(retrievedHistory, "history for Makefile should not be null");
829833
}
834+
835+
@ParameterizedTest
836+
@ValueSource(strings = {
837+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
838+
"<java version=\"11.0.8\" class=\"java.beans.XMLDecoder\">\n" +
839+
" <object class=\"java.lang.Runtime\" method=\"getRuntime\">\n" +
840+
" <void method=\"exec\">\n" +
841+
" <array class=\"java.lang.String\" length=\"2\">\n" +
842+
" <void index=\"0\">\n" +
843+
" <string>/usr/bin/nc</string>\n" +
844+
" </void>\n" +
845+
" <void index=\"1\">\n" +
846+
" <string>-l</string>\n" +
847+
" </void>\n" +
848+
" </array>\n" +
849+
" </void>\n" +
850+
" </object>\n" +
851+
"</java>",
852+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
853+
"<java version=\"11.0.8\" class=\"java.beans.XMLDecoder\">\n" +
854+
" <object class=\"java.lang.ProcessBuilder\">\n" +
855+
" <array class=\"java.lang.String\" length=\"1\" >\n" +
856+
" <void index=\"0\"> \n" +
857+
" <string>/usr/bin/curl https://oracle.com</string>\n" +
858+
" </void>\n" +
859+
" </array>\n" +
860+
" <void method=\"start\"/>\n" +
861+
" </object>\n" +
862+
"</java>",
863+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
864+
"<java version=\"11.0.8\" class=\"java.beans.XMLDecoder\">\n" +
865+
" <object class = \"java.io.FileOutputStream\"> \n" +
866+
" <string>opengrok_test.txt</string>\n" +
867+
" <method name = \"write\">\n" +
868+
" <array class=\"byte\" length=\"3\">\n" +
869+
" <void index=\"0\"><byte>96</byte></void>\n" +
870+
" <void index=\"1\"><byte>96</byte></void>\n" +
871+
" <void index=\"2\"><byte>96</byte></void>\n" +
872+
" </array>\n" +
873+
" </method>\n" +
874+
" <method name=\"close\"/>\n" +
875+
" </object>\n" +
876+
"</java>"
877+
})
878+
void testDeserializationOfNotWhiteListedClassThrowsError(final String exploit) {
879+
assertThrows(IllegalAccessError.class, () -> FileHistoryCache.readCache(exploit));
880+
}
881+
882+
@Test
883+
void testReadCacheValid() throws IOException {
884+
File testFile = new File(FileHistoryCacheTest.class.getClassLoader().
885+
getResource("history/FileHistoryCache.java.gz").getFile());
886+
History history = FileHistoryCache.readCache(testFile);
887+
assertNotNull(history);
888+
assertEquals(30, history.getHistoryEntries().size());
889+
}
830890
}
Binary file not shown.

0 commit comments

Comments
 (0)