[JAVA] Élément 88: Écriture défensive des méthodes readObject

88. Ecrire une méthode readObject de manière défensive

Dans Item50, il y avait une classe de plage de dates immuable avec un champ Date privé modifiable. Il a été protégé contre l'immuable en utilisant une copie défensive de l'objet Date dans son constructeur et son accesseur.

package tryAny.effectiveJava;

import java.io.Serializable;
import java.util.Date;

public final class Period implements Serializable {
    private final Date start;
    private final Date end;

    /**
     * @param start
     *            the beginning of the period
     * @param end
     *            the end of the period; must not precede start
     * @throws IllegalArgumentException
     *             if start is after end
     * @throws NullPointerException
     *             if start or end is null
     */
    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());
        if (this.start.compareTo(this.end) > 0)
            throw new IllegalArgumentException(this.start + " after " + this.end);
    }

    public Date start() {
        return new Date(start.getTime());
    }

    public Date end() {
        return new Date(end.getTime());
    }

    public String toString() {
        return start + "-" + end;
    }

}

Pensez à rendre cette classe sérialisable. Puisqu'il n'y a pas de divergence entre la structure physique et le contenu logique des données, il semble qu'il soit suffisant d'utiliser la forme sérialisée par défaut, c'est-à-dire d'implémenter Serializable, mais cela ne conserve pas la condition invariante.

Le problème est que la méthode readObject est essentiellement un constructeur public et nécessite le même soin que les autres constructeurs. Le constructeur doit vérifier les arguments (Item49) et requiert une copie défensive des arguments (Item50). Par conséquent, readObject a la même considération. Oublier ces considérations dans readObject permet à un attaquant de briser des conditions invariantes.

Cas de versement d'un flux d'octets défectueux

En gros, readObject est un constructeur qui traite un flux d'octets comme un argument unique. Les Bytestreams sont généralement le résultat de la sérialisation d'une instance normalement construite. Le problème est un flux d'octets construit artificiellement pour violer les conditions invariantes. Un tel flux d'octets créerait normalement un objet impossible. Voici un exemple (la fin de la période vient avant le début), mais dans mon environnement,

Exception in thread "main" java.lang.IllegalArgumentException: java.lang.ClassNotFoundException: Period
	at tryAny.effectiveJava.BogusPeriod.deserialize(BogusPeriod.java:31)
	at tryAny.effectiveJava.BogusPeriod.main(BogusPeriod.java:20)

Sortira. ..

package tryAny.effectiveJava;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;

public class BogusPeriod {
    // Byte stream could not have come from real Period instance
    private static final byte[] serializedForm = new byte[] { (byte) 0xac, (byte) 0xed, 0x00, 0x05, 0x73, 0x72, 0x00,
            0x06, 0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte) 0xf8, 0x2b, 0x4f, 0x46, (byte) 0xc0,
            (byte) 0xf4, 0x02, 0x00, 0x02, 0x4c, 0x00, 0x03, 0x65, 0x6e, 0x64, 0x74, 0x00, 0x10, 0x4c, 0x6a, 0x61, 0x76,
            0x61, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x2f, 0x44, 0x61, 0x74, 0x65, 0x3b, 0x4c, 0x00, 0x05, 0x73, 0x74, 0x61,
            0x72, 0x74, 0x71, 0x00, 0x7e, 0x00, 0x01, 0x78, 0x70, 0x73, 0x72, 0x00, 0x0e, 0x6a, 0x61, 0x76, 0x61, 0x2e,
            0x75, 0x74, 0x69, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x68, 0x6a, (byte) 0x81, 0x01, 0x4b, 0x59, 0x74, 0x19,
            0x03, 0x00, 0x00, 0x78, 0x70, 0x77, 0x08, 0x00, 0x00, 0x00, 0x66, (byte) 0xdf, 0x6e, 0x1e, 0x00, 0x78, 0x73,
            0x71, 0x00, 0x7e, 0x00, 0x03, 0x77, 0x08, 0x00, 0x00, 0x00, (byte) 0xd5, 0x17, 0x69, 0x22, 0x00, 0x78 };

    public static void main(String[] args) {
        Period p = (Period) deserialize(serializedForm);
        System.out.println(p);
    }

    // Returns the object with the specified serialized form
    public static Object deserialize(byte[] sf) {
        try {
            InputStream is = new ByteArrayInputStream(sf);
            ObjectInputStream ois = new ObjectInputStream(is);
            return ois.readObject();
        } catch (Exception e) {
            throw new IllegalArgumentException(e.toString());
        }
    }

}

La classe supérieure devrait normalement afficher la date de fin avant la date de début, ce qui viole la condition invariante de la classe.

Afin d'éviter cette attaque, il est nécessaire d'effectuer un contrôle de validation avec la méthode readObject comme indiqué ci-dessous.

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();

        if (start.compareTo(end) > 0) {
            throw new InvalidObjectException(start + "after" + end);
        }
    }

Ajouter une mauvaise référence

Même si l'attaque ci-dessus peut être empêchée, il est possible de créer une instance en respectant les conditions de Period, et d'ajouter une référence à Date dans le champ privé de Period à la fin.

Autrement dit, un attaquant lit une instance Period à partir d'un ObjectInputStream et lit la "mauvaise référence d'objet" attachée à ce flux. Ces références permettent à un attaquant d'obtenir une référence à une instance Date dans un objet Period et de modifier l'instance Period. Ce qui suit est un exemple.

package tryAny.effectiveJava;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Date;

public class MutablePeriod {
    public final Period period;

    public final Date start;
    public final Date end;

    public MutablePeriod() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);
            //Écrire une instance de période
            out.writeObject(new Period(new Date(), new Date()));
            //Créer une référence à une propriété spécifique d'une instance Period
            byte[] ref = { 0x71, 0, 0x7e, 0, 5 };
            bos.write(ref);
            ref[4] = 4;
            bos.write(ref);

            //Générer des instances de période variable
            ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
            period = (Period) in.readObject();
            start = (Date) in.readObject();
            end = (Date) in.readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new AssertionError(e);
        }
    }

    public static void main(String[] args) {
        MutablePeriod mp = new MutablePeriod();
        Period p = mp.period;
        Date pEnd = mp.end;

        // Let's turn back the clock
        pEnd.setYear(78);
        System.out.println(p);

        // Bring back the 60s!
        pEnd.setYear(69);
        System.out.println(p);
    }
}

Lorsque ce programme est exécuté, la sortie est la suivante.

Mon Oct 08 18:43:49 JST 2018-Sun Oct 08 18:43:49 JST 1978
Mon Oct 08 18:43:49 JST 2018-Wed Oct 08 18:43:49 JST 1969

Si vous laissez l'instance Period telle quelle, vous serez libre de manipuler les composants internes. Les systèmes qui reposent sur l'immuabilité de Period pour la sécurité peuvent alors être attaqués.

La cause est que la copie défensive n'a pas été prise dans la méthode readObject de Period. ** Lorsqu'un objet est désérialisé, il est impératif de faire une copie défensive de tout champ contenant une référence d'objet que le client ne doit pas détenir. ** ** Plus précisément, il doit s'agir d'un readObject comme celui ci-dessous. (À ce stade, le début et la fin ne sont plus définitifs)

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();

        start = new Date(start.getTime());
        end = new Date(end.getTime());

        if (start.compareTo(end) > 0) {
            throw new InvalidObjectException(start + "after" + end);
        }
    }

En faisant cela, même lorsque MutablePeriod est exécuté, ce sera comme suit et la fraude ne se produira pas.

Mon Oct 08 20:19:56 JST 2018-Mon Oct 08 20:19:56 JST 2018
Mon Oct 08 20:19:56 JST 2018-Mon Oct 08 20:19:56 JST 2018

Si readObject peut être laissé à sa valeur par défaut

Vous devez vous demander si vous souhaitez créer un constructeur public qui prend des champs non transitoires comme arguments, et si vous n'avez pas à valider cet argument. Si cela ne fonctionne pas, vous devrez également faire une copie défensive de readObject et effectuer une vérification de validation. Comme alternative, vous devez utiliser le modèle de proxy de sérialisation (Item90).

Une autre similitude entre readObject et les constructeurs est que vous ne devez pas utiliser de méthodes qui peuvent être remplacées, directement ou indirectement, dans la méthode readObject (Item19). Si elle est utilisée, la méthode surchargée sera appelée et l'état de la sous-classe sera exécuté sans être désérialisé, ce qui peut entraîner une erreur.

Résumé

Les instructions pour écrire readObject sont résumées ci-dessous.

Recommended Posts

Élément 88: Écriture défensive des méthodes readObject
Point 29: Favoriser les types génériques
Point 66: Utiliser judicieusement les méthodes natives
Élément 88: Écriture défensive des méthodes readObject
Point 66: Utiliser judicieusement les méthodes natives
Écrire des méthodes Ruby en utilisant C (Partie 1)