[
https://issues.apache.org/jira/browse/TIKA-4936?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18119957#comment-18119957
]
ASF GitHub Bot commented on TIKA-4936:
--------------------------------------
Copilot commented on code in PR #3263:
URL: https://github.com/apache/tika/pull/3263#discussion_r4118949696
##########
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-code-module/src/main/java/org/apache/tika/parser/executable/ExecutableParser.java:
##########
@@ -255,6 +272,15 @@ public void parsePE(XHTMLContentHandler xhtml, Metadata
metadata, InputStream ti
metadata.set(MACHINE_TYPE, MACHINE_UNKNOWN);
break;
}
+
+ if (extractIcons) {
+ try {
+ PEIconExtractor.extract(tis, sizeOptHdrs, numSectors, xhtml,
context);
+ } catch (IOException | TikaException | RuntimeException e) {
+ // A broken resource section must not cost us the metadata
above
+ EmbeddedDocumentUtil.recordEmbeddedStreamException(e,
metadata, context);
+ }
Review Comment:
`shouldParseEmbedded()` is allowed to throw `EmbeddedLimitReachedException`
when throw-on-limit is configured, but this catches it as a generic
`RuntimeException` and records it as an embedded-stream failure. That
suppresses the configured max-depth/max-count/deadline stop; rethrow this
exception before the broad catch, as `CompositeParser` does.
This issue also appears on line 279 of the same file.
##########
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-code-module/src/main/java/org/apache/tika/parser/executable/PEIconExtractor.java:
##########
@@ -0,0 +1,457 @@
+/*
+ * 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.executable;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.io.IOUtils;
+import org.xml.sax.SAXException;
+
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.extractor.EmbeddedDocumentUtil;
+import org.apache.tika.io.EndianUtils;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.HttpHeaders;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.sax.XHTMLContentHandler;
+
+/**
+ * Extracts the icons of a PE file (EXE/DLL) from its resource section and
+ * hands each icon group to the {@link EmbeddedDocumentExtractor} as a
+ * standalone <code>.ico</code> file.
+ * <p>
+ * Windows stores an icon as a group resource ({@code RT_GROUP_ICON}) that
+ * lists the individual images, which are stored as {@code RT_ICON} resources.
+ * The {@code .ico} file format is nearly identical to the group resource;
+ * the only difference is that the group refers to its images by resource id
+ * whereas the file refers to them by file offset. This class rebuilds the
+ * file from the two resource types.
+ * <p>
+ * The extractor reads the stream strictly forward, so it works on
+ * non-seekable input. It is written to survive truncated or malicious
+ * files: every offset is bounds checked, the resource tree depth and
+ * entry count are capped and cycles in the tree are detected.
+ */
+class PEIconExtractor {
+
+ static final String ICON_MIME_TYPE = "image/vnd.microsoft.icon";
+
+ private static final int RT_ICON = 3;
+ private static final int RT_GROUP_ICON = 14;
+
+ private static final int IMAGE_DIRECTORY_ENTRY_RESOURCE = 2;
+ private static final int PE32_MAGIC = 0x10b;
+ private static final int PE32PLUS_MAGIC = 0x20b;
+ private static final int SECTION_HEADER_SIZE = 40;
+ private static final int RESOURCE_DIRECTORY_SIZE = 16;
+ private static final int RESOURCE_DIRECTORY_ENTRY_SIZE = 8;
+ private static final int RESOURCE_DATA_ENTRY_SIZE = 16;
+ private static final int GRP_ICON_DIR_ENTRY_SIZE = 14;
+ private static final int ICON_DIR_ENTRY_SIZE = 16;
+ private static final long HIGH_BIT = 0x80000000L;
+
+ // Sanity limits for hostile input
+ private static final int MAX_SECTIONS = 96; // the PE spec's own limit
+ private static final int MAX_RESOURCE_SECTION_SIZE = 64 * 1024 * 1024;
+ private static final int MAX_RESOURCE_TREE_DEPTH = 3; // type / name /
language
+ private static final int MAX_RESOURCES = 10000;
+ private static final int MAX_RESOURCE_NAME_LENGTH = 256;
+ private static final int MAX_ICONS_PER_GROUP = 256;
+
+ private PEIconExtractor() {
+ }
+
+ /**
+ * Continues reading the PE file directly after the COFF file header and
+ * emits every icon group as an embedded document.
+ *
+ * @param stream the input positioned right after the 24 byte COFF
header
+ * @param sizeOptHdrs the SizeOfOptionalHeader field of the COFF header
+ * @param numSections the NumberOfSections field of the COFF header
+ */
+ static void extract(TikaInputStream stream, int sizeOptHdrs, int
numSections,
+ XHTMLContentHandler xhtml, ParseContext context)
+ throws IOException, SAXException, TikaException {
+ if (numSections <= 0 || numSections > MAX_SECTIONS) {
+ return;
+ }
+ // The optional header holds the data directories, of which we
+ // need the one pointing at the resource tree
+ byte[] optHdr = new byte[sizeOptHdrs];
+ IOUtils.readFully(stream, optHdr);
+ int dataDirOffset;
+ switch (sizeOptHdrs >= 2 ? EndianUtils.getUShortLE(optHdr, 0) : 0) {
+ case PE32_MAGIC:
+ dataDirOffset = 96;
+ break;
+ case PE32PLUS_MAGIC:
+ dataDirOffset = 112;
+ break;
+ default:
+ return;
+ }
+ int rsrcEntry = dataDirOffset + IMAGE_DIRECTORY_ENTRY_RESOURCE * 8;
+ if (rsrcEntry + 8 > sizeOptHdrs) {
+ return;
+ }
+ long numDataDirs = getUIntLE(optHdr, dataDirOffset - 4);
+ if (numDataDirs <= IMAGE_DIRECTORY_ENTRY_RESOURCE) {
+ return;
+ }
+ long rsrcRva = getUIntLE(optHdr, rsrcEntry);
+ long rsrcSize = getUIntLE(optHdr, rsrcEntry + 4);
+ if (rsrcRva == 0 || rsrcSize == 0) {
+ return;
+ }
+
+ // The section table tells us where in the file the resource RVA lives
+ byte[] sections = new byte[numSections * SECTION_HEADER_SIZE];
+ IOUtils.readFully(stream, sections);
+ long sectionVa = -1;
+ long sectionRawPtr = -1;
+ long sectionRawSize = -1;
+ for (int i = 0; i < numSections; i++) {
+ int off = i * SECTION_HEADER_SIZE;
+ long va = getUIntLE(sections, off + 12);
+ long rawSize = getUIntLE(sections, off + 16);
+ long rawPtr = getUIntLE(sections, off + 20);
+ if (rsrcRva >= va && rsrcRva < va + rawSize) {
+ sectionVa = va;
+ sectionRawPtr = rawPtr;
+ sectionRawSize = rawSize;
+ break;
+ }
+ }
+ if (sectionVa < 0 || sectionRawSize > MAX_RESOURCE_SECTION_SIZE) {
+ return;
+ }
+
+ // Everything read so far: DOS header up to and including the section
table
+ long position = stream.getPosition();
+ if (sectionRawPtr < position) {
+ return;
+ }
+ IOUtils.skipFully(stream, sectionRawPtr - position);
+ // A truncated file simply yields a shorter section; the bounds checks
+ // below deal with that
+ byte[] rsrc = new byte[(int) sectionRawSize];
+ int read = IOUtils.read(stream, rsrc);
+ if (read < rsrc.length) {
+ rsrc = Arrays.copyOf(rsrc, read);
+ }
+
+ Resources resources = new Resources(rsrc, sectionVa);
+ readDirectory(resources, rsrcRva - sectionVa, 0, new HashSet<>(),
null);
+ emitIcons(resources, xhtml, context);
+ }
+
+ /**
+ * Walks the three level resource tree (type / name / language) and
+ * collects every icon and icon group.
+ *
+ * @param parent the entry that led to this directory, or null for the root
+ */
+ private static void readDirectory(Resources resources, long dirOffset, int
depth,
+ Set<Long> visited, Resource parent)
throws TikaException {
+ if (depth >= MAX_RESOURCE_TREE_DEPTH || !visited.add(dirOffset)) {
+ return;
+ }
+ byte[] rsrc = resources.rsrc;
+ int dir = toIndex(dirOffset, RESOURCE_DIRECTORY_SIZE, rsrc);
+ if (dir < 0) {
+ return;
+ }
+ int numEntries = EndianUtils.getUShortLE(rsrc, dir + 12) +
+ EndianUtils.getUShortLE(rsrc, dir + 14);
+ for (int i = 0; i < numEntries; i++) {
+ int entry = toIndex(dirOffset + RESOURCE_DIRECTORY_SIZE +
+ (long) i * RESOURCE_DIRECTORY_ENTRY_SIZE,
RESOURCE_DIRECTORY_ENTRY_SIZE, rsrc);
+ if (entry < 0) {
+ return;
+ }
+ long nameField = getUIntLE(rsrc, entry);
+ long dataField = getUIntLE(rsrc, entry + 4);
+
+ // Only icons are interesting; prune everything else at the type
level
+ if (depth == 0 && nameField != RT_ICON && nameField !=
RT_GROUP_ICON) {
+ continue;
+ }
+ Resource current = parent == null ? new Resource() : new
Resource(parent);
+ if ((nameField & HIGH_BIT) != 0) {
+ String name = readName(rsrc, nameField & ~HIGH_BIT);
+ if (name == null) {
+ continue;
+ }
+ current.setLevel(depth, 0, name);
+ } else {
+ current.setLevel(depth, (int) (nameField & 0xffff), null);
+ }
+
+ if ((dataField & HIGH_BIT) != 0) {
+ readDirectory(resources, dataField & ~HIGH_BIT, depth + 1,
visited, current);
Review Comment:
PE resource-directory offsets are relative to the resource-directory base
(`rsrcRva`), while `rsrc` is loaded from the section start (`sectionVa`). This
recursive call uses `dataField` without adding `rsrcRva - sectionVa`, so a
valid PE whose resource directory is not at the section start will resolve
child directories (and subsequently names/data entries) at the wrong offsets
and miss its icons. Carry the resource-base offset through the walk or
normalize every directory-relative offset before indexing.
##########
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-code-module/src/main/java/org/apache/tika/parser/executable/PEIconExtractor.java:
##########
@@ -0,0 +1,457 @@
+/*
+ * 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.executable;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.io.IOUtils;
+import org.xml.sax.SAXException;
+
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.extractor.EmbeddedDocumentUtil;
+import org.apache.tika.io.EndianUtils;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.HttpHeaders;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.sax.XHTMLContentHandler;
+
+/**
+ * Extracts the icons of a PE file (EXE/DLL) from its resource section and
+ * hands each icon group to the {@link EmbeddedDocumentExtractor} as a
+ * standalone <code>.ico</code> file.
+ * <p>
+ * Windows stores an icon as a group resource ({@code RT_GROUP_ICON}) that
+ * lists the individual images, which are stored as {@code RT_ICON} resources.
+ * The {@code .ico} file format is nearly identical to the group resource;
+ * the only difference is that the group refers to its images by resource id
+ * whereas the file refers to them by file offset. This class rebuilds the
+ * file from the two resource types.
+ * <p>
+ * The extractor reads the stream strictly forward, so it works on
+ * non-seekable input. It is written to survive truncated or malicious
+ * files: every offset is bounds checked, the resource tree depth and
+ * entry count are capped and cycles in the tree are detected.
+ */
+class PEIconExtractor {
+
+ static final String ICON_MIME_TYPE = "image/vnd.microsoft.icon";
+
+ private static final int RT_ICON = 3;
+ private static final int RT_GROUP_ICON = 14;
+
+ private static final int IMAGE_DIRECTORY_ENTRY_RESOURCE = 2;
+ private static final int PE32_MAGIC = 0x10b;
+ private static final int PE32PLUS_MAGIC = 0x20b;
+ private static final int SECTION_HEADER_SIZE = 40;
+ private static final int RESOURCE_DIRECTORY_SIZE = 16;
+ private static final int RESOURCE_DIRECTORY_ENTRY_SIZE = 8;
+ private static final int RESOURCE_DATA_ENTRY_SIZE = 16;
+ private static final int GRP_ICON_DIR_ENTRY_SIZE = 14;
+ private static final int ICON_DIR_ENTRY_SIZE = 16;
+ private static final long HIGH_BIT = 0x80000000L;
+
+ // Sanity limits for hostile input
+ private static final int MAX_SECTIONS = 96; // the PE spec's own limit
+ private static final int MAX_RESOURCE_SECTION_SIZE = 64 * 1024 * 1024;
+ private static final int MAX_RESOURCE_TREE_DEPTH = 3; // type / name /
language
+ private static final int MAX_RESOURCES = 10000;
+ private static final int MAX_RESOURCE_NAME_LENGTH = 256;
+ private static final int MAX_ICONS_PER_GROUP = 256;
+
+ private PEIconExtractor() {
+ }
+
+ /**
+ * Continues reading the PE file directly after the COFF file header and
+ * emits every icon group as an embedded document.
+ *
+ * @param stream the input positioned right after the 24 byte COFF
header
+ * @param sizeOptHdrs the SizeOfOptionalHeader field of the COFF header
+ * @param numSections the NumberOfSections field of the COFF header
+ */
+ static void extract(TikaInputStream stream, int sizeOptHdrs, int
numSections,
+ XHTMLContentHandler xhtml, ParseContext context)
+ throws IOException, SAXException, TikaException {
+ if (numSections <= 0 || numSections > MAX_SECTIONS) {
+ return;
+ }
+ // The optional header holds the data directories, of which we
+ // need the one pointing at the resource tree
+ byte[] optHdr = new byte[sizeOptHdrs];
+ IOUtils.readFully(stream, optHdr);
+ int dataDirOffset;
+ switch (sizeOptHdrs >= 2 ? EndianUtils.getUShortLE(optHdr, 0) : 0) {
+ case PE32_MAGIC:
+ dataDirOffset = 96;
+ break;
+ case PE32PLUS_MAGIC:
+ dataDirOffset = 112;
+ break;
+ default:
+ return;
+ }
+ int rsrcEntry = dataDirOffset + IMAGE_DIRECTORY_ENTRY_RESOURCE * 8;
+ if (rsrcEntry + 8 > sizeOptHdrs) {
+ return;
+ }
+ long numDataDirs = getUIntLE(optHdr, dataDirOffset - 4);
+ if (numDataDirs <= IMAGE_DIRECTORY_ENTRY_RESOURCE) {
+ return;
+ }
+ long rsrcRva = getUIntLE(optHdr, rsrcEntry);
+ long rsrcSize = getUIntLE(optHdr, rsrcEntry + 4);
+ if (rsrcRva == 0 || rsrcSize == 0) {
+ return;
+ }
+
+ // The section table tells us where in the file the resource RVA lives
+ byte[] sections = new byte[numSections * SECTION_HEADER_SIZE];
+ IOUtils.readFully(stream, sections);
+ long sectionVa = -1;
+ long sectionRawPtr = -1;
+ long sectionRawSize = -1;
+ for (int i = 0; i < numSections; i++) {
+ int off = i * SECTION_HEADER_SIZE;
+ long va = getUIntLE(sections, off + 12);
+ long rawSize = getUIntLE(sections, off + 16);
+ long rawPtr = getUIntLE(sections, off + 20);
+ if (rsrcRva >= va && rsrcRva < va + rawSize) {
+ sectionVa = va;
+ sectionRawPtr = rawPtr;
+ sectionRawSize = rawSize;
+ break;
+ }
+ }
+ if (sectionVa < 0 || sectionRawSize > MAX_RESOURCE_SECTION_SIZE) {
+ return;
+ }
+
+ // Everything read so far: DOS header up to and including the section
table
+ long position = stream.getPosition();
+ if (sectionRawPtr < position) {
+ return;
+ }
+ IOUtils.skipFully(stream, sectionRawPtr - position);
+ // A truncated file simply yields a shorter section; the bounds checks
+ // below deal with that
+ byte[] rsrc = new byte[(int) sectionRawSize];
Review Comment:
The resource-directory size from the PE data directory is available as
`rsrcSize`, but this allocates and reads the entire section's `SizeOfRawData`.
A PE can have a large aligned/padded or attacker-controlled raw section while
declaring a tiny resource directory, so every default-enabled parse can
allocate up to the 64 MiB cap unnecessarily. Bound the buffer by the resource
directory size as well as the raw section size.
##########
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-code-module/src/main/java/org/apache/tika/parser/executable/ExecutableParser.java:
##########
@@ -112,10 +127,12 @@ public void parse(TikaInputStream tis, ContentHandler
handler, Metadata metadata
}
/**
- * Parses a DOS or Windows PE file
+ * Parses a DOS or Windows PE file, extracting metadata and, if
+ * {@link #isExtractIcons()} is set, the icon resources as embedded
documents.
*/
- public void parsePE(XHTMLContentHandler xhtml, Metadata metadata,
InputStream tis,
- byte[] first4) throws TikaException, IOException {
+ public void parsePE(XHTMLContentHandler xhtml, Metadata metadata,
TikaInputStream tis,
+ byte[] first4, ParseContext context)
+ throws TikaException, IOException, SAXException {
Review Comment:
`parsePE` was a public method accepting `(XHTMLContentHandler, Metadata,
InputStream, byte[])`; replacing it with this five-argument
`TikaInputStream`/`ParseContext` signature breaks source and binary clients
even though the repository has no remaining callers. Keep a compatibility
overload (and define its icon-extraction behavior) rather than removing the
existing public API.
##########
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-code-module/src/main/java/org/apache/tika/parser/executable/PEIconExtractor.java:
##########
@@ -0,0 +1,457 @@
+/*
+ * 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.executable;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.io.IOUtils;
+import org.xml.sax.SAXException;
+
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.extractor.EmbeddedDocumentUtil;
+import org.apache.tika.io.EndianUtils;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.HttpHeaders;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.sax.XHTMLContentHandler;
+
+/**
+ * Extracts the icons of a PE file (EXE/DLL) from its resource section and
+ * hands each icon group to the {@link EmbeddedDocumentExtractor} as a
+ * standalone <code>.ico</code> file.
+ * <p>
+ * Windows stores an icon as a group resource ({@code RT_GROUP_ICON}) that
+ * lists the individual images, which are stored as {@code RT_ICON} resources.
+ * The {@code .ico} file format is nearly identical to the group resource;
+ * the only difference is that the group refers to its images by resource id
+ * whereas the file refers to them by file offset. This class rebuilds the
+ * file from the two resource types.
+ * <p>
+ * The extractor reads the stream strictly forward, so it works on
+ * non-seekable input. It is written to survive truncated or malicious
+ * files: every offset is bounds checked, the resource tree depth and
+ * entry count are capped and cycles in the tree are detected.
+ */
+class PEIconExtractor {
+
+ static final String ICON_MIME_TYPE = "image/vnd.microsoft.icon";
+
+ private static final int RT_ICON = 3;
+ private static final int RT_GROUP_ICON = 14;
+
+ private static final int IMAGE_DIRECTORY_ENTRY_RESOURCE = 2;
+ private static final int PE32_MAGIC = 0x10b;
+ private static final int PE32PLUS_MAGIC = 0x20b;
+ private static final int SECTION_HEADER_SIZE = 40;
+ private static final int RESOURCE_DIRECTORY_SIZE = 16;
+ private static final int RESOURCE_DIRECTORY_ENTRY_SIZE = 8;
+ private static final int RESOURCE_DATA_ENTRY_SIZE = 16;
+ private static final int GRP_ICON_DIR_ENTRY_SIZE = 14;
+ private static final int ICON_DIR_ENTRY_SIZE = 16;
+ private static final long HIGH_BIT = 0x80000000L;
+
+ // Sanity limits for hostile input
+ private static final int MAX_SECTIONS = 96; // the PE spec's own limit
+ private static final int MAX_RESOURCE_SECTION_SIZE = 64 * 1024 * 1024;
+ private static final int MAX_RESOURCE_TREE_DEPTH = 3; // type / name /
language
+ private static final int MAX_RESOURCES = 10000;
+ private static final int MAX_RESOURCE_NAME_LENGTH = 256;
+ private static final int MAX_ICONS_PER_GROUP = 256;
+
+ private PEIconExtractor() {
+ }
+
+ /**
+ * Continues reading the PE file directly after the COFF file header and
+ * emits every icon group as an embedded document.
+ *
+ * @param stream the input positioned right after the 24 byte COFF
header
+ * @param sizeOptHdrs the SizeOfOptionalHeader field of the COFF header
+ * @param numSections the NumberOfSections field of the COFF header
+ */
+ static void extract(TikaInputStream stream, int sizeOptHdrs, int
numSections,
+ XHTMLContentHandler xhtml, ParseContext context)
+ throws IOException, SAXException, TikaException {
+ if (numSections <= 0 || numSections > MAX_SECTIONS) {
+ return;
+ }
+ // The optional header holds the data directories, of which we
+ // need the one pointing at the resource tree
+ byte[] optHdr = new byte[sizeOptHdrs];
+ IOUtils.readFully(stream, optHdr);
+ int dataDirOffset;
+ switch (sizeOptHdrs >= 2 ? EndianUtils.getUShortLE(optHdr, 0) : 0) {
+ case PE32_MAGIC:
+ dataDirOffset = 96;
+ break;
+ case PE32PLUS_MAGIC:
+ dataDirOffset = 112;
+ break;
+ default:
+ return;
+ }
+ int rsrcEntry = dataDirOffset + IMAGE_DIRECTORY_ENTRY_RESOURCE * 8;
+ if (rsrcEntry + 8 > sizeOptHdrs) {
+ return;
+ }
+ long numDataDirs = getUIntLE(optHdr, dataDirOffset - 4);
+ if (numDataDirs <= IMAGE_DIRECTORY_ENTRY_RESOURCE) {
+ return;
+ }
+ long rsrcRva = getUIntLE(optHdr, rsrcEntry);
+ long rsrcSize = getUIntLE(optHdr, rsrcEntry + 4);
+ if (rsrcRva == 0 || rsrcSize == 0) {
+ return;
+ }
+
+ // The section table tells us where in the file the resource RVA lives
+ byte[] sections = new byte[numSections * SECTION_HEADER_SIZE];
+ IOUtils.readFully(stream, sections);
+ long sectionVa = -1;
+ long sectionRawPtr = -1;
+ long sectionRawSize = -1;
+ for (int i = 0; i < numSections; i++) {
+ int off = i * SECTION_HEADER_SIZE;
+ long va = getUIntLE(sections, off + 12);
+ long rawSize = getUIntLE(sections, off + 16);
+ long rawPtr = getUIntLE(sections, off + 20);
+ if (rsrcRva >= va && rsrcRva < va + rawSize) {
+ sectionVa = va;
+ sectionRawPtr = rawPtr;
+ sectionRawSize = rawSize;
+ break;
+ }
+ }
+ if (sectionVa < 0 || sectionRawSize > MAX_RESOURCE_SECTION_SIZE) {
+ return;
+ }
+
+ // Everything read so far: DOS header up to and including the section
table
+ long position = stream.getPosition();
+ if (sectionRawPtr < position) {
+ return;
+ }
+ IOUtils.skipFully(stream, sectionRawPtr - position);
+ // A truncated file simply yields a shorter section; the bounds checks
+ // below deal with that
+ byte[] rsrc = new byte[(int) sectionRawSize];
+ int read = IOUtils.read(stream, rsrc);
+ if (read < rsrc.length) {
+ rsrc = Arrays.copyOf(rsrc, read);
+ }
+
+ Resources resources = new Resources(rsrc, sectionVa);
+ readDirectory(resources, rsrcRva - sectionVa, 0, new HashSet<>(),
null);
+ emitIcons(resources, xhtml, context);
+ }
+
+ /**
+ * Walks the three level resource tree (type / name / language) and
+ * collects every icon and icon group.
+ *
+ * @param parent the entry that led to this directory, or null for the root
+ */
+ private static void readDirectory(Resources resources, long dirOffset, int
depth,
+ Set<Long> visited, Resource parent)
throws TikaException {
+ if (depth >= MAX_RESOURCE_TREE_DEPTH || !visited.add(dirOffset)) {
+ return;
+ }
+ byte[] rsrc = resources.rsrc;
+ int dir = toIndex(dirOffset, RESOURCE_DIRECTORY_SIZE, rsrc);
+ if (dir < 0) {
+ return;
+ }
+ int numEntries = EndianUtils.getUShortLE(rsrc, dir + 12) +
+ EndianUtils.getUShortLE(rsrc, dir + 14);
+ for (int i = 0; i < numEntries; i++) {
+ int entry = toIndex(dirOffset + RESOURCE_DIRECTORY_SIZE +
+ (long) i * RESOURCE_DIRECTORY_ENTRY_SIZE,
RESOURCE_DIRECTORY_ENTRY_SIZE, rsrc);
+ if (entry < 0) {
+ return;
+ }
+ long nameField = getUIntLE(rsrc, entry);
+ long dataField = getUIntLE(rsrc, entry + 4);
+
+ // Only icons are interesting; prune everything else at the type
level
+ if (depth == 0 && nameField != RT_ICON && nameField !=
RT_GROUP_ICON) {
+ continue;
+ }
+ Resource current = parent == null ? new Resource() : new
Resource(parent);
+ if ((nameField & HIGH_BIT) != 0) {
+ String name = readName(rsrc, nameField & ~HIGH_BIT);
+ if (name == null) {
+ continue;
+ }
+ current.setLevel(depth, 0, name);
+ } else {
+ current.setLevel(depth, (int) (nameField & 0xffff), null);
+ }
+
+ if ((dataField & HIGH_BIT) != 0) {
+ readDirectory(resources, dataField & ~HIGH_BIT, depth + 1,
visited, current);
+ } else if (depth == MAX_RESOURCE_TREE_DEPTH - 1) {
+ readDataEntry(resources, dataField, current);
+ }
+ }
+ }
+
+ private static void readDataEntry(Resources resources, long offset,
Resource resource)
+ throws TikaException {
+ byte[] rsrc = resources.rsrc;
+ int entry = toIndex(offset, RESOURCE_DATA_ENTRY_SIZE, rsrc);
+ if (entry < 0) {
+ return;
+ }
+ long dataRva = getUIntLE(rsrc, entry);
+ long size = getUIntLE(rsrc, entry + 4);
+ // Resource data normally lives in the same section as the tree; if
+ // it doesn't we can't reach it with a forward-only read
+ int dataIdx = toIndex(dataRva - resources.sectionVa, size, rsrc);
+ if (dataIdx < 0) {
+ return;
+ }
+ if (resources.count++ >= MAX_RESOURCES) {
+ throw new TikaException("Too many resources in PE file");
+ }
+ resource.offset = dataIdx;
+ resource.size = (int) size;
+ if (resource.type == RT_ICON) {
+ // named icons can't be referenced from a group, which uses
numeric ids
+ if (resource.name == null) {
+ resources.icons.computeIfAbsent(resource.id, k -> new
ArrayList<>()).add(resource);
+ }
+ } else {
+ resources.groups.add(resource);
+ }
+ }
+
+ private static String readName(byte[] rsrc, long offset) {
+ int idx = toIndex(offset, 2, rsrc);
+ if (idx < 0) {
+ return null;
+ }
+ int length = EndianUtils.getUShortLE(rsrc, idx);
+ if (length == 0 || length > MAX_RESOURCE_NAME_LENGTH ||
+ toIndex(offset + 2, (long) length * 2, rsrc) < 0) {
+ return null;
+ }
+ return new String(rsrc, idx + 2, length * 2,
StandardCharsets.UTF_16LE);
+ }
+
+ /**
+ * Rebuilds an <code>.ico</code> file for every icon group and passes it on
+ * as an embedded document. The first group in resource order is the one
+ * Windows shows for the file itself.
+ */
+ private static void emitIcons(Resources resources, XHTMLContentHandler
xhtml,
+ ParseContext context) throws IOException,
SAXException {
+ if (resources.groups.isEmpty()) {
+ return;
+ }
+ EmbeddedDocumentExtractor extractor =
+ EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context);
+ // Count the language variants per group so that the names stay unique
+ Map<String, Integer> languagesPerGroup = new HashMap<>();
+ for (Resource group : resources.groups) {
+ languagesPerGroup.merge(group.displayName(), 1, Integer::sum);
+ }
+ boolean first = true;
+ for (Resource group : resources.groups) {
+ byte[] ico = buildIco(group, resources);
+ if (ico == null) {
+ continue;
+ }
+ String name = "icon_" + group.displayName();
+ if (languagesPerGroup.get(group.displayName()) > 1) {
+ name += "_" + group.language;
+ }
+ Metadata metadata = new Metadata();
+ metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, name + ".ico");
+ metadata.set(HttpHeaders.CONTENT_TYPE, ICON_MIME_TYPE);
+ metadata.set(TikaCoreProperties.EMBEDDED_RELATIONSHIP_ID,
+ RT_GROUP_ICON + "/" + group.displayName() + "/" +
group.language);
+ metadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, first ?
+
TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString() :
+
TikaCoreProperties.EmbeddedResourceType.ATTACHMENT.toString());
+ first = false;
+ if (!extractor.shouldParseEmbedded(metadata, context)) {
+ continue;
+ }
+ try (TikaInputStream tis = TikaInputStream.get(ico)) {
+ extractor.parseEmbedded(tis, xhtml, metadata, context, true);
+ }
+ }
+ }
+
+ /**
+ * Converts a {@code GRPICONDIR} plus its {@code RT_ICON} images into an
+ * {@code ICONDIR} based <code>.ico</code> file.
+ *
+ * @return the file, or null if the group is unusable
+ */
+ private static byte[] buildIco(Resource group, Resources resources) {
+ byte[] rsrc = resources.rsrc;
+ int g = group.offset;
+ if (group.size < 6 || EndianUtils.getUShortLE(rsrc, g) != 0 ||
+ EndianUtils.getUShortLE(rsrc, g + 2) != 1) {
+ return null;
+ }
+ int count = EndianUtils.getUShortLE(rsrc, g + 4);
+ if (count == 0 || count > MAX_ICONS_PER_GROUP ||
+ 6 + count * GRP_ICON_DIR_ENTRY_SIZE > group.size) {
+ return null;
+ }
+ List<Resource> images = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ int e = g + 6 + i * GRP_ICON_DIR_ENTRY_SIZE;
+ int id = EndianUtils.getUShortLE(rsrc, e + 12);
+ Resource image = resources.findIcon(id, group.language);
+ if (image == null) {
+ return null;
+ }
+ images.add(image);
+ }
+
+ ByteArrayOutputStream ico = new ByteArrayOutputStream();
+ // ICONDIR: reserved, type, count - identical to the GRPICONDIR
+ ico.write(rsrc, g, 6);
+ int imageOffset = 6 + count * ICON_DIR_ENTRY_SIZE;
+ for (int i = 0; i < count; i++) {
+ int e = g + 6 + i * GRP_ICON_DIR_ENTRY_SIZE;
+ Resource image = images.get(i);
+ // width, height, colours, reserved, planes and bit count are
shared
+ ico.write(rsrc, e, 8);
+ // the group's BytesInRes may disagree with the actual resource;
trust the resource
+ writeIntLE(ico, image.size);
+ writeIntLE(ico, imageOffset);
+ imageOffset += image.size;
+ }
+ for (Resource image : images) {
+ ico.write(rsrc, image.offset, image.size);
Review Comment:
The per-group output is not bounded: a group may reference up to 256
`RT_ICON` entries, and each entry may be as large as the 64 MiB resource
section (the same image can be referenced repeatedly). The loop therefore can
try to build a multi-gigabyte `byte[]`/`ByteArrayOutputStream`, overflow the
`int` file offsets, or exhaust the heap on a hostile but bounds-valid PE.
Reject groups whose reconstructed size exceeds a separate output limit before
allocating/writing the icon.
> Extract icons from PE executables (EXE/DLL) as embedded documents
> -----------------------------------------------------------------
>
> Key: TIKA-4936
> URL: https://issues.apache.org/jira/browse/TIKA-4936
> Project: Tika
> Issue Type: New Feature
> Environment:
>
>
>
> Reporter: Dominik Schmidt
> Priority: Major
>
> h3. Background
> {{ExecutableParser}} currently only reads the COFF file header of PE files
> (EXE/DLL) and emits basic metadata (machine type, architecture bits,
> endianness, created date). The resource section ({{.rsrc}}) is not parsed, so
> resources such as the application icon are not accessible via Tika.
> h3. Proposal
> Parse the PE resource directory and emit each icon group as an embedded
> document:
> * Parse optional header, data directories and section table to locate the
> resource directory (RVA → file offset).
> * Walk the resource tree (type → name/ID → language).
> * For each {{RT_GROUP_ICON}} (type 14), reconstruct a standalone {{.ico}}
> file from the {{GRPICONDIR}} and the referenced {{RT_ICON}} (type 3) entries:
> write an {{ICONDIR}} header and replace the 2-byte resource IDs ({{nID}})
> with 4-byte image offsets.
> * Pass each reconstructed file to the {{EmbeddedDocumentExtractor}} with:
> ** {{Content-Type}}: {{image/vnd.microsoft.icon}}
> ** {{resourceName}}: e.g. {{icon_<id-or-name>.ico}}
> ** {{embeddedResourceType}}: {{THUMBNAIL}} for the first icon group (the icon
> shown by Windows Explorer), {{ATTACHMENT}} for all others
> ** the resource language ID
> Single {{RT_ICON}} entries are intentionally not emitted on their own:
> BMP-based entries are not valid standalone images (no {{BITMAPFILEHEADER}},
> double height for the AND mask), and emitting them would duplicate the group
> data.
> h3. Robustness
> The parser must handle malformed or malicious binaries gracefully:
> * bound the resource tree depth (normally 3 levels) and the number of entries
> * validate all offsets and sizes against the file size
> * guard against cycles in the resource directory
> * failures while extracting resources must not break the existing metadata
> extraction
> h3. Out of scope (possible follow-ups)
> * {{RT_GROUP_CURSOR}}/{{RT_CURSOR}} → {{.cur}}
> * {{RT_MANIFEST}} as an embedded XML document
> * {{VS_VERSIONINFO}} (product name, file version, company) as metadata
> h3. Acceptance criteria
> * Icons of 32- and 64-bit EXE and DLL test files are extracted as valid
> {{.ico}} files that are detected as {{image/vnd.microsoft.icon}}.
> * Icon groups containing both PNG- and BMP-encoded entries are supported.
> * Files without a resource section, or without icons, are parsed as before,
> with no embedded documents.
> * Truncated or corrupted resource sections do not throw and still yield the
> existing metadata.
> * Unit tests use small, license-compatible test files.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)