diff options
Diffstat (limited to 'sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.java')
-rw-r--r-- | sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.java | 119 |
1 files changed, 119 insertions, 0 deletions
diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.java new file mode 100644 index 00000000..da5beaa8 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.java @@ -0,0 +1,119 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed 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 com.code_intelligence.jazzer.sanitizers; + +import static java.util.Collections.unmodifiableSet; +import static java.util.stream.Collectors.toSet; + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh; +import com.code_intelligence.jazzer.api.HookType; +import com.code_intelligence.jazzer.api.Jazzer; +import com.code_intelligence.jazzer.api.MethodHook; +import java.lang.invoke.MethodHandle; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; + +/** + * Detects SQL injections. + * + * <p>Untrusted input has to be escaped in such a way that queries remain valid otherwise an + * injection could be possible. This sanitizer guides the fuzzer to inject insecure characters. If + * an exception is raised during execution the fuzzer was able to inject an invalid pattern, + * otherwise all input was escaped correctly. + * + * <p>Two types of methods are hooked: + * <ol> + * <li>Methods that take an SQL query as the first argument (e.g. {@link + * java.sql.Statement#execute}]).</li> + * <li>Methods that don't take any arguments and execute an already prepared statement (e.g. {@link + * java.sql.PreparedStatement#execute}).</li> + * </ol> + * + * For 1. we validate the syntax of the query using <a + * href="https://github.com/JSQLParser/JSqlParser">jsqlparser</a> and if both the syntax is invalid + * and the query execution throws an exception we report an SQL injection. Since we can't reliably + * validate SQL queries in arbitrary dialects this hook is expected to produce some amount of false + * positives. For 2. we can't validate the query syntax and therefore only rethrow any exceptions. + */ +@SuppressWarnings("unused") +public class SqlInjection { + // Characters that should be escaped in user input. + // See https://dev.mysql.com/doc/refman/8.0/en/string-literals.html + private static final String CHARACTERS_TO_ESCAPE = "'\"\b\n\r\t\\%_"; + + private static final Set<String> SQL_SYNTAX_ERROR_EXCEPTIONS = unmodifiableSet( + Stream + .of("java.sql.SQLException", "java.sql.SQLNonTransientException", + "java.sql.SQLSyntaxErrorException", "org.h2.jdbc.JdbcSQLSyntaxErrorException", + "org.h2.jdbc.JdbcSQLFeatureNotSupportedException") + .collect(toSet())); + + @MethodHook( + type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "execute") + @MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", + targetMethod = "executeBatch") + @MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", + targetMethod = "executeLargeBatch") + @MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", + targetMethod = "executeLargeUpdate") + @MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", + targetMethod = "executeQuery") + @MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", + targetMethod = "executeUpdate") + @MethodHook(type = HookType.REPLACE, targetClassName = "javax.persistence.EntityManager", + targetMethod = "createNativeQuery") + public static Object + checkSqlExecute(MethodHandle method, Object thisObject, Object[] arguments, int hookId) + throws Throwable { + boolean hasValidSqlQuery = false; + + if (arguments.length > 0 && arguments[0] instanceof String) { + String query = (String) arguments[0]; + hasValidSqlQuery = isValidSql(query); + Jazzer.guideTowardsContainment(query, CHARACTERS_TO_ESCAPE, hookId); + } + try { + return method.invokeWithArguments( + Stream.concat(Stream.of(thisObject), Arrays.stream(arguments)).toArray()); + } catch (Throwable throwable) { + // If we already validated the query string and know it's correct, + // The exception is likely thrown by a non-existent table or something + // that we don't want to report. + if (!hasValidSqlQuery + && SQL_SYNTAX_ERROR_EXCEPTIONS.contains(throwable.getClass().getName())) { + Jazzer.reportFindingFromHook(new FuzzerSecurityIssueHigh( + String.format("SQL Injection%nInjected query: %s%n", arguments[0]))); + } + throw throwable; + } + } + + private static boolean isValidSql(String sql) { + try { + CCJSqlParserUtil.parseStatements(sql); + return true; + } catch (JSQLParserException e) { + return false; + } catch (Throwable t) { + // Catch any unexpected exceptions so that we don't disturb the + // instrumented application. + t.printStackTrace(); + return true; + } + } +} |