[JAVA] Punkt 88: ReadObject-Methoden defensiv schreiben

88. Schreiben Sie eine readObject-Methode defensiv

In Item50 gab es eine unveränderliche Datumsbereichsklasse mit einem veränderlichen privaten Datumsfeld. Es wurde unveränderlich gehalten, indem eine defensive Kopie des Date-Objekts in seinem Konstruktor und Accessor verwendet wurde.

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;
    }

}

Erwägen Sie, diese Klasse serialisierbar zu machen. Da es keine Diskrepanz zwischen der physischen Struktur und dem logischen Dateninhalt gibt, scheint es ausreichend zu sein, die standardmäßige serialisierte Form zu verwenden, dh Serializable zu implementieren, aber dabei bleibt die invariante Bedingung nicht erhalten.

Das Problem ist, dass die readObject-Methode im Wesentlichen ein öffentlicher Konstruktor ist und die gleiche Sorgfalt wie andere Konstruktoren erfordert. Der Konstruktor muss die Argumente überprüfen (Item49) und benötigt eine defensive Kopie der Argumente (Item50). Daher hat readObject die gleiche Überlegung. Wenn Sie diese Überlegungen in readObject vergessen, kann ein Angreifer unveränderliche Bedingungen brechen.

Fall des Gießens eines fehlerhaften Bytestreams

Grob gesagt ist readObject ein Konstruktor, der einen Bytestream als einzelnes Argument behandelt. Bytestreams sind normalerweise das Ergebnis der Serialisierung einer normal erstellten Instanz. Das Problem ist ein Byte-Stream, der künstlich konstruiert wurde, um invariante Bedingungen zu verletzen. Ein solcher Bytestrom würde normalerweise ein unmögliches Objekt erzeugen. Unten ist ein Beispiel (das Ende der Periode kommt vor dem Start), aber in meiner Umgebung,

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)

Wird rauskommen. ..

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());
        }
    }

}

Die obere Klasse sollte normalerweise das Enddatum vor dem Startdatum anzeigen, was gegen die invariante Bedingung der Klasse verstößt.

Um diesen Angriff zu verhindern, muss eine Validierungsprüfung mit der readObject-Methode durchgeführt werden (siehe unten).

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

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

Fügen Sie eine schlechte Referenz hinzu

Selbst wenn der obige Angriff verhindert werden kann, ist es möglich, eine Instanz zu erstellen, während die Bedingungen der Periode beachtet werden, und am Ende im privaten Feld der Periode einen Verweis auf Datum hinzuzufügen.

Das heißt, ein Angreifer liest eine Period-Instanz aus einem ObjectInputStream und liest die an diesen Stream angehängte "Referenz für fehlerhafte Objekte". Mit diesen Verweisen kann ein Angreifer einen Verweis auf eine Date-Instanz innerhalb eines Period-Objekts abrufen und die Period-Instanz ändern. Das Folgende ist ein Beispiel.

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);
            //Schreibperiodeninstanz
            out.writeObject(new Period(new Date(), new Date()));
            //Erstellen Sie einen Verweis auf eine bestimmte Eigenschaft einer Period-Instanz
            byte[] ref = { 0x71, 0, 0x7e, 0, 5 };
            bos.write(ref);
            ref[4] = 4;
            bos.write(ref);

            //Generieren Sie variable Periodeninstanzen
            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);
    }
}

Wenn dieses Programm ausgeführt wird, lautet die Ausgabe wie folgt.

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

Wenn Sie die Period-Instanz unverändert lassen, können Sie die internen Komponenten frei bearbeiten. Systeme, deren Sicherheit auf der Unveränderlichkeit von Period beruht, können dann angegriffen werden.

Die Ursache ist, dass die defensive Kopie nicht in der readObject-Methode von Period erstellt wurde. ** Wenn ein Objekt deserialisiert wird, muss unbedingt eine defensive Kopie eines Felds erstellt werden, das eine Objektreferenz enthält, die der Client nicht enthalten darf. ** ** ** Insbesondere sollte es sich um ein readObject handeln, wie unten gezeigt. (Zu diesem Zeitpunkt sind Start und Ende nicht mehr endgültig)

    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);
        }
    }

Auf diese Weise tritt Folgendes auf, auch wenn MutablePeriod ausgeführt wird, und es tritt kein Betrug auf.

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

Ob readObject als Standard-Lithomas beibehalten werden kann oder nicht: Wie wäre es mit dem Konstruktor?

Sie sollten sich fragen, ob Sie einen öffentlichen Konstruktor erstellen möchten, der nicht transiente Felder als Argumente verwendet, und ob Sie dieses Argument nicht validieren müssen. Wenn dies nicht funktioniert, müssen Sie auch eine defensive Kopie des readObject erstellen und eine Validierungsprüfung durchführen. Alternativ sollten Sie das Serialisierungs-Proxy-Muster (Item90) verwenden.

Eine weitere Ähnlichkeit zwischen readObject und dem Konstruktor besteht darin, dass Sie keine Methoden verwenden dürfen, die direkt oder indirekt innerhalb der readObject-Methode (Item19) überschrieben werden können. Bei Verwendung wird die überschriebene Methode aufgerufen und der Status der Unterklasse wird ausgeführt, ohne deserialisiert zu werden, was zu einem Fehler führen kann.

Zusammenfassung

Die Richtlinien zum Schreiben von readObject sind unten zusammengefasst.

Recommended Posts

Punkt 88: ReadObject-Methoden defensiv schreiben
Punkt 29: Generische Typen bevorzugen
Punkt 66: Verwenden Sie native Methoden mit Bedacht
Punkt 88: ReadObject-Methoden defensiv schreiben
Punkt 66: Verwenden Sie native Methoden mit Bedacht
Schreiben Sie Ruby-Methoden mit C (Teil 1)