package de.superx.servlet; import java.io.File; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import org.apache.log4j.Logger; import org.apache.log4j.spi.LoggingEvent; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import org.springframework.test.context.ContextConfiguration; import de.superx.BaseDbTest; import de.superx.TestApplicationConfigPg; import de.superx.bin.ExecuteMask; import de.superx.common.UngueltigeEingabeException; import de.superx.sec.InputCheckRegistry; import de.superx.util.UnittestAppender; //@ContextConfiguration(classes = { TestApplicationConfigPg.class }) public class SQLInjectionTest extends BaseDbTest { Logger logger = Logger.getLogger(ExecuteMask.class); private static String outputFilename; @BeforeClass public static void setup() throws Exception { outputFilename = String.join(File.separator, "test", "output_160440.html"); InputCheckRegistry.registerDefaultChecks(); ExecuteMask.setMyWEBINFFilePath(SuperXManager.getWEB_INFPfad()); } @SuppressWarnings("static-method") @Test public void testExpansionInjection() throws Exception { String[] params = buildParamsStudierendeDatenblatt("123)@@SEMICOLON@@insert into xdummy(c) values (2)@@SEMICOLON@@commit@@SEMICOLON@@--"); Logger sxsql_ServerLogger = setUpLogger(); UnittestAppender ta = setUpTestAppender(sxsql_ServerLogger); try (Connection conn = SxPools.get(MANDANTEN_ID).getConnection()) { String entries_before = getXDummyEntries(conn); boolean bla = setFieldAttribute(conn, 160462, "{InputCheck:disabled}"); try { ExecuteMask.execute(params); } catch (Exception e) { // as most exceptions are not throws, but caught and written to log, we have to analize the log... } assertThatInjectionAttemptCausedSQLException(ta); assertThatSQLExceptionWasNotCausedByFaultyTestEnvironment(ta); String entries_after = getXDummyEntries(conn); Assert.assertEquals("Count of 'xdummy' entries should not have changed, when injection is prevented", entries_before, entries_after); } } @SuppressWarnings("static-method") @Test public void testDatabaseFunctionInjection() throws Exception { String[] params = buildParamsStudierendeDatenblatt("sp_get_keylist_str($$bla1$$,$$bla2$$)::INT"); try (Connection conn = SxPools.get(MANDANTEN_ID).getConnection()) { boolean bla = setFieldAttribute(conn, 160462, "{InputCheck:matrikelnummern}"); } executeMaskAndValidateExpectedOutcome(params); } @SuppressWarnings("static-method") @Test public void testMultilineCommentInjection() throws Exception { String[] params = buildParamsVerwundbarkeitstest("bla\nblub", ""); try (Connection conn = SxPools.get(MANDANTEN_ID).getConnection()) { prepareVerwundbarbeitstest(conn); } executeMaskAndValidateExpectedOutcome(params); } @SuppressWarnings("static-method") @Test public void testZombieParameterInjection() throws Exception { String[] params = buildParamsVerwundbarkeitstest("", "ksjasf"); try (Connection conn = SxPools.get(MANDANTEN_ID).getConnection()) { prepareVerwundbarbeitstest(conn); } executeMaskAndValidateExpectedOutcome(params); } private static void assertThatSQLExceptionWasNotCausedByFaultyTestEnvironment(UnittestAppender ta) { String message = null; boolean log_events_contain_errors_due_to_missing_db_functions = false; for (LoggingEvent event : ta.loggingEventList) { message = event.getMessage().toString(); if (message != null && message.contains("Fehler beim Aufruf eine benutzerdefinierten Funktion")) { log_events_contain_errors_due_to_missing_db_functions = true; break; } } Assert.assertFalse("Found error message:\n" + message, log_events_contain_errors_due_to_missing_db_functions); } private static void assertThatInjectionAttemptCausedSQLException(UnittestAppender ta) { boolean log_events_contain_exception_with_injected_sql = false; for (LoggingEvent event : ta.loggingEventList) { String message = event.getMessage().toString(); if (message != null && message.contains("insert into xdummy(")) { log_events_contain_exception_with_injected_sql = true; break; } } Assert.assertTrue("Application log should contain an SQLException due to injected SQL", log_events_contain_exception_with_injected_sql); } private static UnittestAppender setUpTestAppender(Logger sxsql_ServerLogger) { UnittestAppender ta = new UnittestAppender(); sxsql_ServerLogger.addAppender(ta); return ta; } private static Logger setUpLogger() { Logger sxsql_ServerLogger = Logger.getLogger("superx_" + MANDANTEN_ID); sxsql_ServerLogger.setAdditivity(false); sxsql_ServerLogger.removeAllAppenders(); sxsql_ServerLogger.setLevel(org.apache.log4j.Level.ALL); return sxsql_ServerLogger; } private static void executeMaskAndValidateExpectedOutcome(String[] params) { try { ExecuteMask.execute(params); } catch (Exception e) { Assert.assertTrue( "Exception is expected to be caused by InputCheck and thus to be of type UngueltigeEingabeException", e.getClass().equals(UngueltigeEingabeException.class) ); } } private static boolean setFieldAttribute(Connection conn, int tid, String value) throws SQLException { Statement statement = conn.createStatement(); return statement.execute("UPDATE felderinfo SET attribut = '" + value + "' WHERE tid = " + tid + ";"); } private static void prepareVerwundbarbeitstest(Connection conn) throws SQLException { try (Statement statement = conn.createStatement()) { // extend the mask-SQL with // 1. the /* -- <>*/ pattern, which is quite common in superx // 2. a parameter placeholder, which has no configuration in table 'felderinfo' ('zombie_parameter') statement.execute("UPDATE maskeninfo mi SET select_stmt = replace(select_stmt, 'where ', " + "E'WHERE\\n/* 1 = 1 AND -- <>*/\\n/* <> AND */') " + "WHERE mi.name = 'Verwundbarkeitstest';"); // replace the GROUP BY clauses with versions that also work in H2 (should be rolled out in the unl-file as well) statement.execute("UPDATE felderinfo SET relation = replace(relation, 'group by 1', 'group by buchungsab_fb') " + "WHERE name = 'Buchungsab_fb' AND tid IN" + "(SELECT felderinfo_id FROM masken_felder_bez mfb JOIN maskeninfo mi ON mfb.maskeninfo_id = mi.tid AND mi.name = 'Verwundbarkeitstest');"); statement.execute("UPDATE felderinfo SET relation = replace(relation, 'group by 1', 'group by projnr_ins') " + "WHERE name = 'Projnr_ins' AND tid IN" + "(SELECT felderinfo_id FROM masken_felder_bez mfb JOIN maskeninfo mi ON mfb.maskeninfo_id = mi.tid AND mi.name = 'Verwundbarkeitstest');"); statement.execute("UPDATE felderinfo SET zeilenanzahl = 1 " + "WHERE name = 'Inhalt2' AND tid IN" + "(SELECT felderinfo_id FROM masken_felder_bez mfb JOIN maskeninfo mi ON mfb.maskeninfo_id = mi.tid AND mi.name = 'Verwundbarkeitstest');"); } } private static String getXDummyEntries(Connection conn) throws SQLException { String entries = ""; try (Statement statement = conn.createStatement(); ResultSet resultSet = statement.executeQuery("select count(*) entries from xdummy")) { resultSet.next(); entries = resultSet.getString("entries"); } return entries; } private static String[] buildParamsStudierendeDatenblatt(String injection) { String contenttype = ""; String stylesheet = "tabelle_html_datenblatt.xsl"; String[] params = { "-tid:160440", "-out:" + outputFilename, "-user:admin", "-params:Köpfe oder Fälle ?=KoF_2&Stichtag=1" + "&Matrikel-Nr.=" + injection + "&stylesheet=" + stylesheet + "&tablestylesheet=" + stylesheet + "&contenttype=" + contenttype, "-logger:" + logConfigPath, "-mandantenID:" + MANDANTEN_ID }; return params; } private static String[] buildParamsVerwundbarkeitstest(String multiline_injection_payload, String zombie_param_injection_payload) { String contenttype = ""; String stylesheet = "tabelle_html_datenblatt.xsl"; String[] params = { "-tid:71580", "-out:" + outputFilename, "-user:admin", "-params:Inhalt=test" + "&Inhalt2=" + multiline_injection_payload + "zombie_parameter=" + zombie_param_injection_payload + "&Jahr=2018&stylesheet=" + stylesheet + "&tablestylesheet=" + stylesheet + "&contenttype=" + contenttype, "-logger:" + logConfigPath, "-mandantenID:" + MANDANTEN_ID }; return params; } }