Статьи

Пользовательская аннотация в Java для безопасных параметров SQL-инъекций

Привет, Java-разработчики! В современном мире ORM мы в основном используем реализации на основе JPA для нашей настойчивости.

Мы также довольно часто используем шаблоны JPA поверх Hibernate, Spring Data или Spring JDBC.

Хотя эти платформы уже в значительной степени решают проблему SQL-инъекций, мы сталкиваемся с определенными сценариями, в которых вы все еще хотите проверить, является ли входящая строка данных безопасной для SQL-инъекций.

На всякий случай, если кому-то интересно, что такое SQL-инъекция, вы можете найти его здесь для минималистичного введения: http://www.w3schools.com/sql/sql_injection.asp

Мы должны помнить, что использование даже самых лучших инструментов не поможет, если они используются неправильно.

Я имею в виду, что вы никогда не должны создавать запросы к базе данных вместе со значениями (на случай, если вы создаете их сами). Например:select name, area from city where name = ‘Oslo’ and country = ‘Norway’

Вместо этого вы всегда должны убедиться, что ваша реализация использует подготовленные операторы и параметры, например: В select name, area from city where name = :cityName and country = :countryNameэтом примере показано использование именованного параметра, который также может быть справедливым:?в случае, если вы просто используете параметры на основе позиции. Это довольно безопасно в отношении SQL-инъекций.

Возвращаясь к нашему обсуждению: если вы все еще хотите проверить, являются ли параметры входящих параметров безопасными для SQL-инъекций, один из подходов, которые вы можете использовать, — создать для этого пользовательскую аннотацию .

Это подход, который я выбрал. Я делюсь этим с вами и надеюсь, что это поможет вам, ребята.

Я использовал это прежде всего для весеннего основанного на отдыхе веб-приложения.

Так что мой пример проекта будет использовать весеннюю загрузку.

Итак, давайте запачкаем руки.

Аннотация

При условии, что у вас есть простой проект Java, первым делом нужно определить интерфейс аннотации SQLInjectionSafe

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = SQLInjectionSafeConstraintValidator.class)
@Target( { ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInjectionSafe {

    String message() default "{SQLInjectionSafe}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

Это довольно просто. Вы определяете имя аннотации и целевой раздел кода.

Вы должны отметить @Target( { ElementType.FIELD, ElementType.PARAMETER})

Это поможет вам использовать аннотацию над полями и параметрами, наиболее вероятным местом для входящих данных.

И вы также указываете валидатор для данных. SQLInjectionSafeConstraintValidator.class

Мы рассмотрим валидатор в следующем разделе.

Валидатор

В интерфейсе аннотации мы указали валидатор: @Constraint(validatedBy = SQLInjectionSafeConstraintValidator.class)

Теперь давайте создадим сам валидатор, который является основой нашей безопасной аннотации SQL-инъекции.

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class SQLInjectionSafeConstraintValidator implements ConstraintValidator<SQLInjectionSafe, String> {


    public static final String SQL_TYPES =
            "TABLE, TABLESPACE, PROCEDURE, FUNCTION, TRIGGER, KEY, VIEW, MATERIALIZED VIEW, LIBRARY" +
            "DATABASE LINK, DBLINK, INDEX, CONSTRAINT, TRIGGER, USER, SCHEMA, DATABASE, PLUGGABLE DATABASE, BUCKET, " +
            "CLUSTER, COMMENT, SYNONYM, TYPE, JAVA, SESSION, ROLE, PACKAGE, PACKAGE BODY, OPERATOR" +
            "SEQUENCE, RESTORE POINT, PFILE, CLASS, CURSOR, OBJECT, RULE, USER, DATASET, DATASTORE, " +
            "COLUMN, FIELD, OPERATOR";

    private static final String[] sqlRegexps = {
            "(?i)(.*)(\\b)+SELECT(\\b)+\\s.*(\\b)+FROM(\\b)+\\s.*(.*)",
            "(?i)(.*)(\\b)+INSERT(\\b)+\\s.*(\\b)+INTO(\\b)+\\s.*(.*)",
            "(?i)(.*)(\\b)+UPDATE(\\b)+\\s.*(.*)",
            "(?i)(.*)(\\b)+DELETE(\\b)+\\s.*(\\b)+FROM(\\b)+\\s.*(.*)",
            "(?i)(.*)(\\b)+UPSERT(\\b)+\\s.*(.*)",
            "(?i)(.*)(\\b)+SAVEPOINT(\\b)+\\s.*(.*)",
            "(?i)(.*)(\\b)+CALL(\\b)+\\s.*(.*)",
            "(?i)(.*)(\\b)+ROLLBACK(\\b)+\\s.*(.*)",
            "(?i)(.*)(\\b)+KILL(\\b)+\\s.*(.*)",
            "(?i)(.*)(\\b)+DROP(\\b)+\\s.*(.*)",
            "(?i)(.*)(\\b)+CREATE(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",
            "(?i)(.*)(\\b)+ALTER(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",
            "(?i)(.*)(\\b)+TRUNCATE(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",
            "(?i)(.*)(\\b)+LOCK(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",
            "(?i)(.*)(\\b)+UNLOCK(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",
            "(?i)(.*)(\\b)+RELEASE(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",
            "(?i)(.*)(\\b)+DESC(\\b)+(\\w)*\\s.*(.*)",
            "(?i)(.*)(\\b)+DESCRIBE(\\b)+(\\w)*\\s.*(.*)",
            "(.*)(/\\*|\\*/|;){1,}(.*)",
            "(.*)(-){2,}(.*)",

    };

    // pre-build the Pattern objects for faster validation
    private List<Pattern> validationPatterns = getValidationPatterns();

    @Override
    public void initialize(SQLInjectionSafe sqlInjectionSafe) { }

    @Override
    public boolean isValid(String dataString, ConstraintValidatorContext cxt) {
        return isSqlInjectionSafe(dataString);
    }

    private boolean isSqlInjectionSafe(String dataString){
        if(isEmpty(dataString)){
            return true;
        }

        for(Pattern pattern : validationPatterns){
            if(matches(pattern, dataString)){
                return false;
            }
        }
        return true;
    }

    private boolean matches(Pattern pattern, String dataString){
        Matcher matcher = pattern.matcher(dataString);
        return matcher.matches();
    }

    private static List<Pattern> getValidationPatterns(){
        List<Pattern> patterns = new ArrayList<Pattern>();
        for(String sqlExpression : sqlRegexps){
            patterns.add(getPattern(sqlExpression));
        }
        return patterns;
    }


    private static Pattern getPattern(String regEx){
        return Pattern.compile(regEx, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
    }

    private boolean isEmpty(CharSequence cs) {
        return cs == null || cs.length() == 0;
    }

}

В этом методе isValidмы делим проверку проверки на другой метод isSqlInjectionSafe, и именно здесь происходит волшебство.

Во-первых, у нас есть проверка безопасности на входящей dataString, является нулевой безопасностью. Для этого я мог бы использовать общее достояние StringUtils.isEmpty().

Тем не менее, я хотел бы, чтобы мои аннотации были как можно более самостоятельными. Итак, у нас есть собственный метод isEmpty, который очень похож на реализацию apache-commons isEmpty.

Затем мы запускаем нашу dataString через серию регулярных выражений, чтобы определить, соответствует ли она какому-либо стандартному шаблону вредоносных данных SQL. В первом наборе регулярных выражений мы проверяем, соответствует ли оно любому из следующих простых шаблонов SQL:

"(?i)(.*)(\\b)+SELECT(\\b)+\\s.*(\\b)+FROM(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+INSERT(\\b)+\\s.*(\\b)+INTO(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+UPDATE(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+DELETE(\\b)+\\s.*(\\b)+FROM(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+UPSERT(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+SAVEPOINT(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+CALL(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+ROLLBACK(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+KILL(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+DROP(\\b)+\\s.*(.*)",

Следующий набор выражений:

"(?i)(.*)(\\b)+CREATE(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+ALTER(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+TRUNCATE(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+LOCK(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+UNLOCK(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+RELEASE(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",

Эти регулярные выражения связаны с SQL_TYPES, Это гарантирует, что вы не создадите, не измените, не передадите, не заблокируете, не отпустите и не опишете sql-типы, такие как таблицы, представления, триггеры и т. Д. Полный список защищенных sql-типов основан на этой константе.SQL_TYPES

Я пытался создать как можно более целостный список sql-типов с моей стороны.

Далее приведен окончательный набор выражений для обнаружения деструктивных символов в SQL:

"(?i)(.*)(\\b)+DESC(\\b)+(\\w)*\\s.*(.*)",
"(?i)(.*)(\\b)+DESCRIBE(\\b)+(\\w)*\\s.*(.*)",
"(.*)(/\\*|\\*/|;){1,}(.*)",
"(.*)(-){2,}(.*)",

Если вам нужно, вы можете расширить SQL_TYPES и набор регулярных выражений еще дальше.

Также еще одна вещь, которую стоит отметить, это то, что мы предварительно строим объекты Pattern. Обычно создание объекта Pattern является более трудоемкой частью. Теперь, когда мы уже создаем их, мы можем ожидать небольшое улучшение производительности (очень небольшое улучшение, что-то лучше, чем ничего ;-)).

Использование

Теперь вы можете комментировать свои поля и параметры с @SQLInjectionSafe

Например.

private @SQLInjectionSafe String id;

Я в основном использовал его в весеннем контроллере MVC для проверки параметра входящего запроса.

К сожалению, вы не можете напрямую аннотировать RequestParameterнашу аннотацию и ожидать, что она сработает. Spring MVC имеет свой собственный способ запуска валидаторов на RequestParameters.

Обратитесь к этим ссылкам для этого сценария:

Поэтому прямое аннотирование параметра запроса не работает. Чтобы заставить это работать, нужно создать класс-оболочку, и в этом классе-оболочке аннотировать наш параметр. Таким образом, вы можете создать класс-оболочку, который будет выглядеть примерно так:

public static class IdWrapper{

    private @SQLInjectionSafe String id;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}

Затем вы можете использовать обертку в вашем контроллере следующим образом:

@RequestMapping(value = "/getById)
public MyResponseObject getById(
        @Valid @ModelAttribute() IdWrapper idWrapper){

// do your stuff
}

Теперь, когда у вас есть Ошибка проверки входящего параметра, Spring создает a BindException. Если не обработано, BindExceptionоно отправляется как ответ напрямую.

Чтобы отправить более чистый ответ, вы можете создать ExceptionHandlerметод в контроллере.

Это может быть что-то вроде этого:

@ExceptionHandler(BindException.class)
public @ResponseBody WebResponse handleBindException(BindException be ){

    return new MyResponseObject(false,
            getBindExceptionMessage(be) // find and send an appropriate response 
    );

}

Обратите внимание, что этот способ проверки параметров запроса в контроллерах является лишь одним из многих возможных способов сделать это. Просто я нашел этот подход более удобным и чистым среди других подходов.

Вы можете запустить веб-приложение, запустив Application.java. Вы можете проверить, какие ответы вы получите, когда получите эти URL:

http://localhost:3000//api/data/getById?id=123

http://localhost:3000//api/data/getById?id=create table abcd

Тестирование Это

Поскольку мы реализуем что-то очень важное, нам лучше подкрепить это модульными тестами. Я сделал это, и вы найдете это в SQLInjectSafeConstraintValidatorTest.javaисходном коде на GitHub .

То, что мы протестировали, — это наиболее распространенные вхождения SQL в обычные данные:

String[] maliciousDataSamples = {
        "select adf from abc",
        "insert into abcd",
        "update abcd",
        "delete from abcd",
        "upsert abcd",
        "call abcd",
        "rollback ",
        "create table abc",
        "drop table",
        "drop view",
        "alter table abc",
        "truncate table abc",
        "desc abc",
};

Мы также проверили некоторые разрушающие SQL данные, такие как:

String[] sqlDisruptiveDataSamples = {
        "--",
        "/*",
        "*/",
        ";",
        "someone -- abcd",
        "abcd /* adf */ adf",
};

Мы тщательно проверили, отклоняются ли эти вредоносные образцы данных, когда они:

  • Физическое лицо

  • Префикс и суффикс чем-то

  • Все колпачки

  • Строчные буквы

Мы должны позволить хорошим данным пройти. Мы также убедились в этом в хороших тестах данных

Вы можете найти пример этого проекта на GitHub здесь .

URL: https://github.com/rkpunjal/sql-safe-annotation-example

Включите это как библиотеку / зависимость в ваш проект.

Я преобразовал это в библиотеку с открытым исходным кодом, которую можно использовать в вашем проекте, включив ее в качестве зависимости

Включите это в свой pom.xml добавить библиотеку в качестве зависимости

<dependency>
  <groupId>com.github.rkpunjal.sqlsafe</groupId>
  <artifactId>sql-injection-safe</artifactId>
  <version>1.0.2</version>
</dependency>

Ссылка GitHub на библиотеку: https://github.com/rkpunjal/sql-injection-safe/

А теперь необходимые формальности

Я считаю, что этот подход довольно эффективен и хорошо сработал для меня.

*** Мой отказ от ответственности заключается в том, что вы должны использовать его на свой страх и риск после оценки, подходит ли вам это решение.

Я искренне надеюсь, что это поможет вам. Оставьте комментарий со своим мнением.

связи