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}