Skip to content

Commit b2495b0

Browse files
committed
2 parents 473294e + 4869af1 commit b2495b0

3 files changed

Lines changed: 361 additions & 15 deletions

File tree

knowage-api/pom.xml

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,43 @@
141141
<artifactId>resteasy-multipart-provider</artifactId>
142142
<version>4.6.0.Final</version>
143143
</dependency>
144-
<!--
144+
<!-- Commons IO - Must be BEFORE POI to ensure correct version in classpath -->
145+
<dependency>
146+
<groupId>commons-io</groupId>
147+
<artifactId>commons-io</artifactId>
148+
<version>2.15.1</version>
149+
<scope>compile</scope>
150+
</dependency>
151+
<!-- Apache POI for Excel validation -->
152+
<dependency>
153+
<groupId>org.apache.poi</groupId>
154+
<artifactId>poi</artifactId>
155+
<version>${apache.poi}</version>
156+
<exclusions>
157+
<exclusion>
158+
<groupId>commons-io</groupId>
159+
<artifactId>commons-io</artifactId>
160+
</exclusion>
161+
</exclusions>
162+
</dependency>
163+
<dependency>
164+
<groupId>org.apache.poi</groupId>
165+
<artifactId>poi-ooxml</artifactId>
166+
<version>${apache.poi-ooxml}</version>
167+
<exclusions>
168+
<exclusion>
169+
<groupId>commons-io</groupId>
170+
<artifactId>commons-io</artifactId>
171+
</exclusion>
172+
</exclusions>
173+
</dependency>
174+
<!-- SuperCSV for CSV validation -->
175+
<dependency>
176+
<groupId>net.sf.supercsv</groupId>
177+
<artifactId>super-csv</artifactId>
178+
<version>2.1.0</version>
179+
</dependency>
180+
<!--
145181
____ _ __ _ _
146182
/ __ \ | | / _| | | | |
147183
| | | |_ __ | |_ _ | |_ ___ _ __ | |_ ___ ___| |_ ___
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
/*
2+
* Knowage, Open Source Business Intelligence suite
3+
* Copyright (C) 2021 Engineering Ingegneria Informatica S.p.A.
4+
*
5+
* Knowage is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* Knowage is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
package it.eng.knowage.knowageapi.utils;
19+
20+
import java.io.ByteArrayInputStream;
21+
import java.io.ByteArrayOutputStream;
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.io.InputStreamReader;
25+
import java.nio.charset.StandardCharsets;
26+
import java.util.Arrays;
27+
28+
import org.apache.log4j.Logger;
29+
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
30+
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
31+
import org.json.JSONArray;
32+
import org.json.JSONObject;
33+
import org.supercsv.io.CsvMapReader;
34+
import org.supercsv.prefs.CsvPreference;
35+
36+
import com.fasterxml.jackson.databind.ObjectMapper;
37+
38+
import it.eng.knowage.boot.error.KnowageRuntimeException;
39+
40+
/**
41+
* Utility class to validate uploaded files:
42+
* 1. Verify file extension matches actual content
43+
* 2. Detect corrupted files by attempting partial parsing
44+
* <p>
45+
* Supported formats: XML, JSON, CSV, XLS, XLSX
46+
*
47+
* @author Knowage
48+
*/
49+
public class FileContentValidator {
50+
51+
private static final Logger logger = Logger.getLogger(FileContentValidator.class);
52+
53+
// Magic bytes signatures for file type detection
54+
private static final byte[] ZIP_SIGNATURE = new byte[] { 0x50, 0x4B, 0x03, 0x04 }; // PK.. (ZIP/XLSX)
55+
private static final byte[] OLE2_SIGNATURE = new byte[] { (byte) 0xD0, (byte) 0xCF, 0x11, (byte) 0xE0 }; // XLS
56+
57+
private static final int BUFFER_SIZE = 8192; // 8KB for initial read
58+
59+
/**
60+
* Validates that the file content matches its declared extension and is not corrupted.
61+
*
62+
* @param inputStream The file input stream (will be consumed and recreated)
63+
* @param fileName The declared filename with extension
64+
* @return A new InputStream with the same content (caller must close it)
65+
* @throws KnowageRuntimeException if validation fails
66+
*/
67+
public static InputStream validateFileContent(InputStream inputStream, String fileName) throws IOException {
68+
if (inputStream == null || fileName == null) {
69+
throw new KnowageRuntimeException("Invalid parameters: inputStream and fileName cannot be null");
70+
}
71+
72+
String extension = getFileExtension(fileName).toLowerCase();
73+
74+
// Skip validation for non-supported formats
75+
if (!isSupportedFormat(extension)) {
76+
logger.debug("File extension '" + extension + "' not in validation scope, skipping validation");
77+
return inputStream;
78+
}
79+
80+
// Read the entire stream into a byte array so we can re-use it
81+
byte[] fileContent = readFullStream(inputStream);
82+
83+
try {
84+
// Validate based on extension
85+
switch (extension) {
86+
case "xml":
87+
validateXML(fileContent);
88+
break;
89+
case "json":
90+
validateJSON(fileContent);
91+
break;
92+
case "csv":
93+
validateCSV(fileContent);
94+
break;
95+
case "xls":
96+
validateXLS(fileContent);
97+
break;
98+
case "xlsx":
99+
validateXLSX(fileContent);
100+
break;
101+
default:
102+
// Already checked in isSupportedFormat, should not happen
103+
break;
104+
}
105+
} catch (Exception e) {
106+
logger.error("File validation failed for " + fileName + ": " + e.getMessage(), e);
107+
throw new KnowageRuntimeException("Invalid or corrupted file: " + e.getMessage(), e);
108+
}
109+
110+
// Return a new InputStream from the byte array
111+
return new ByteArrayInputStream(fileContent);
112+
}
113+
114+
/**
115+
* Checks if the file format is supported for validation
116+
*/
117+
private static boolean isSupportedFormat(String extension) {
118+
return Arrays.asList("xml", "json", "csv", "xls", "xlsx").contains(extension);
119+
}
120+
121+
/**
122+
* Extracts file extension from filename
123+
*/
124+
private static String getFileExtension(String fileName) {
125+
int lastDotIndex = fileName.lastIndexOf('.');
126+
if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) {
127+
return fileName.substring(lastDotIndex + 1);
128+
}
129+
return "";
130+
}
131+
132+
/**
133+
* Reads entire InputStream into byte array
134+
*/
135+
private static byte[] readFullStream(InputStream inputStream) throws IOException {
136+
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
137+
byte[] data = new byte[BUFFER_SIZE];
138+
int nRead;
139+
while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
140+
buffer.write(data, 0, nRead);
141+
}
142+
buffer.flush();
143+
return buffer.toByteArray();
144+
}
145+
146+
/**
147+
* Validates XML file: checks XML header and attempts parsing
148+
*/
149+
private static void validateXML(byte[] content) throws Exception {
150+
if (content.length == 0) {
151+
throw new KnowageRuntimeException("File is empty");
152+
}
153+
154+
// Check for XML declaration or root element
155+
String header = new String(content, 0, Math.min(1000, content.length), StandardCharsets.UTF_8);
156+
String trimmedHeader = header.trim();
157+
158+
if (!trimmedHeader.startsWith("<?xml") && !trimmedHeader.startsWith("<")) {
159+
throw new KnowageRuntimeException("File does not appear to be valid XML (missing XML header or root element)");
160+
}
161+
162+
// Attempt to parse with a basic XML parser (SAX for security)
163+
try {
164+
javax.xml.parsers.SAXParserFactory factory = javax.xml.parsers.SAXParserFactory.newInstance();
165+
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
166+
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
167+
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
168+
javax.xml.parsers.SAXParser parser = factory.newSAXParser();
169+
parser.parse(new ByteArrayInputStream(content), new org.xml.sax.helpers.DefaultHandler());
170+
} catch (org.xml.sax.SAXException e) {
171+
throw new KnowageRuntimeException("XML file is corrupted or malformed: " + e.getMessage());
172+
}
173+
}
174+
175+
/**
176+
* Validates JSON file: attempts parsing with Jackson
177+
*/
178+
private static void validateJSON(byte[] content) {
179+
if (content.length == 0) {
180+
throw new KnowageRuntimeException("File is empty");
181+
}
182+
183+
// Check if content starts with { or [ (allowing whitespace)
184+
String preview = new String(content, 0, Math.min(100, content.length), StandardCharsets.UTF_8).trim();
185+
if (!preview.startsWith("{") && !preview.startsWith("[")) {
186+
throw new KnowageRuntimeException("File does not appear to be valid JSON (must start with { or [)");
187+
}
188+
189+
// Attempt to parse JSON
190+
try {
191+
String jsonString = new String(content, StandardCharsets.UTF_8);
192+
// Try as JSONObject first
193+
if (preview.startsWith("{")) {
194+
new JSONObject(jsonString);
195+
} else {
196+
// Try as JSONArray
197+
new JSONArray(jsonString);
198+
}
199+
} catch (Exception e) {
200+
// Fallback to Jackson for better error messages
201+
try {
202+
ObjectMapper mapper = new ObjectMapper();
203+
mapper.readTree(content);
204+
} catch (Exception je) {
205+
throw new KnowageRuntimeException("JSON file is corrupted or malformed: " + je.getMessage());
206+
}
207+
}
208+
}
209+
210+
/**
211+
* Validates CSV file: checks basic structure and attempts reading first rows
212+
*/
213+
private static void validateCSV(byte[] content) throws Exception {
214+
if (content.length == 0) {
215+
throw new KnowageRuntimeException("File is empty");
216+
}
217+
218+
// Check for binary content that doesn't match CSV expectations
219+
if (startsWithSignature(content, ZIP_SIGNATURE) || startsWithSignature(content, OLE2_SIGNATURE)) {
220+
throw new KnowageRuntimeException("File appears to be binary (ZIP/XLS) but declared as CSV");
221+
}
222+
223+
// Attempt to read first few lines with SuperCSV
224+
try (InputStreamReader reader = new InputStreamReader(new ByteArrayInputStream(content), StandardCharsets.UTF_8);
225+
CsvMapReader csvReader = new CsvMapReader(reader, CsvPreference.STANDARD_PREFERENCE)) {
226+
227+
// Try to read header
228+
String[] header = csvReader.getHeader(true);
229+
if (header == null || header.length == 0) {
230+
throw new KnowageRuntimeException("CSV file has no valid header");
231+
}
232+
233+
// Try to read at least one data row if available
234+
csvReader.read(header);
235+
236+
} catch (Exception e) {
237+
throw new KnowageRuntimeException("CSV file is corrupted or malformed: " + e.getMessage());
238+
}
239+
}
240+
241+
/**
242+
* Validates XLS file: checks OLE2 signature and attempts to open with POI
243+
*/
244+
private static void validateXLS(byte[] content) throws Exception {
245+
if (content.length == 0) {
246+
throw new KnowageRuntimeException("File is empty");
247+
}
248+
249+
// Check for OLE2 signature
250+
if (!startsWithSignature(content, OLE2_SIGNATURE)) {
251+
throw new KnowageRuntimeException("File does not have valid XLS signature (expected OLE2 format)");
252+
}
253+
254+
// Attempt to open with Apache POI
255+
try (HSSFWorkbook workbook = new HSSFWorkbook(new ByteArrayInputStream(content))) {
256+
// If we can instantiate it, the file is valid
257+
if (workbook.getNumberOfSheets() == 0) {
258+
logger.warn("XLS file has no sheets but is structurally valid");
259+
}
260+
} catch (Exception e) {
261+
throw new KnowageRuntimeException("XLS file is corrupted or cannot be opened: " + e.getMessage());
262+
}
263+
}
264+
265+
/**
266+
* Validates XLSX file: checks ZIP signature and attempts to open with POI
267+
*/
268+
private static void validateXLSX(byte[] content) throws Exception {
269+
if (content.length == 0) {
270+
throw new KnowageRuntimeException("File is empty");
271+
}
272+
273+
// Check for ZIP signature (XLSX is a ZIP archive)
274+
if (!startsWithSignature(content, ZIP_SIGNATURE)) {
275+
throw new KnowageRuntimeException("File does not have valid XLSX signature (expected ZIP format)");
276+
}
277+
278+
// Attempt to open with Apache POI
279+
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) {
280+
// If we can instantiate it, the file is valid
281+
if (workbook.getNumberOfSheets() == 0) {
282+
logger.warn("XLSX file has no sheets but is structurally valid");
283+
}
284+
} catch (Exception e) {
285+
throw new KnowageRuntimeException("XLSX file is corrupted or cannot be opened: " + e.getMessage());
286+
}
287+
}
288+
289+
/**
290+
* Checks if byte array starts with a specific signature
291+
*/
292+
private static boolean startsWithSignature(byte[] content, byte[] signature) {
293+
if (content.length < signature.length) {
294+
return false;
295+
}
296+
for (int i = 0; i < signature.length; i++) {
297+
if (content[i] != signature[i]) {
298+
return false;
299+
}
300+
}
301+
return true;
302+
}
303+
}
304+

0 commit comments

Comments
 (0)