relocSerial その2 内部クラスのシリアライズストリームをstaticとしてデシリアライズ

2019年5月22日

シリアライゼーションクラスを自由に変更するライブラリrelocSerialの紹介の続きである。

relocSerialは、既にシリアライズされてしまった後のシリアライゼーションストリームから、シリアライズ時のクラスとは全く異なるクラスにデシリアライズするためのライブラリである。これを使用すれば、「既にシリアライズされたクラス」のパッケージ名やクラス名を変更することができる。

今回のお題は「内部クラスをシリアライズした結果のシリアライズストリーム中のクラスをstaticクラスとしてデシリアライズすることができるか?」である。結論から言えば可能だ。

可能であることを示す例

これが可能であることを示すユニットテストは以下である(もちろん、relocSerialライブラリが必要)。

import static org.junit.Assert.*;

import java.io.*;

import org.junit.*;

import com.cm55.deser.*;

public class InnerToStaticTest {

  @Test
  public void test1() throws Exception {

    // シリアライズする。Bar0は内部クラス
    byte[]bytes;
    {
      ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
      ObjectOutputStream objOut = new ObjectOutputStream(byteOut);    
      objOut.writeObject(new Foo0());
      bytes = byteOut.toByteArray();
    }

    // デシリアライズする。Bar1はstaticクラス
    RelocSerializer ser = new RelocSerializer();
    ser.addTarget(Foo1.class, "foo", Foo0.class.getName());
    ser.addTarget(Foo1.Bar1.class, "bar", Foo0.Bar0.class.getName());
    Foo1 foo = ser.deserialize(bytes);
    assertEquals(Foo1.Bar1.class, foo.bar.getClass());
    assertEquals(123, foo.bar.value);
  }

  public static class Foo0 implements Serializable {
    private static final long serialVersionUID = 1L;
    Bar0 bar = new Bar0();

    public class Bar0 implements Serializable {
      private static final long serialVersionUID = 1L;
      int value = 123;
    }
  }

  public static class Foo1 implements Serializable {
    private static final long serialVersionUID = 1L;
    Bar1 bar = new Bar1();

    public static class Bar1 implements Serializable {
      private static final long serialVersionUID = 1L;
      int value;
    }
  }
}

ここで、Bar0はFoo0の内部クラスであり、これをシリアライゼーションしbyte配列を作成したが、このbyte配列のデシリアライズでは、staticであるBar1を使用している。結果的に正常にデシリアライズされている。

なぜこれが可能なのか?

this$1変数

内部クラスは、その外側のクラスのオブジェクトを参照している。シリアライズした結果のストリームにもこれが記述されているのだ。しかし、この外側クラスのオブジェクト参照に使われているのは、「見えない」this$1という変数であるようだ。つまり、以下のプログラムがあるとすれば、

  public static class Foo0 implements Serializable {
    private static final long serialVersionUID = 1L;
    Bar0 bar = new Bar0();

    public class Bar0 implements Serializable {
      private static final long serialVersionUID = 1L;
      int value = 123;
    }
  }

これは本質的には以下と同じだ。

  public static class Foo0 implements Serializable {
    private static final long serialVersionUID = 1L;
    Bar0 bar = new Bar0();

    public static /* STATIC !!! */class Bar0 implements Serializable {
      private static final long serialVersionUID = 1L;
      Foo0 this$1;  /** Foo0への参照 !!! */
      int value = 123;
    }
  }

そして、this$1という変数には自動的にFoo0オブジェクトが格納されているわけだ。

変数が存在しないときには単純に無視される

そして、Javaシリアライゼーションの仕様として、デシリアライズに該当する変数が無い場合には単純にその値は無視される。つまり、先のFoo0のシリアライゼーションストリームを、以下のクラスにデシリアライズすると、

  public static class Foo1 implements Serializable {
    private static final long serialVersionUID = 1L;
    Bar1 bar = new Bar1();

    public static class Bar1 implements Serializable {
      private static final long serialVersionUID = 1L;
      int value;
    }
  }

単純にthis$1という変数が無いので無視されるだけなのである。

内部クラスをシリアライズしてはならない

そもそも内部クラスをシリアライズすることは危険である。その外部クラスをも知らないうちにシリアライズしてしまうからだ。

この件については、なぜかJPCERTに文書がある。

リスク評価としては「内部クラスをシリアライズ可能にすると、プラットフォーム依存なコードになる可能性がある。また、内部クラスをシリアライズすると、外部クラスのインスタンスも一緒にシリアライズされることがある。」のみで、どういうわけかサイバーセキュリティとは全く何の関係も無いようだ。単に「動かないかもよ」というだけの話であって、具体的なセキュリティ問題の記述は一切無い。

デシリアライズ先クラスにthis$1を設置するとどうなるか?

では、デシリアライズ先のstaticクラスに、わざとthis$1という変数を設置するとどうなるだろうか?結論からいえば、完全にうまく行く。次のようなものだ。

import static org.junit.Assert.*;

import java.io.*;

import org.junit.*;


public class InnerToStatic2Test {

  @Test
  public void test1() throws Exception {

    // シリアライズする。Bar0は内部クラス
    byte[]bytes;
    {
      ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
      ObjectOutputStream objOut = new ObjectOutputStream(byteOut);    
      objOut.writeObject(new Foo0());
      bytes = byteOut.toByteArray();
    }


    // デシリアライズする
    RelocSerializer ser = new RelocSerializer();
    ser.addTarget(Foo2.class, "foo", Foo0.class.getName());
    ser.addTarget(Foo2.Bar2.class, "bar", Foo0.Bar0.class.getName());
    Foo2 foo = ser.deserialize(bytes);
    assertEquals(Foo2.Bar2.class, foo.bar.getClass());
    assertEquals(123, foo.bar.value);
    assertSame(foo, foo.bar.this$1); // <--- this$1にFoo2クラスへの参照が格納されている!
  }


  public static class Foo0 implements Serializable {
    private static final long serialVersionUID = 1L;
    Bar0 bar = new Bar0();

    public class Bar0 implements Serializable {
      private static final long serialVersionUID = 1L;
      int value = 123;
    }
  }


  public static class Foo2 implements Serializable {
    private static final long serialVersionUID = 1L;
    Bar2 bar = new Bar2();

    public static class Bar2 implements Serializable {
      private static final long serialVersionUID = 1L;
      Foo2 this$1;
      int value;
    }
  }
}