jdk.serialFilter не работает для ограничения глубины TreeMap в Java (предотвращает DoS-атаку через Java)

Как предотвратить DoS-атаку через Java TreeMap?

В моем коде есть API, который принимает объект Map. Теперь я хочу запретить клиенту отправлять Map объектов определенной длины.

Теперь maxarray в jdk.serialFilter может предотвратить отправку клиентом HashMap объект размером > maxarray.

Я хочу сделать то же самое и для TreeMap. Но поле maxarray не работает для TreeMap. Он не может отклонить этот запрос.

Я тоже поставил maxdepth размер. Но ничего не работает.

Кто-нибудь может помочь мне с этим?


person learner    schedule 29.07.2019    source источник
comment
я не эксперт по безопасности, но у меня есть некоторые базовые знания о DDOS ... насколько мне известно, нет способа предотвратить эту атаку с помощью кода JAVA .. см. эту ссылку --- esecurityplanet.com/network-security/   -  person Istiaque Hossain    schedule 29.07.2019
comment
Я могу отклонить запрос, используя maxArraysize jdk.serialFilter для объекта hashmap. док. oracle.com/javase/10/core/ . Но не в состоянии сделать это для карты дерева.   -  person learner    schedule 29.07.2019
comment
Я не думаю, что maxarray (не maxArraySize, а также DoS, а не DDoS здесь) затронул HashMap, так как он не использует массив в качестве своей последовательной формы. Однако принципиально эти меры неэффективны — при любой разумной конфигурации с обратными ссылками можно иметь несколько уровней коллекций по сотне элементов в каждой. Единственное преимущество TreeMap заключается в том, что для этого требуется Comparator или Comparable. Мне было бы интересно услышать другое.   -  person Tom Hawtin - tackline    schedule 07.02.2020
comment
@TomHawtin-tackline Глядя на свой профиль, вы знаете свое дело. Что вы думаете о моем ответе/подходе?   -  person Tschallacka    schedule 07.02.2020
comment
@Tschallacka Это, безусловно, много работы. Я думаю, вы могли бы избежать повторной реализации части java.io., вместо этого заставив ее создавать фиктивные объекты. Тем не менее, вы должны быть крайне заинтересованы в сохранении формата сериализации Java, если повторная реализация стоимостной стороны вещей с соответствующими проверками работоспособности того стоит.   -  person Tom Hawtin - tackline    schedule 08.02.2020
comment
@TomHawtin-tackline да, единственная причина, по которой я это сделал, заключается в том, что в древовидной карте есть опция сравнения, и это делает позицию 4 байтов, необходимых для проверки, полным подстановочным знаком. Следуя шаблону чтения десериализации и проверкам, вы получаете полунадежный способ отбрасывать ненужные данные, получая при этом желаемое значение. Это окольный путь, и проверка размера почтового ящика проще. Но таким образом вы получаете данные с минимальными накладными расходами и выделением памяти с содержимым карты.   -  person Tschallacka    schedule 08.02.2020
comment
@Tschallacka Каждый пользовательский readObject должен вызывать defaultReadObject или эквивалент. Это будет читать любые поля, которые злоумышленник захочет поместить в поток. (ObjectStreamClass - это действительно несколько классов в одном. Трудно читать исходный код, но макет в потоке вообще не должен соответствовать классу.)   -  person Tom Hawtin - tackline    schedule 08.02.2020
comment
@TomHawtin-tackline Насколько я видел, defaultReadObject выделяет много материала, занимая драгоценные ресурсы. Вот почему я просто переместил указатель вместе с чтением. Также он работает с контекстом и выдает ошибки, когда не является частью потока.   -  person Tschallacka    schedule 08.02.2020


Ответы (3)


TL;DR;

Это было целое приключение по изучению кода, обрабатывающего сериализацию TreeMap, но мне удалось найти золото. Чтобы найти золото (код), прокрутите ответ до конца. Если вы хотите следовать процессу дедукции, чтобы вы могли делать это с другими классами, вам придется бороться с моим бредом.

Я мог бы сделать это более кратким, но я только что провел 7 часов, читая код и экспериментируя, мне это надоело на данный момент, и этот пост может быть поучительным для других, которые хотят предпринять это приключение.

Введение

Моя атака заключается в том, что десериализация всего этого занимает слишком много памяти, выделяя объекты, которые вы, возможно, не хотите использовать, или занимая оперативную память. Поэтому я решил просто прочитать необработанные данные и проверить размер TreeMap. Таким образом, у нас есть единственный фрагмент данных, который нам нужен, чтобы оценить, следует ли нам принимать или нет. Да, это означает считывание данных дважды, если они приняты, но это компромисс, на который вам нужно пойти, если вы хотите использовать это. Этот код пропускает множество шагов проверки, которые использует java, потому что нас это не интересует. Нам просто нужен надежный способ получить размер TreeMap без необходимости загружать всю карту дерева со всеми данными.

Там, где обычно вы загружаете все данные, читаете весь файл/поток байтов и используете его для инициализации, нам нужно только прочитать части начала файла. Сокращение работы, которую необходимо выполнить, и времени, которое необходимо тратить впустую. Нам просто нужно переместить указатель чтения файла вперед надежным способом, чтобы мы всегда получали правильные байты для нашего процесса. Это значительно сокращает рабочий процесс для процесса Java. После того, как размер был проверен быстрым обычным чтением файла, его можно подвергнуть фактическому процессу сериализации или отбросить. Это лишь небольшие накладные расходы по сравнению с обычной работой, но они служат эффективным барьером, оставаясь при этом гибкими в ваших принятых критериях.

нормальная работа против проверки работы

Начало приключения

Глядя на исходный код TreeMap https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/util/TreeMap.java#L123 мы видим, что размер переходное значение. Это означает, что он не кодируется в сериализованных данных, поэтому при быстрой проверке его нельзя проверить, прочитав значение поля из отправленных байтов.

Но... не вся надежда потеряна. Потому что, если мы проверим writeObject(), мы увидим, что размер закодирован https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/util/TreeMap.java#L2268

Это означает, что у нас есть значения байтов, которые мы можем проверить в отправленных необработанных данных!.

Теперь давайте проверим, что делает defaultReadObject.

L492 Сначала он проверяет, десериализуется ли он, если нет, то блокируется. Ладно, нам это неинтересно.
L495 Затем ему нужен экземпляр объекта, SerialCallbackContext был инициализирован с этим, поэтому он не выполняет чтение.
L496 Затем он получает экземпляр ObjectStreamClass из SerialCallbackContext, так что теперь мы будем работать с ObjectStream.
L497 Некоторые режимы были изменены, но затем мы идем читать поля.

Хорошо, переходим на ObjectInputStream
L1944 снова ссылка на класс, которая была предоставлена ​​экземпляру потока объектов (для быстрого изложения L262, который установлен в L442), поэтому чтение не выполняется.
L1949 получение размера полей по умолчанию с помощью getPrimDataSize, который задается в computeFieldOffsets. Это полезно для нас, только позор... это недоступно, поэтому давайте разберемся, как это эмулировать, просто как примечание.


L1255 Он использует переменную поля. Это установлено в getSerialFields, который, к сожалению, также является приватным. В этот момент у меня сложилось впечатление, что я возился с силами, которых не должен касаться. Но я иду дальше, игнорируя запретный знак, меня ждут приключения!
getDeclaredSerialFields и getDefaultSerialFields вызывается в этом методе, поэтому мы можем использовать его содержимое для эмуляции его функциональности.
Анализ getDeclaredSerialFields мы см., это действует только в том случае, если serialPersistentFields объявлен в файле класс TreeMap. Ни TreeMap, ни его родитель AbstractMap содержит это поле. Поэтому мы игнорируем метод getDeclaredSerialFields. На getDefaultSerialFields

Итак, если мы возьмем этот код, повозимся с ним, мы сможем получить значимые данные и увидим, что TreeMap имеет одно поле, и теперь у нас есть динамический метод для «эмулирования» получения полей по умолчанию, если что-то изменится по какой-либо причине.

https://ideone.com/UqqKSG (я оставил имена классов с полными путями, чтобы было легче увидеть, какие классы я пользуюсь)

    java.lang.reflect.Field[] clFields = TreeMap.class.getDeclaredFields();
    ArrayList<java.lang.reflect.Field> list = new ArrayList<>();
    int mask = java.lang.reflect.Modifier.STATIC | java.lang.reflect.Modifier.TRANSIENT;

    for (int i = 0; i < clFields.length; i++) {
        // Check for non transient and non static fields.
        if ((clFields[i].getModifiers() & mask) == 0) {
            list.add(clFields[i]);
            System.out.println("Found field " + clFields[i].getName());
        }
    }
    int size = list.size();
    System.out.println(size);

Компаратор найденных полей
1


L1951 Вернувшись в ObjectInputStream, мы видим, что этот размер используется для создания массива, который будет использоваться в качестве буфера для чтения, а затем они считываются полностью, с аргументами пустой массив, смещение 0, длина полей (1) и ЛОЖЬ. Этот метод вызывается в BlockDataInputStream, а значение false означает, что он не будет скопирован. Это всего лишь вспомогательный метод для обработки потока данных с помощью PeekInputStream(in), мы можем использовать те же методы в потоке, который мы собираемся использовать с некоторой возней, хотя сейчас нам это не нужно, потому что нет примитивных типов хранятся в TreeMap. Поэтому я оставлю этот ход мыслей для этого ответа.

L1964 вызывает readObject0, который считывает компаратор, используемый в TreeMap. Он проверяет oldMode, который возвращает, читается ли поток в режиме блочных данных или нет, и мы можем видеть, что он был установлен в режим потока (false) в readFields, поэтому я пропущу эту часть.
L1315 простая проверка того, что рекурсия не происходит более одного раза, но просматривается один байт. Давайте посмотрим, что для этого может предоставить TreeMap. Это заняло у меня больше времени, чем я ожидал. Я не могу опубликовать код здесь, он слишком длинный, но он у меня есть на ideone и суть.

  • В основном вам нужно скопировать встроенный класс BlockDataInputStream,
  • добавьте private static native void bytesToFloats(byte[] src, int srcpos, float[] dst, int dstpos, int nfloats);private static native void bytesToDoubles(byte[] src, int srcpos, double[] dst, int dstpos, int ndoubles); в BlockDataInputStream. Если вам действительно нужно использовать эти методы, замените их чем-то Java. Это будет выдает ошибку времени выполнения.
  • скопировать встроенный класс PeekInputStream
  • скопируйте класс java.io.Bits.
  • Ссылки TC_ должны указывать на java.io.ObjectStreamConstants.TC_
    BlockDataInputStream bin = new BlockDataInputStream(getTreeMapInputStream());
    bin.setBlockDataMode(false);
    byte b = bin.peekByte();
    System.out.println("Does b ("+String.format("%02X ", b)+") equals TC_RESET?" + (java.io.ObjectStreamConstants.TC_RESET == b ? "yes": "no"));

Соответствует ли b (-84) TC_RESET? нет

Мы видим, что читаем 0xAC, давайте срезать путь и заглянуть в java.io.ObjectStreamConstants, что это такое. Для чисто 0xAC нет записи, но похоже, что это часть заголовка.
Давайте проверим работоспособность с readStreamHeader и вставьте содержимое этого метода прямо перед нашим кодом peekByte, снова обновив ссылки TC_. Теперь мы получаем вывод 0x73. Прогресс!
0x73 — это TC_OBJECT, так что давайте перейдем к L1347
Там мы находим, что readOrdinaryObject, который выполняет readByte().
Затем classDescription считывается, что приводит к переходу на readNonProxy
Затем у нас есть вызов readUTF(), readLong(), readByte(), readShort, чтобы читать поля..., затем для каждого поле a readByte(), readUTF ().

Итак, давайте подражать этому. Первое, с чем я сталкиваюсь, это то, что он пытается читать за пределами длины строки (имя класса из 29184 символов? Вы так не думаете ) для имени класса, поэтому я что-то упускаю. Я понятия не имею, чего мне не хватает на данный момент, но я запускаю его на ideone, и, возможно, он работает на версии java, где они добавили дополнительный байт перед чтением UTF. Мне не терпится посмотреть, если честно. Работает, я доволен. В любом случае, после считывания лишнего байта все работает отлично, и мы имеем желаемое. TODO: выяснить, где считывается лишний байт

    BlockDataInputStream bin = new BlockDataInputStream(getTreeMapInputStream());
    bin.setBlockDataMode(false);
    short s0 = bin.readShort();
    short s1 = bin.readShort();
    if (s0 != java.io.ObjectStreamConstants.STREAM_MAGIC || s1 != java.io.ObjectStreamConstants.STREAM_VERSION) {
        throw new StreamCorruptedException(
            String.format("invalid stream header: %04X%04X", s0, s1));
    }
    byte b = bin.readByte();
    if(b == java.io.ObjectStreamConstants.TC_OBJECT) {
        bin.readByte();
        String name = bin.readUTF();
        System.out.println(name);
        System.out.println("Is string ("+name+")it a java.util.TreeMap? "+(name.equals("java.util.TreeMap") ? "yes":"no"));
        bin.readLong();
        bin.readByte();
        short fields = bin.readShort();
        for(short i = 0; i < fields; i++) {
            bin.readByte();
            System.out.println("Read field name "+bin.readUTF());
        }
    }

Теперь мы продолжаем на Строка 1771, чтобы увидеть, что читается после прочтения описания класса. После этого происходит много проверок экземпляров объектов и т. д. Это похоже на спагетти, через которые мне не хочется разбираться. Давайте взламываем и анализируем данные.

Данные в виде строки

tLjava/util/Comparator;xppwsrjava.lang.Integer¬ᅠᄂ￷チヌ8Ivaluexrjava.lang.Numberニᆲユヤ¢ヒxptData1sq~tData5sq~tData4sq~tData2sq~FtData3x -74 -00 -16 -4C -6A -61 -76 - 61 -2Ф -75 -74 -69 -6С -2Ф -43 -6Ф -6Д -70 -61 -72 -61 -74 -6Ф -72 -3Б -78 -70 -70 -77 -04 -00 -00 - 00 -05 -73 -72 -00 -11 -6А -61 -76 -61 -2Е -6С -61 -6Е -67 -2Е -49 -6Е -74 -65 -67 -65 -72 -12 -Е2 - А0 -А4 -Ф7 -81 -87 -38 -02 -00 -01 -49 -00 -05 -76 -61 -6С -75 -65 -78 -72 -00 -10 -6А -61 -76 -61 - 2Е -6С -61 -6Е -67 -2Е -4Е -75 -6Д -62 -65 -72 -86 -АС -95 -1Д -0Б -94 -Е0 -8Б -02 -00 -00 -78 -70 - 00 -00 -00 -01 -74 -00 -05 -44 -61 -74 -61 -31 -73 -71 -00 -7Е -00 -03 -00 -00 -00 -02 -74 -00 -05 - 44 -61 -74 -61 -35 -73 -71 -00 -7Е -00 -03 -00 -00 -00 -04 -74 -00 -05 -44 -61 -74 -61 -34 -73 -71 - 00 -7Е -00 -03 -00 -00 -00 -17 -74 -00 -05 -44 -61 -74 -61 -32 -73 -71 -00 -7Е -00 -03 -00 -00 -00 - 46 -74 -00 -05 -44 -61 -74 -61 -33 -78

Т. Мы знаем, что размер элементов записывается перед элементами. Поля Data1 - Date5 — это значения, хранящиеся на карте. Поэтому, когда часть Data1sq приходит после этого, все спорно. Давайте добавим элемент на карту, чтобы увидеть, какое значение изменится!

74 -00 -16 -4С -6А -61 -76 -61 -2Ф -75 -74 -69 -6С -2Ф -43 -6Ф -6Д -70 -61 -72 -61 -74 -6Ф -72 -3Б - 78 -78 -70 -70 -77 -04 -00 -00 -00 -05 -73 -72 -00 -11 -6A -61 -76 -61 -2E
74 -00 -16 -4C -6A -61 -76 -61 -2F -75 -74 -69 -6C -2F -43 -6F -6D -70 -61 -72 -61 -74 -6F -72 -3B -78 -78 -70 -70 -77 -04 -00 -00 -00 -06 -73 -72 -00 -11 -6А -61 -76 -61 -2Е

Хорошо, теперь мы знаем, сколько укусов нам еще предстоит зарезать. Давайте посмотрим, сможем ли мы вывести здесь некоторую логику с заданными значениями.
Первое значение — 74. Проверяя ObjectStreamConstants, мы видим, что это означает строку. Давайте прочитаем этот байт, а затем UTF.
Теперь у нас осталось -70 -70 -77 -04 -00 -00 -00 -06. Положим это помимо констант.

NULL - NULL - BLOCKDATA - значение 4 - значение 0 - значение 0 - значение 0 - значение 6

Мы могли бы теоретизировать здесь:

После данных блока записывается целое число. Целое число составляет четыре байта. отсюда и четверка. Следующие четыре позиции составляют целое число.

Давайте посмотрим, что произойдет, если мы добавим компаратор в древовидную карту.

xpsr'java.util.Collections$ReverseComparatordハ￰SNJ￐xpwsrjava.lang.Integer¬ᅠᄂ￷チヌ8I
-78 -70 -73 -72 -00 -27 -6A -61 -76 -61 -2E - 75 -74 -69 -6C -2E -43 -6F -6C -6C -65 -63 -74 -69 -6F -6E -73 -24 -52 -65 -76 -65 -72 -73 -65 -43 - 6Ф -6Д -70 -61 -72 -61 -74 -6Ф -72 -64 -04 -8А -Ф0 -53 -4Е -4А -Д0 -02 -00 -00 -78 -70 -77 -04 -00 - 00 -00 -06

Мы видим END_BLOCK, NULL, OBJECT

Хорошо. Итак, теперь мы знаем, что второй Null является держателем данных Comparator. Так что мы можем взглянуть на это. Нам нужно пропустить два байта, а затем посмотреть, является ли это байтом объекта. Если это так, нам нужно прочитать данные объекта, чтобы мы могли добраться до желаемой позиции.

Давайте сделаем паузу и просмотрим код на данный момент: https://ideone.com/ma6nQy

    BlockDataInputStream bin = new BlockDataInputStream(getTreeMapInputStream());
    bin.setBlockDataMode(false);
    short s0 = bin.readShort();
    short s1 = bin.readShort();
    if (s0 != java.io.ObjectStreamConstants.STREAM_MAGIC || s1 != java.io.ObjectStreamConstants.STREAM_VERSION) {
        throw new StreamCorruptedException(
            String.format("invalid stream header: %04X%04X", s0, s1));
    }
    byte b = bin.peekByte();

    if(b == java.io.ObjectStreamConstants.TC_OBJECT) {
        Ideone.readObject(bin,true);
    }

    if(bin.readByte() == java.io.ObjectStreamConstants.TC_STRING) {
        String className = bin.readUTF();
        System.out.println(className + "starts with L "+(className.charAt(0) == 'L' ? "yes": "no"));
        if(className.charAt(0) == 'L') {
            // Skip two bytes
            bin.readByte();
            bin.readByte();
            b = bin.peekByte();
            if(b == java.io.ObjectStreamConstants.TC_OBJECT) {
                System.out.println("reading object");
                Ideone.readObject(bin,true);
            }
            else {
                // remove the null byte so we end up at same position
                bin.readByte();
            }
        }
    }
    int length = 50;
    byte[] bytes = new byte[length];
    for(int c=0;c<length;c++) {
        bytes[c] = bin.readByte();
        System.out.print((char)(bytes[c]));
    }
    for(int c=0;c<length;c++) {
        System.out.print("-"+String.format("%02X ", bytes[c]));
    }
}

public static void readObject(BlockDataInputStream bin, boolean doExtra) throws Exception {
    byte b = bin.readByte();
    if(b == java.io.ObjectStreamConstants.TC_OBJECT) {
        if(doExtra) {
          bin.readByte();
        }
        String name = bin.readUTF();
        System.out.println(name);
        System.out.println("Is string ("+name+")it a java.util.TreeMap? "+(name.equals("java.util.TreeMap") ? "yes":"no"));
        bin.readLong();
        bin.readByte();
        short fields = bin.readShort();
        for(short i = 0; i < fields; i++) {
            bin.readByte();
            System.out.println("Read field name "+bin.readUTF());
        }
    }
}

Компаратор найденных полей
1
java.util.TreeMap
Является ли строка (java.util.TreeMap) java.util.TreeMap? yes
Чтение компаратора имен полей
Ljava/util/Comparator; начинается с L yes
чтение объекта
java.util.Collections$ReverseComparator
Является строкой (java.util.Collections$ReverseComparator) это java.util.TreeMap? no
xpwsrjava.lang.Integer¬ᅠᄂ￷チヌ8Ivaluexr
-78 -70 -77 -04 -00 -00 -00 -06 -73 -72 -00 -11 -6A -61 -76 -61 -2Е -6С -61 -6Е -67 -2Е -49 -6Е -74 -65 -67 -65 -72 -12 -Е2 -А0 -А4 -Ф7 -81 -87 -38 -02 -00 -01 -49 -00 -05 -76 -61 -6С -75 -65 -78 -72

К сожалению, мы не оказываемся в одних и тех же точках на временной шкале.

Когда есть компаратор, мы заканчиваем:

-78 -70 -77 -04 -00 -00 -00 -06

Когда компаратор удален, мы заканчиваем:

-77 -04 -00 -00 -00 -06

Хм. Эти BLOCK END и NULL выглядят очень знакомо. Это те самые байты, которые мы пропустили при чтении компаратора. Эти два байта всегда удаляются, но, по-видимому, компаратор также объявляет свои собственные значения BLOCK END и NULL.

Итак, если есть компаратор, удалите два конечных байта, чтобы мы получили то, что хотим, последовательно. https://ideone.com/pTu8Fd

-77 -04 -00 -00 -00 -06

Затем мы пропускаем следующий BLOCKDATA маркер (77) и доберитесь до золота!

Добавляя дополнительные строки, мы получаем наш вывод: https://ideone.com/wy0uF2

    System.out.println(String.format("%02X ", bin.readByte()));
    if(bin.readByte() == (byte)4) {
        System.out.println("The length is "+ bin.readInt());
    }

77
Длина 6

И у нас есть магическое число, которое нам нужно!

Хорошо. Вывод сделан, давайте очистим его

Полезные вещи, о которых вы заботитесь

Запускаемый фрагмент кода: https://ideone.com/J6ovMy
Полный код также как суть: https://gist.github.com/tschallacka/8f89982e9569d0b9974dff37d8f45faf

 /**
This is dual licensed under MIT. You can choose wether you want to use CC-BY-SA or MIT.
Copyright 2020 Tschallacka
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import java.util.*;
import java.lang.*;
import java.io.*;

/* Name of the class has to be "Main" only if the class is public. */
class Ideone
{
    public static void main (String[] args) throws java.lang.Exception
    {

        doTest(1,true);
        doTest(1,false);

        doTest(20,true);
        doTest(20,false);

        doTest(4,true);
        doTest(19,false);
    }

    public static void doTest(int size, boolean comparator) throws java.lang.Exception {
        SerializedTreeMapAnalyzer analyzer = new SerializedTreeMapAnalyzer();
        System.out.println(analyzer.getSize(Ideone.getTreeMapInputStream(size,comparator)));
    }

    public static ByteArrayInputStream getTreeMapInputStream(int size, boolean comparator) throws Exception {
      TreeMap<Integer, String> tmap = 
             new TreeMap<Integer, String>(comparator?Collections.reverseOrder():null);

      /*Adding elements to TreeMap*/
      for(int i = 0; size > 0 && i < size; i++) {
        tmap.put(i, "Data"+i);
      }

      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      ObjectOutputStream oos = new ObjectOutputStream( baos );
      oos.writeObject( tmap );
      oos.close();
      return  new ByteArrayInputStream(baos.toByteArray());
    }
}

class SerializedTreeMapAnalyzer 
{
    public int getSize(InputStream stream) throws IOException, StreamCorruptedException, Exception {
        BlockDataInputStream bin = new BlockDataInputStream(stream);
        bin.setBlockDataMode(false);

        short s0 = bin.readShort();
        short s1 = bin.readShort();

        if (s0 != java.io.ObjectStreamConstants.STREAM_MAGIC || s1 != java.io.ObjectStreamConstants.STREAM_VERSION) {
            throw new StreamCorruptedException(
                String.format("invalid stream header: %04X%04X", s0, s1));
        }

        byte b = bin.peekByte();

        if(b == java.io.ObjectStreamConstants.TC_OBJECT) {
            this.readObject(bin,true);
        }

        if(bin.readByte() == java.io.ObjectStreamConstants.TC_STRING) {
            String className = bin.readUTF();

            if(className.charAt(0) == 'L') {
                // Skip two bytes
                bin.readByte();
                bin.readByte();
                b = bin.peekByte();
                if(b == java.io.ObjectStreamConstants.TC_OBJECT) {

                    this.readObject(bin,true);
                    bin.readByte();
                    bin.readByte();
                }
                else {
                    // remove the null byte so we end up at same position
                    bin.readByte();
                }
            }
        }
        bin.readByte();
        if(bin.readByte() == (byte)4) {
            return bin.readInt();
        }
        return -1;
    }

    protected void readObject(BlockDataInputStream bin, boolean doExtra) throws Exception {
        byte b = bin.readByte();
        if(b == java.io.ObjectStreamConstants.TC_OBJECT) {
            if(doExtra) {
              bin.readByte();
            }
            String name = bin.readUTF();
            bin.readLong();
            bin.readByte();
            short fields = bin.readShort();
            for(short i = 0; i < fields; i++) {
                bin.readByte();
                bin.readUTF();
            }
        }
    }
}

1
1
20
20
4
19

person Tschallacka    schedule 07.02.2020

Не зная вашего API, но обычно вы ограничиваете размер сообщения, принимаемый вашим сервером приложений. В WildFly вы можете добавить свойство max-post-size к вашему http/https-listener. Это ограничит объем данных, которые ваш сервер готов получить, и, как следствие, ограничит объем данных, которые могут быть обработаны за один запрос.

Другой подход заключается в том, чтобы ввести что-то вроде ограничения скорости — когда ваш клиент выполняет слишком много запросов, вы можете запретить любую обработку данных. Это распространенный подход к ограничению вычислительной мощности, потребляемой отдельными клиентами. Поскольку ваш API не кажется открытым (по крайней мере, вы не говорили об этом), вы можете определить ограничение скорости на уровне клиента. Это может быть лучшим подходом в вашем случае.

К вашему подходу: когда ваш сервер знает, насколько велика карта, он фактически уже принял и получил данные, поэтому ресурсов для этого уже нет (хотя обработка может быть ограничена).

В конце концов, вам нужно будет выбрать подходящий способ для вашего варианта использования. В вашем случае узким местом является не сеть, а вычислительная мощность. Поэтому я предполагаю, что комбинация ограниченного размера поста и лимита скорости будет лучшим решением в вашем случае.

person maio290    schedule 07.02.2020

Поиск в OpenJDK getJavaObjectInputStreamAccess().checkArray приводит к maxarray проверке этих классов.

  • java.util.ArrayDeque
  • java.util.ArrayList
  • java.util.Collection
  • java.util.HashMap
  • java.util.HashSet
  • java.util.Hashtable
  • java.util.IdentityHashMap
  • java.util.ImmutableCollections
  • java.util.PriorityBlockingQueue
  • java.util.PriorityQueue
  • java.util.Properties
  • java.util.concurrent.CopyOnArrayList
  • javax.management.openmbean.TabularDataSupport

И java.io.ObjectInputStream этим пользуется, конечно.

От чего maxarray пытается защититься? Предположительно вредоносные потоки, вызывающие выделение чрезвычайно непропорционального объема памяти. Но это не является кумулятивным, поэтому кажется, что оно полностью неэффективно против чего-либо нового.

TreeMap не использует массивы, поэтому maxarray не может применяться. Если мы хотим ограничить размер TreeMap в рамках усилий по уменьшению максимального размера десериализованных объектов, то maxrefs и maxbytes подходят, как и для любого другого сериализуемого объекта.

person Tom Hawtin - tackline    schedule 14.02.2020