Skip to content
This repository was archived by the owner on May 5, 2021. It is now read-only.

Commit c27c5ff

Browse files
authored
SORMAS-Foundation#2990 Add custom CSVWriter implementation to sanitize formula pattern (SORMAS-Foundation#3299)
* SORMAS-Foundation#2990 Add custom CSVWriter implementation to sanitize formula pattern * SORMAS-Foundation#2990 Highlighting code changes from original version
1 parent 861356d commit c27c5ff

3 files changed

Lines changed: 155 additions & 2 deletions

File tree

sormas-api/src/main/java/de/symeda/sormas/api/utils/CSVUtils.java

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
*******************************************************************************/
1818
package de.symeda.sormas.api.utils;
1919

20+
import java.io.IOException;
2021
import java.io.Reader;
2122
import java.io.Writer;
23+
import java.util.regex.Pattern;
2224

2325
import com.opencsv.CSVParserBuilder;
2426
import com.opencsv.CSVReader;
@@ -44,7 +46,71 @@ public static CSVReader createCSVReader(Reader reader, char separator, LineValid
4446
}
4547

4648
public static CSVWriter createCSVWriter(Writer writer, char separator) {
47-
return new CSVWriter(writer, separator, CSVWriter.DEFAULT_QUOTE_CHARACTER, CSVWriter.DEFAULT_ESCAPE_CHARACTER, CSVWriter.DEFAULT_LINE_END);
49+
return new SafeCSVWriter(writer, separator, CSVWriter.DEFAULT_QUOTE_CHARACTER, CSVWriter.DEFAULT_ESCAPE_CHARACTER, CSVWriter.DEFAULT_LINE_END);
4850
}
4951

52+
/**
53+
* Extension of the {@link CSVWriter} which sanitizes each element of the CSV to prevent CSV Injection.
54+
* Implementation based on version 5.3 of opencsv.
55+
*
56+
* @see <a href="https://owasp.org/www-community/attacks/CSV_Injection">CSV Injection</a>
57+
*/
58+
public static class SafeCSVWriter extends CSVWriter {
59+
60+
private static final String FORMULA_PREFIX = "'";
61+
62+
private final Pattern formulaPattern = Pattern.compile("(?s)^[+=@-].*", Pattern.MULTILINE);
63+
64+
public SafeCSVWriter(Writer writer, char separator, char quotechar, char escapechar, String lineEnd) {
65+
super(writer, separator, quotechar, escapechar, lineEnd);
66+
}
67+
68+
@Override
69+
public void writeNext(String[] nextLine, boolean applyQuotesToAll, Appendable appendable) throws IOException {
70+
if (nextLine == null) {
71+
return;
72+
}
73+
74+
for (int i = 0; i < nextLine.length; i++) {
75+
76+
if (i != 0) {
77+
appendable.append(separator);
78+
}
79+
80+
String nextElement = nextLine[i];
81+
82+
if (nextElement == null) {
83+
continue;
84+
}
85+
86+
Boolean stringContainsSpecialCharacters = stringContainsSpecialCharacters(nextElement);
87+
88+
appendQuoteCharacterIfNeeded(applyQuotesToAll, appendable, stringContainsSpecialCharacters);
89+
90+
// begin code change from parent code
91+
if(formulaPattern.matcher(nextElement).matches()) {
92+
appendable.append(FORMULA_PREFIX);
93+
}
94+
// end
95+
96+
if (stringContainsSpecialCharacters) {
97+
processLine(nextElement, appendable);
98+
} else {
99+
appendable.append(nextElement);
100+
}
101+
102+
appendQuoteCharacterIfNeeded(applyQuotesToAll, appendable, stringContainsSpecialCharacters);
103+
}
104+
105+
appendable.append(lineEnd);
106+
writer.write(appendable.toString());
107+
}
108+
109+
// Copied from the parent class since it's access modifier was private
110+
private void appendQuoteCharacterIfNeeded(boolean applyQuotesToAll, Appendable appendable, Boolean stringContainsSpecialCharacters) throws IOException {
111+
if ((applyQuotesToAll || stringContainsSpecialCharacters) && quotechar != NO_QUOTE_CHARACTER) {
112+
appendable.append(quotechar);
113+
}
114+
}
115+
}
50116
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* SORMAS® - Surveillance Outbreak Response Management & Analysis System
3+
* Copyright © 2016-2020 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU General Public License for more details.
12+
* You should have received a copy of the GNU General Public License
13+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
14+
*/
15+
16+
package de.symeda.sormas.api.utils;
17+
18+
import com.opencsv.CSVWriter;
19+
import org.junit.Test;
20+
21+
import java.io.BufferedWriter;
22+
import java.io.IOException;
23+
import java.io.StringWriter;
24+
25+
import static org.junit.Assert.assertEquals;
26+
27+
/**
28+
* @author Alex Vidrean
29+
* @since 26-Oct-20
30+
*/
31+
public class CSVUtilsTest {
32+
33+
@Test
34+
public void testCSVFormulaInjectionPreventionSingleLine() throws IOException {
35+
36+
String[] line = new String[] {
37+
"text",
38+
"=today()",
39+
"=1+1",
40+
"+2-1",
41+
"-1-1",
42+
"@sum(1+9)" };
43+
44+
StringWriter sw = new StringWriter();
45+
BufferedWriter bw = new BufferedWriter(sw);
46+
47+
CSVWriter csvWriter = CSVUtils.createCSVWriter(bw, ',');
48+
csvWriter.writeNext(line);
49+
csvWriter.flush();
50+
51+
String string = sw.toString();
52+
String expectedString = "\"text\",\"'=today()\",\"'=1+1\",\"'+2-1\",\"'-1-1\",\"'@sum(1+9)\"" + CSVWriter.DEFAULT_LINE_END;
53+
54+
assertEquals(expectedString, string);
55+
56+
}
57+
58+
@Test
59+
public void testCSVFormulaInjectionMultipleLines() throws IOException {
60+
61+
String[] firstLine = new String[] {
62+
"1+2",
63+
"now=today",
64+
"to-from",
65+
66+
String[] secondLine = new String[] {
67+
"=DATE(1,1,1)",
68+
"@VALUE(\"2\")",
69+
"+CONCAT(\"test\",\n\"test2\")",
70+
"-5*5" };
71+
72+
StringWriter sw = new StringWriter();
73+
BufferedWriter bw = new BufferedWriter(sw);
74+
75+
CSVWriter csvWriter = CSVUtils.createCSVWriter(bw, ',');
76+
csvWriter.writeNext(firstLine);
77+
csvWriter.writeNext(secondLine);
78+
csvWriter.flush();
79+
80+
String string = sw.toString();
81+
String expectedString = "\"1+2\",\"now=today\",\"to-from\",\"[email protected]\"" + CSVWriter.DEFAULT_LINE_END
82+
+ "\"'=DATE(1,1,1)\",\"'@VALUE(\"\"2\"\")\",\"'+CONCAT(\"\"test\"\",\n\"\"test2\"\")\",\"'-5*5\"" + CSVWriter.DEFAULT_LINE_END;
83+
84+
assertEquals(expectedString, string);
85+
}
86+
87+
}

sormas-base/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@
387387
<dependency>
388388
<groupId>com.opencsv</groupId>
389389
<artifactId>opencsv</artifactId>
390-
<version>5.2</version>
390+
<version>5.3</version>
391391
</dependency>
392392

393393
<dependency>

0 commit comments

Comments
 (0)