001/* 002 * Logback: the reliable, generic, fast and flexible logging framework. 003 * Copyright (C) 1999-2026, QOS.ch. All rights reserved. 004 * 005 * This program and the accompanying materials are dual-licensed under 006 * either the terms of the Eclipse Public License v2.0 as published by 007 * the Eclipse Foundation 008 * 009 * or (per the licensee's choosing) 010 * 011 * under the terms of the GNU Lesser General Public License version 2.1 012 * as published by the Free Software Foundation. 013 */ 014package ch.qos.logback.core.net; 015 016import ch.qos.logback.core.Context; 017import ch.qos.logback.core.spi.ContextAwareImpl; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.InvalidClassException; 022import java.io.ObjectInputFilter; 023import java.io.ObjectInputStream; 024import java.io.ObjectStreamClass; 025import java.util.ArrayList; 026import java.util.Arrays; 027import java.util.HashMap; 028import java.util.List; 029 030/** 031 * HardenedObjectInputStream restricts the set of classes that can be 032 * deserialized to a set of explicitly whitelisted classes. This prevents 033 * certain type of attacks from being successful. 034 * 035 * <p> 036 * It is assumed that classes in the "java.lang" and "java.util" packages are 037 * always authorized. 038 * </p> 039 * 040 * @author Ceki Gülcü 041 * @since 1.2.0 042 */ 043public class HardenedObjectInputStream extends ObjectInputStream { 044 045 final private List<String> whitelistedClassNames; 046 final private static String[] JAVA_CLASSES = new String[] { "java.lang.Boolean", 047 "java.lang.Byte", 048 "java.lang.Character", 049 "java.lang.Double", 050 "java.lang.Float", 051 "java.lang.Integer", 052 "java.lang.Long", 053 "java.lang.Number", 054 "java.lang.Short", 055 "java.lang.String", 056 "java.lang.Throwable", 057 "java.util.ArrayList", 058 "java.util.Collections$EmptyMap", 059 "java.util.Collections$UnmodifiableMap", 060 "java.util.concurrent.CopyOnWriteArrayList", 061 "java.util.HashMap", 062 "java.util.LinkedHashMap" 063 064 //"java.util.HashSet", 065 //"java.util.Hashtable", 066 067 // PASS 068 //"java.util.LinkedHashMap", 069 //"java.util.LinkedHashSet", 070 //"java.util.LinkedList", 071 //"java.util.Stack", 072 //"java.util.TreeMap", 073 //"java.util.TreeSet", 074 //"java.util.Vector" 075 }; 076 final private static int DEPTH_LIMIT = 16; 077 final private static int ARRAY_LIMIT = 10000; 078 final private static int ERROR_COUNT_LIMIT = 10; 079 080 final private ContextAwareImpl contextAware; 081 final private HashMap<String, Integer> errorMap = new HashMap<>(); 082 083 public HardenedObjectInputStream(Context context, InputStream in, String[] whitelistStrings) throws IOException { 084 this(context, in, Arrays.asList(whitelistStrings)); 085 } 086 public HardenedObjectInputStream(Context context, InputStream in, List<String> whitelist) throws IOException { 087 super(in); 088 089 if(context != null) 090 this.contextAware = new ContextAwareImpl(context, this); 091 else 092 this.contextAware = null; 093 094 this.initObjectFilter(); 095 this.whitelistedClassNames = new ArrayList<String>(); 096 this.whitelistedClassNames.addAll(whitelist); 097 } 098 099 100 private void initObjectFilter() { 101 this.setObjectInputFilter(ObjectInputFilter.Config.createFilter( 102 "maxarray=" + ARRAY_LIMIT + ";maxdepth=" + DEPTH_LIMIT + ";" 103 )); 104 } 105 106 @Override 107 protected Class<?> resolveClass(ObjectStreamClass anObjectStreamClass) throws IOException, ClassNotFoundException { 108 109 String incomingClassName = anObjectStreamClass.getName(); 110 111 if (!isWhitelisted(incomingClassName)) { 112 throw new InvalidClassException("Unauthorized deserialization attempt", anObjectStreamClass.getName()); 113 } 114 115 return super.resolveClass(anObjectStreamClass); 116 } 117 118 /** 119 * There is no reason to have proxy classes in logback deserialization, so we just 120 * throw an exception here to prevent any potential bypasses that could be achieved 121 * through proxy classes. 122 * 123 * @param interfaces the list of interface names that were 124 * deserialized in the proxy class descriptor 125 * @return 126 * @throws IOException 127 * @throws ClassNotFoundException 128 * @since 1.5.34 129 */ 130 @Override 131 protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException { 132 throw new InvalidClassException("Unauthorized deserialization attempt ", Arrays.toString(interfaces)); 133 } 134 135 private boolean isWhitelisted(String incomingClassName) { 136 for (String javaClass : JAVA_CLASSES) { 137 if (incomingClassName.equals(javaClass)) 138 return true; 139 } 140 for (String whiteListed : whitelistedClassNames) { 141 if (incomingClassName.equals(whiteListed)) 142 return true; 143 } 144 145 146 int errorCount = errorMap.getOrDefault(incomingClassName, 0) + 1; 147 errorMap.put(incomingClassName, errorCount); 148 if(contextAware != null && errorCount < ERROR_COUNT_LIMIT) { 149 contextAware.addError("Unauthorized deserialization attempt for class [" + incomingClassName+"]"); 150 contextAware.addError(("If you deem the class to be legitimate, please contact the project maintainers to have it whitelisted.")); 151 } 152 153 return false; 154 } 155 156 protected void addToWhitelist(List<String> additionalAuthorizedClasses) { 157 whitelistedClassNames.addAll(additionalAuthorizedClasses); 158 } 159}