001/* 002 * jPOS Project [http://jpos.org] 003 * Copyright (C) 2000-2026 jPOS Software SRL 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jpos.util; 020 021import java.io.*; 022import java.util.HashMap; 023import java.util.Map; 024import java.util.Set; 025 026/** 027 * Java-serialization helpers with deserialization filters that reject 028 * known gadget-chain classes and enforce a depth limit. 029 */ 030public class Serializer { 031 /** Utility class; instances carry no state. */ 032 public Serializer() {} 033 private static final int MAX_DEPTH = 32; 034 035 private static final Set<String> REJECTED_CLASSES = Set.of( 036 "org.apache.commons.collections.functors.InvokerTransformer", 037 "org.apache.commons.collections.functors.InstantiateTransformer", 038 "org.apache.commons.collections4.functors.InvokerTransformer", 039 "org.apache.commons.collections4.functors.InstantiateTransformer", 040 "org.apache.xalan.xsltc.trax.TemplatesImpl", 041 "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", 042 "org.codehaus.groovy.runtime.ConvertedClosure", 043 "org.codehaus.groovy.runtime.MethodClosure", 044 "org.springframework.beans.factory.ObjectFactory", 045 "com.sun.org.apache.bcel.internal.util.ClassLoader", 046 "org.mozilla.javascript.NativeJavaObject", 047 "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource", 048 "com.mchange.v2.c3p0.JndiRefForwardingDataSource", 049 "bsh.XThis", 050 "bsh.Interpreter", 051 "com.sun.rowset.JdbcRowSetImpl" 052 ); 053 054 private static final Set<String> REJECTED_PACKAGES = Set.of( 055 "org.apache.commons.collections.functors.", 056 "org.apache.commons.collections4.functors.", 057 "javassist.", 058 "net.bytebuddy.", 059 "org.hibernate.jmx.", 060 "javax.management." 061 ); 062 063 private static final ObjectInputFilter SERIAL_FILTER = filterInfo -> { 064 if (filterInfo.depth() > MAX_DEPTH) 065 return ObjectInputFilter.Status.REJECTED; 066 067 Class<?> clazz = filterInfo.serialClass(); 068 if (clazz != null) { 069 String name = clazz.getName(); 070 if (REJECTED_CLASSES.contains(name)) 071 return ObjectInputFilter.Status.REJECTED; 072 for (String pkg : REJECTED_PACKAGES) { 073 if (name.startsWith(pkg)) 074 return ObjectInputFilter.Status.REJECTED; 075 } 076 } 077 return ObjectInputFilter.Status.UNDECIDED; 078 }; 079 080 /** 081 * Creates an ObjectInputStream with a deserialization filter that rejects 082 * known gadget-chain classes and enforces resource limits. 083 * 084 * @param in the underlying input stream 085 * @return a filtered ObjectInputStream 086 * @throws IOException if an I/O error occurs 087 */ 088 public static ObjectInputStream createSafeObjectInputStream(InputStream in) throws IOException { 089 ObjectInputStream ois = new ObjectInputStream(in); 090 ois.setObjectInputFilter(SERIAL_FILTER); 091 return ois; 092 } 093 094 /** 095 * Creates an ObjectInputStream with an allow-list filter that only permits 096 * classes matching the specified packages or exact class names. 097 * 098 * @param in the underlying input stream 099 * @param allowedPackages package prefixes to allow (e.g. "org.jpos.iso.") 100 * @return a filtered ObjectInputStream 101 * @throws IOException if an I/O error occurs 102 */ 103 public static ObjectInputStream createAllowListObjectInputStream(InputStream in, String... allowedPackages) throws IOException { 104 ObjectInputStream ois = new ObjectInputStream(in); 105 ois.setObjectInputFilter(filterInfo -> { 106 if (filterInfo.depth() > MAX_DEPTH) 107 return ObjectInputFilter.Status.REJECTED; 108 109 Class<?> clazz = filterInfo.serialClass(); 110 if (clazz == null) 111 return ObjectInputFilter.Status.UNDECIDED; 112 113 if (clazz.isPrimitive() || clazz.isArray()) 114 return ObjectInputFilter.Status.ALLOWED; 115 116 String name = clazz.getName(); 117 if (name.startsWith("java.lang.") || name.startsWith("java.util.") || name.startsWith("java.math.")) 118 return ObjectInputFilter.Status.ALLOWED; 119 120 for (String pkg : allowedPackages) { 121 if (name.startsWith(pkg)) 122 return ObjectInputFilter.Status.ALLOWED; 123 } 124 return ObjectInputFilter.Status.REJECTED; 125 }); 126 return ois; 127 } 128 129 /** 130 * Serializes {@code obj} into a byte array using standard Java serialization. 131 * 132 * @param obj object to serialize 133 * @return the serialized byte array 134 * @throws IOException if writing fails 135 */ 136 public static byte[] serialize (Object obj) throws IOException { 137 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 138 ObjectOutputStream os = new ObjectOutputStream(baos); 139 os.writeObject(obj); 140 return baos.toByteArray(); 141 } 142 /** 143 * Deserializes the byte array using {@link #createSafeObjectInputStream(InputStream)}. 144 * 145 * @param b serialized bytes 146 * @return the deserialized object 147 * @throws IOException if reading fails 148 * @throws ClassNotFoundException if a referenced class cannot be loaded 149 */ 150 public static Object deserialize (byte[] b) throws IOException, ClassNotFoundException { 151 ByteArrayInputStream bais = new ByteArrayInputStream(b); 152 ObjectInputStream is = createSafeObjectInputStream(bais); 153 return is.readObject(); 154 } 155 /** 156 * Deserializes the byte array and casts the result to {@code T}. 157 * 158 * @param <T> expected concrete type 159 * @param b serialized bytes 160 * @param clazz expected class (used for the unchecked cast) 161 * @return the deserialized object 162 * @throws IOException if reading fails 163 * @throws ClassNotFoundException if a referenced class cannot be loaded 164 */ 165 @SuppressWarnings("unchecked") 166 public static <T> T deserialize (byte[] b, Class<T> clazz) throws IOException, ClassNotFoundException { 167 return (T) deserialize(b); 168 } 169 /** 170 * Round-trips an object through serialization and back, useful for deep-cloning. 171 * 172 * @param <T> object type 173 * @param obj object to clone 174 * @return a fresh deserialized copy of {@code obj} 175 * @throws IOException if serialization fails 176 * @throws ClassNotFoundException if a referenced class cannot be loaded 177 */ 178 @SuppressWarnings("unchecked") 179 public static <T> T serializeDeserialize (T obj) throws IOException, ClassNotFoundException { 180 return (T) deserialize (serialize(obj)); 181 } 182 183 /** 184 * Serializes a {@code Map<String,String>} using a compact entry-by-entry format. 185 * 186 * @param m the map to serialize 187 * @return the serialized byte array 188 * @throws IOException if writing fails 189 */ 190 public static byte[] serializeStringMap (Map<String,String> m) 191 throws IOException 192 { 193 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 194 ObjectOutputStream oos = new ObjectOutputStream (baos); 195 Set s = m.entrySet(); 196 oos.writeInt (s.size()); 197 for (Object value : s) { 198 Map.Entry entry = (Map.Entry) value; 199 oos.writeObject(entry.getKey()); 200 oos.writeObject(entry.getValue()); 201 } 202 oos.close(); 203 return baos.toByteArray(); 204 } 205 /** 206 * Inverse of {@link #serializeStringMap(Map)}; only allows JDK collection/string classes. 207 * 208 * @param buf the serialized bytes 209 * @return the deserialized map 210 * @throws ClassNotFoundException if a referenced class cannot be loaded 211 * @throws IOException if reading fails 212 */ 213 public static Map<String,String> deserializeStringMap (byte[] buf) 214 throws ClassNotFoundException, IOException 215 { 216 ByteArrayInputStream bais = new ByteArrayInputStream (buf); 217 ObjectInputStream ois = createAllowListObjectInputStream(bais); 218 Map<String,String> m = new HashMap<>(); 219 int size = ois.readInt(); 220 for (int i=0; i<size; i++) { 221 m.put ( 222 (String) ois.readObject(), 223 (String) ois.readObject() 224 ); 225 } 226 return m; 227 } 228}