= SQL Injection Prevention Strategy = ==概述 == SQL Injection е критична безбедносна ранливост каде нападачот внесува злонамерен SQL код преку корисничкиот влез за манипулирање со базата на податоци. Овој документ опишува како е имплементирана заштитата во Trekr backend за спречување на овој вид напади. == 1. Безбедни пракси имплементирани во проектот == === 1.1 Параметризирани пропити преку JPA === Целиот backend користи Spring Data JPA која автоматски генерира параметризирани пропити што спречуваат SQL injection. ==== Безбедно: Derived Query Methods ==== Кога користиме method names на Spring Data, те се автоматски преведуваат во параметризирани SQL пропити: {{{ // UserRepository.java public interface UserRepository extends JpaRepository { Optional findByEmail(String email); Optional findByUsername(String username); } // Сэчно користи параметриран пропит во подложина (EQUIVALENT TO): // SELECT * FROM users WHERE email = ? // параметарот se праќа одвоено од SQL командата }}} ==== Безбедно: @Query со @Param ==== За комплексни пропити, користториве @Query со именувани параметри: {{{ // TaskRepository.java @Modifying @Query("update Task t set t.finished = false where t.disciplineUser.userId = :userId") int resetFinishedForUser(@Param("userId") Long userId); // Параметарот :userId се врзува безбедно преку JDBC, не преку string concatenation }}} ==== Безбедно: Collection параметри ==== За множество вредности (IN клаузули), користи параметризиран пропит: {{{ // TrainingSessionRepository.java @java.util.List findByTrainingUser_UserIdAndDateIn( Long userId, Collection dates ); // Сэчно: SELECT * FROM training_sessions WHERE training_user_id = ? AND date IN (?, ?, ?) // Сите параметри се врзени безбедно, не преку строг конкатенација }}} === 1.2 Строга типизација на параметри === Кога параметарите се типизирани (Long, BigDecimal, LocalDate), т.е. не strings, SQL injection е веќе невозможна: {{{ // FinanceService.java - пример од createIncome BigDecimal amount = request.getAmount(); // BigDecimal, не String if (amount.compareTo(BigDecimal.ZERO) <= 0) { throw new RuntimeException("Amount must be greater than 0"); } Income income = new Income(); income.setAmount(amount); // Num values can't inject SQL incomeRepository.save(income); // Генерирана SQL: INSERT INTO incomes (amount) VALUES (?) // Параметарот amount се врзува како број, не како строк }}} === 1.3 Основна валидација на влезот === Така поради внесување и валидација на нивото на апликација пред db операции: {{{ // CustomTrackingService.java public CustomTrackingCategoryDto createCustomTrackingCategory( Long userId, CreateCustomTrackingCategoryRequest request) { String name = request.getName(); // Валидација: не null/empty if (name == null || name.isBlank()) { throw new RuntimeException("Name is required"); } String trimmed = name.trim(); // Валидација: должина if (trimmed.length() > 100) { throw new RuntimeException("Name is too long"); } // На крај, trimmed value се зачувува преку ORM // Никогаш не се користи raw SQL string concatenation CustomTrackingCategory category = new CustomTrackingCategory(); category.setName(trimmed); customTrackingCategoryRepository.save(category); } }}} === 1.4 Authorization checks за ownership === Дополнително заштита: провера дека конкретен корисник има право да приступи на специфичната информација: {{{ // CustomTrackingService.java @Transactional public void deleteCustomCategoryTask(Long userId, Long customTrackingId, Long taskId) { Task task = taskRepository.findById(taskId) .orElseThrow(() -> new RuntimeException("Task not found")); // Провера: дали задачата припаѓа на корисникот if (task.getCustomTrackingCategory() == null || !customTrackingId.equals(task.getCustomTrackingCategory().getCustomTrackingId()) || task.getCustomTrackingCategory().getUser() == null || !userId.equals(task.getCustomTrackingCategory().getUser().getUserId())) { throw new RuntimeException("Task not found"); // Скриена информација } int deleted = taskRepository.deleteByTaskIdAndCustomTrackingCategory_CustomTrackingIdAndCustomTrackingCategory_User_UserId( taskId, customTrackingId, userId); if (deleted == 0) { throw new RuntimeException("Task not found"); } } }}} == 2. НЕБЕЗБЕДНИ ПРАКСИ која НИКОГАШ не смеаат да бидат користени == === 2.1 String Concatenation со SQL === ❌ НЕБЕЗБЕДНО - НЕ КВАЧИ НИКОГАШ: {{{ // ОПАСНО: SQL injection уязвимост String email = userInput; // од форма или параметар String query = "SELECT * FROM users WHERE email = '" + email + "'"; // Ако userInput = "admin' OR '1'='1", резултира: // SELECT * FROM users WHERE email = 'admin' OR '1'='1' // Сие врати сите корисници! }}} === 2.2 Директни SQL пропити со методи како createNativeQuery без параметри === ❌ НЕБЕЗБЕДНО: {{{ // ОПАСНО String userProvidedFilter = request.getFilter(); TypedQuery query = entityManager.createNativeQuery( "SELECT * FROM users WHERE name = '" + userProvidedFilter + "'", User.class ); // Позволува SQL injection }}} ✅ БЕЗБЕДНО: {{{ // ПРАВИЛНО TypedQuery query = entityManager.createNativeQuery( "SELECT * FROM users WHERE name = ?1", User.class ); query.setParameter(1, userProvidedFilter); // Параметарот se врзува безбедно }}} === 2.3 Динамички конструирање на JPQL пропити преку string concatenation === ❌ НЕБЕЗБЕДНО: {{{ // ОПАСНО String sortBy = request.getSortBy(); // "name' OR 'x'='x" String jpql = "SELECT u FROM User u ORDER BY u." + sortBy; TypedQuery query = entityManager.createQuery(jpql, User.class); // Позволува SQL injection преку ORDER BY clause }}} ✅ БЕЗБЕДНО: {{{ // ПРАВИЛНО: користи Specification или CriteriaBuilder за динамички пропити CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery cq = cb.createQuery(User.class); Root root = cq.from(User.class); // Динамички sort параметар е безбедно обработен Order order = Sort.Direction.ASC.equals(direction) ? cb.asc(root.get(sortField)) : cb.desc(root.get(sortField)); cq.orderBy(order); TypedQuery query = entityManager.createQuery(cq); }}} == 3. Best Practices за имплементација == === 3.1 Правило: Никогаш не користи user input директно во SQL === * Сите user inputs мора да бидат параметри * Користи Spring Data JPA методи deriva или @Query со @Param * За комплексни пропити, користи CriteriaBuilder или Specification === 3.2 Правило: Типизирај параметри секогаш === ❌ ЛОШО: {{{ Long userId = Long.parseLong(request.getUserId()); // String -> Long конверзија String query = "SELECT * FROM users WHERE id = " + userId; // Неопходно SQL injection ризик }}} ✅ ДОБРО: {{{ Long userId = Long.parseLong(request.getUserId()); // String -> Long конверзија userRepository.findById(userId); // Параметар е Long, не String // Типизирани параметри не можат да содржат SQL код }}} === 3.3 Правило: Валидирай влез пред користење === * Должина на strings * Формат на emails (regex или validator) * Диапаз��н на числа * Null/empty проверки {{{ // FinanceService.java - пример public void startOrUpdateTracking(Long userId, FinanceStartRequest request) { BigDecimal spending = normalizePercent(request.getSpendingBudget()); // ... остали валидации ... // Валидирај сума = 100 validateSumTo100(spending, saving, investing, donation, credit); // Сека валидација е успешна пред зачување во DB financeUserRepository.save(financeUser); } }}} === 3.4 Правило: Користи ОРМ библиотеки (JPA/Hibernate) === * ОРМ автоматски параметризира пропити * Избегнувај raw JDBC комнди без параметри * За специјални случаи, користи JPA Criteria API === 3.5 Правило: Логирај и мониторирај пропокусни пропити === {{{ // application.properties spring.jpa.show-sql=true logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE // TRACE анализира пропити и видија каков параметри се врзуваат }}} == 4. Минимални правила за нови допринесувачи == При собачување нов feature, следи: 1. ✅ Користи Spring Data JPA методи (findBy*, save*, etc.) 2. ✅ За custom пропити, користи @Query("#jpql") со @Param параметри 3. ✅ Никогаш не користи string concatenation со SQL 4. ✅ Валидирај user inputs пред DB операции 5. ✅ Провери ownership/authorization пред delete/update операции 6. ✅ Типизирај сите параметри (Long, BigDecimal, LocalDate, не String) == 5. Пример: Правилна имплементација новог endpoint-а == Сценарио: Нов endpoint за ажурирање на трошок по категорија. {{{ // FinanceController.java @PutMapping("/allocations/{categoryId}") public ResponseEntity updateAllocation( @PathVariable Long categoryId, @Valid @RequestBody UpdateAllocationRequest request) { AllocationDto result = financeService.updateAllocation(userId, categoryId, request); return ResponseEntity.ok(result); } // FinanceService.java @Transactional public AllocationDto updateAllocation(Long userId, Long categoryId, UpdateAllocationRequest request) { // 1. Валидирај input BigDecimal amount = request.getAmount(); if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { throw new RuntimeException("Amount must be greater than 0"); } // 2. Преземи објект од DB (JPA - безбедно) Allocation allocation = allocationRepository.findById(categoryId) .orElseThrow(() -> new RuntimeException("Allocation not found")); // 3. Провери ownership if (!userId.equals(allocation.getUser().getUserId())) { throw new RuntimeException("Allocation not found"); } // 4. Ажурирај и зачувај (JPA - безбедно) allocation.setAmount(amount); Allocation saved = allocationRepository.save(allocation); // 5. Врати DTO return new AllocationDto(saved.getId(), saved.getAmount()); } }}} == 6. Заднила и прилична функција == Оваа заштита е имплементирана во: * UserRepository.java - findByEmail/Username * TaskRepository.java - findByDisciplineUser_UserId, resetFinishedForUser (@Query) * TrainingSessionRepository.java - findByTrainingUser_UserIdAndDateIn * CustomTrackingService.java - ownership checking + validation * FinanceService.java - input validation + JPA save * DisciplineService.java - ownership checks + validation == 7. Потребни алатки за QA == При тестирање на нови endpoints, следи: 1. SQL logging: вклучи DEBUG на Hibernate SQL во тест environment 2. Penetration testing: алати како OWASP ZAP или SQLmap (за белите хакери) 3. Code review: проверка дека нема string concatenation со SQL 4. Static analysis: SonarQube или Checkmarx може да детектери потенцијални SQL injection ризици == Закончување == Trekr backend е заштитен од SQL Injection напади преку: * Spring Data JPA параметризирани пропити * Типизирани параметри * Приоритетна валидација * Ownership & authorization checks * Никогаш нема string concatenation со SQL За сите нови features, следи ги best practices навултени во овој документ.