TikaMp4Reader.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.tika.parser.mp4;
import java.io.IOException;
import java.io.InputStream;
import com.drew.imaging.mp4.Mp4Handler;
import com.drew.lang.StreamReader;
import com.drew.metadata.mp4.Mp4BoxHandler;
import com.drew.metadata.mp4.Mp4Context;
import com.drew.metadata.mp4.Mp4MediaHandler;
/**
* A size-bounded reimplementation of com.drew.imaging.mp4.Mp4Reader.
* <p>
* The metadata-extractor reader eagerly does {@code new byte[(int) boxSize - 8]}
* for every box a handler accepts, with {@code boxSize} attacker-controlled and
* capped only at {@code Integer.MAX_VALUE} (~2GB), and {@code StreamReader.getBytes}
* allocates before checking how much data is actually present. A single crafted
* box header therefore forces a multi-GB allocation. This reader is identical to
* the library's box walk except that an accepted box whose payload exceeds
* {@code maxBoxSize} is skipped (a lazy stream advance, no allocation) instead of
* being read. Boxes the handler does not accept were already skipped by the
* library, so this only bounds the boxes we opt into. See TIKA-4812.
*/
final class TikaMp4Reader {
private TikaMp4Reader() {
}
//MP4 containers nest (moov/trak/mdia/minf/stbl/udta/meta); cap the recursion so a
//crafted chain of nested container headers cannot overflow the stack (an uncaught
//Error, caught by neither the IOException handler below nor CompositeParser). Real
//files nest well under this.
private static final int MAX_BOX_DEPTH = 100;
/**
* @param inputLength total input length in bytes, or -1 if unknown. When known, a box
* that declares more payload than the input holds is skipped rather
* than allocated (StreamReader.getBytes allocates before reading).
*/
static void extract(InputStream inputStream, Mp4BoxHandler handler, long maxBoxSize,
long inputLength) {
StreamReader reader = new StreamReader(inputStream);
reader.setMotorolaByteOrder(true);
processBoxes(reader, -1, handler, new Mp4Context(), maxBoxSize, inputLength, 0);
}
private static void processBoxes(StreamReader reader, long atomEnd, Mp4Handler<?> handler,
Mp4Context context, long maxBoxSize, long inputLength,
int depth) {
if (depth > MAX_BOX_DEPTH) {
handler.addError("MP4 box nesting exceeds the maximum depth of " + MAX_BOX_DEPTH);
return;
}
try {
while (atomEnd == -1 || reader.getPosition() < atomEnd) {
long boxSize = reader.getUInt32();
String boxType = reader.getString(4);
//4 bytes size + 4 bytes type, plus 8 more when a 64-bit largesize follows
int headerSize = boxSize == 1 ? 16 : 8;
if (headerSize == 16) {
boxSize = reader.getInt64();
}
if (boxSize > Integer.MAX_VALUE) {
handler.addError("Box size too large.");
break;
}
if (boxSize < headerSize) {
handler.addError("Box size too small.");
break;
}
long payloadLength = boxSize - headerSize;
if (acceptContainer(handler, boxType)) {
processBoxes(reader, reader.getPosition() + payloadLength,
processBox(handler, boxType, null, boxSize, context), context,
maxBoxSize, inputLength, depth + 1);
} else if (acceptBox(handler, boxType)) {
//StreamReader.getBytes allocates the whole payload up front, so skip
//(a lazy stream advance) any box over the cap, or one that claims more
//than the input holds, instead of allocating it. Skip-and-continue is
//deliberate: unlike the TikaMemoryLimitException other parsers throw,
//this keeps the remaining boxes' metadata; the skip is recorded as a
//warning via the directory's error list.
boolean tooLarge = payloadLength > maxBoxSize;
boolean beyondInput = inputLength >= 0
&& reader.getPosition() + payloadLength > inputLength;
if (tooLarge || beyondInput) {
handler.addError("MP4 box '" + boxType + "' payload (" + payloadLength
+ " bytes) exceeds the "
+ (tooLarge ? "maximum of " + maxBoxSize + " bytes" : "input size")
+ "; skipping.");
reader.skip(payloadLength);
} else {
handler = processBox(handler, boxType,
reader.getBytes((int) payloadLength), boxSize, context);
}
} else {
reader.skip(payloadLength);
}
}
} catch (IOException e) {
handler.addError(e.getMessage() == null ? "IOException reading MP4 boxes"
: e.getMessage());
}
}
//the box walk holds handlers as Mp4Handler, whose accept/process methods are
//protected; every concrete handler in play (Mp4BoxHandler-rooted, or an
//Mp4MediaHandler track handler swapped in on 'hdlr') widens them to public,
//so dispatch through whichever of the two families the instance belongs to.
//A container's handler is obtained with processBox(type, null, ...), which is
//exactly what the library's protected processContainer does.
private static boolean acceptContainer(Mp4Handler<?> handler, String type) {
return handler instanceof Mp4BoxHandler
? ((Mp4BoxHandler) handler).shouldAcceptContainer(type)
: ((Mp4MediaHandler<?>) handler).shouldAcceptContainer(type);
}
private static boolean acceptBox(Mp4Handler<?> handler, String type) {
return handler instanceof Mp4BoxHandler
? ((Mp4BoxHandler) handler).shouldAcceptBox(type)
: ((Mp4MediaHandler<?>) handler).shouldAcceptBox(type);
}
private static Mp4Handler<?> processBox(Mp4Handler<?> handler, String type, byte[] payload,
long boxSize, Mp4Context context) throws IOException {
return handler instanceof Mp4BoxHandler
? ((Mp4BoxHandler) handler).processBox(type, payload, boxSize, context)
: ((Mp4MediaHandler<?>) handler).processBox(type, payload, boxSize, context);
}
}