= 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<User, Long> {
    Optional<User> findByEmail(String email);
    Optional<User> 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<TrainingSession> findByTrainingUser_UserIdAndDateIn(
    Long userId,
    Collection<LocalDate> 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<User> query = entityManager.createNativeQuery(
    "SELECT * FROM users WHERE name = '" + userProvidedFilter + "'",
    User.class
);
// Позволува SQL injection
}}}

✅ БЕЗБЕДНО:

{{{
// ПРАВИЛНО
TypedQuery<User> 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<User> query = entityManager.createQuery(jpql, User.class);
// Позволува SQL injection преку ORDER BY clause
}}}

✅ БЕЗБЕДНО:

{{{
// ПРАВИЛНО: користи Specification или CriteriaBuilder за динамички пропити
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> 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<User> 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<AllocationDto> 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 навултени во овој документ.

