source: backend/docs/SQL_Injection_Prevention.txt@ 42da64d

Last change on this file since 42da64d was 42da64d, checked in by Andrej <asumanovski@…>, 8 weeks ago

Fixed auth issue

  • Property mode set to 100644
File size: 13.6 KB
Line 
1= SQL Injection Prevention Strategy =
2
3==概述 ==
4
5SQL Injection е критична безбедносна ранливост каде нападачот внесува злонамерен SQL код преку корисничкиот влез за манипулирање со базата на податоци. Овој документ опишува како е имплементирана заштитата во Trekr backend за спречување на овој вид напади.
6
7== 1. Безбедни пракси имплементирани во проектот ==
8
9=== 1.1 Параметризирани пропити преку JPA ===
10
11Целиот backend користи Spring Data JPA која автоматски генерира параметризирани пропити што спречуваат SQL injection.
12
13==== Безбедно: Derived Query Methods ====
14
15Кога користиме method names на Spring Data, те се автоматски преведуваат во параметризирани SQL пропити:
16
17{{{
18// UserRepository.java
19public interface UserRepository extends JpaRepository<User, Long> {
20 Optional<User> findByEmail(String email);
21 Optional<User> findByUsername(String username);
22}
23
24// Сэчно користи параметриран пропит во подложина (EQUIVALENT TO):
25// SELECT * FROM users WHERE email = ?
26// параметарот se праќа одвоено од SQL командата
27}}}
28
29==== Безбедно: @Query со @Param ====
30
31За комплексни пропити, користториве @Query со именувани параметри:
32
33{{{
34// TaskRepository.java
35@Modifying
36@Query("update Task t set t.finished = false where t.disciplineUser.userId = :userId")
37int resetFinishedForUser(@Param("userId") Long userId);
38
39// Параметарот :userId се врзува безбедно преку JDBC, не преку string concatenation
40}}}
41
42==== Безбедно: Collection параметри ====
43
44За множество вредности (IN клаузули), користи параметризиран пропит:
45
46{{{
47// TrainingSessionRepository.java
48@java.util.List<TrainingSession> findByTrainingUser_UserIdAndDateIn(
49 Long userId,
50 Collection<LocalDate> dates
51);
52
53// Сэчно: SELECT * FROM training_sessions WHERE training_user_id = ? AND date IN (?, ?, ?)
54// Сите параметри се врзени безбедно, не преку строг конкатенација
55}}}
56
57=== 1.2 Строга типизација на параметри ===
58
59Кога параметарите се типизирани (Long, BigDecimal, LocalDate), т.е. не strings, SQL injection е веќе невозможна:
60
61{{{
62// FinanceService.java - пример од createIncome
63BigDecimal amount = request.getAmount(); // BigDecimal, не String
64if (amount.compareTo(BigDecimal.ZERO) <= 0) {
65 throw new RuntimeException("Amount must be greater than 0");
66}
67
68Income income = new Income();
69income.setAmount(amount); // Num values can't inject SQL
70incomeRepository.save(income);
71
72// Генерирана SQL: INSERT INTO incomes (amount) VALUES (?)
73// Параметарот amount се врзува како број, не како строк
74}}}
75
76=== 1.3 Основна валидација на влезот ===
77
78Така поради внесување и валидација на нивото на апликација пред db операции:
79
80{{{
81// CustomTrackingService.java
82public CustomTrackingCategoryDto createCustomTrackingCategory(
83 Long userId,
84 CreateCustomTrackingCategoryRequest request) {
85
86 String name = request.getName();
87
88 // Валидација: не null/empty
89 if (name == null || name.isBlank()) {
90 throw new RuntimeException("Name is required");
91 }
92
93 String trimmed = name.trim();
94
95 // Валидација: должина
96 if (trimmed.length() > 100) {
97 throw new RuntimeException("Name is too long");
98 }
99
100 // На крај, trimmed value се зачувува преку ORM
101 // Никогаш не се користи raw SQL string concatenation
102 CustomTrackingCategory category = new CustomTrackingCategory();
103 category.setName(trimmed);
104 customTrackingCategoryRepository.save(category);
105}
106}}}
107
108=== 1.4 Authorization checks за ownership ===
109
110Дополнително заштита: провера дека конкретен корисник има право да приступи на специфичната информација:
111
112{{{
113// CustomTrackingService.java
114@Transactional
115public void deleteCustomCategoryTask(Long userId, Long customTrackingId, Long taskId) {
116 Task task = taskRepository.findById(taskId)
117 .orElseThrow(() -> new RuntimeException("Task not found"));
118
119 // Провера: дали задачата припаѓа на корисникот
120 if (task.getCustomTrackingCategory() == null
121 || !customTrackingId.equals(task.getCustomTrackingCategory().getCustomTrackingId())
122 || task.getCustomTrackingCategory().getUser() == null
123 || !userId.equals(task.getCustomTrackingCategory().getUser().getUserId())) {
124 throw new RuntimeException("Task not found"); // Скриена информација
125 }
126
127 int deleted = taskRepository.deleteByTaskIdAndCustomTrackingCategory_CustomTrackingIdAndCustomTrackingCategory_User_UserId(
128 taskId, customTrackingId, userId);
129 if (deleted == 0) {
130 throw new RuntimeException("Task not found");
131 }
132}
133}}}
134
135== 2. НЕБЕЗБЕДНИ ПРАКСИ која НИКОГАШ не смеаат да бидат користени ==
136
137=== 2.1 String Concatenation со SQL ===
138
139❌ НЕБЕЗБЕДНО - НЕ КВАЧИ НИКОГАШ:
140
141{{{
142// ОПАСНО: SQL injection уязвимост
143String email = userInput; // од форма или параметар
144String query = "SELECT * FROM users WHERE email = '" + email + "'";
145// Ако userInput = "admin' OR '1'='1", резултира:
146// SELECT * FROM users WHERE email = 'admin' OR '1'='1'
147// Сие врати сите корисници!
148}}}
149
150=== 2.2 Директни SQL пропити со методи како createNativeQuery без параметри ===
151
152❌ НЕБЕЗБЕДНО:
153
154{{{
155// ОПАСНО
156String userProvidedFilter = request.getFilter();
157TypedQuery<User> query = entityManager.createNativeQuery(
158 "SELECT * FROM users WHERE name = '" + userProvidedFilter + "'",
159 User.class
160);
161// Позволува SQL injection
162}}}
163
164✅ БЕЗБЕДНО:
165
166{{{
167// ПРАВИЛНО
168TypedQuery<User> query = entityManager.createNativeQuery(
169 "SELECT * FROM users WHERE name = ?1",
170 User.class
171);
172query.setParameter(1, userProvidedFilter);
173// Параметарот se врзува безбедно
174}}}
175
176=== 2.3 Динамички конструирање на JPQL пропити преку string concatenation ===
177
178❌ НЕБЕЗБЕДНО:
179
180{{{
181// ОПАСНО
182String sortBy = request.getSortBy(); // "name' OR 'x'='x"
183String jpql = "SELECT u FROM User u ORDER BY u." + sortBy;
184TypedQuery<User> query = entityManager.createQuery(jpql, User.class);
185// Позволува SQL injection преку ORDER BY clause
186}}}
187
188✅ БЕЗБЕДНО:
189
190{{{
191// ПРАВИЛНО: користи Specification или CriteriaBuilder за динамички пропити
192CriteriaBuilder cb = entityManager.getCriteriaBuilder();
193CriteriaQuery<User> cq = cb.createQuery(User.class);
194Root<User> root = cq.from(User.class);
195
196// Динамички sort параметар е безбедно обработен
197Order order = Sort.Direction.ASC.equals(direction)
198 ? cb.asc(root.get(sortField))
199 : cb.desc(root.get(sortField));
200
201cq.orderBy(order);
202TypedQuery<User> query = entityManager.createQuery(cq);
203}}}
204
205== 3. Best Practices за имплементација ==
206
207=== 3.1 Правило: Никогаш не користи user input директно во SQL ===
208
209* Сите user inputs мора да бидат параметри
210* Користи Spring Data JPA методи deriva или @Query со @Param
211* За комплексни пропити, користи CriteriaBuilder или Specification
212
213=== 3.2 Правило: Типизирај параметри секогаш ===
214
215❌ ЛОШО:
216{{{
217Long userId = Long.parseLong(request.getUserId()); // String -> Long конверзија
218String query = "SELECT * FROM users WHERE id = " + userId; // Неопходно SQL injection ризик
219}}}
220
221✅ ДОБРО:
222{{{
223Long userId = Long.parseLong(request.getUserId()); // String -> Long конверзија
224userRepository.findById(userId); // Параметар е Long, не String
225// Типизирани параметри не можат да содржат SQL код
226}}}
227
228=== 3.3 Правило: Валидирай влез пред користење ===
229
230* Должина на strings
231* Формат на emails (regex или validator)
232* Диапаз��н на числа
233* Null/empty проверки
234
235{{{
236// FinanceService.java - пример
237public void startOrUpdateTracking(Long userId, FinanceStartRequest request) {
238 BigDecimal spending = normalizePercent(request.getSpendingBudget());
239 // ... остали валидации ...
240
241 // Валидирај сума = 100
242 validateSumTo100(spending, saving, investing, donation, credit);
243
244 // Сека валидација е успешна пред зачување во DB
245 financeUserRepository.save(financeUser);
246}
247}}}
248
249=== 3.4 Правило: Користи ОРМ библиотеки (JPA/Hibernate) ===
250
251* ОРМ автоматски параметризира пропити
252* Избегнувај raw JDBC комнди без параметри
253* За специјални случаи, користи JPA Criteria API
254
255=== 3.5 Правило: Логирај и мониторирај пропокусни пропити ===
256
257{{{
258// application.properties
259spring.jpa.show-sql=true
260logging.level.org.hibernate.SQL=DEBUG
261logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
262
263// TRACE анализира пропити и видија каков параметри се врзуваат
264}}}
265
266== 4. Минимални правила за нови допринесувачи ==
267
268При собачување нов feature, следи:
269
2701. ✅ Користи Spring Data JPA методи (findBy*, save*, etc.)
2712. ✅ За custom пропити, користи @Query("#jpql") со @Param параметри
2723. ✅ Никогаш не користи string concatenation со SQL
2734. ✅ Валидирај user inputs пред DB операции
2745. ✅ Провери ownership/authorization пред delete/update операции
2756. ✅ Типизирај сите параметри (Long, BigDecimal, LocalDate, не String)
276
277== 5. Пример: Правилна имплементација новог endpoint-а ==
278
279Сценарио: Нов endpoint за ажурирање на трошок по категорија.
280
281{{{
282// FinanceController.java
283@PutMapping("/allocations/{categoryId}")
284public ResponseEntity<AllocationDto> updateAllocation(
285 @PathVariable Long categoryId,
286 @Valid @RequestBody UpdateAllocationRequest request) {
287
288 AllocationDto result = financeService.updateAllocation(userId, categoryId, request);
289 return ResponseEntity.ok(result);
290}
291
292// FinanceService.java
293@Transactional
294public AllocationDto updateAllocation(Long userId, Long categoryId, UpdateAllocationRequest request) {
295
296 // 1. Валидирај input
297 BigDecimal amount = request.getAmount();
298 if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
299 throw new RuntimeException("Amount must be greater than 0");
300 }
301
302 // 2. Преземи објект од DB (JPA - безбедно)
303 Allocation allocation = allocationRepository.findById(categoryId)
304 .orElseThrow(() -> new RuntimeException("Allocation not found"));
305
306 // 3. Провери ownership
307 if (!userId.equals(allocation.getUser().getUserId())) {
308 throw new RuntimeException("Allocation not found");
309 }
310
311 // 4. Ажурирај и зачувај (JPA - безбедно)
312 allocation.setAmount(amount);
313 Allocation saved = allocationRepository.save(allocation);
314
315 // 5. Врати DTO
316 return new AllocationDto(saved.getId(), saved.getAmount());
317}
318}}}
319
320== 6. Заднила и прилична функција ==
321
322Оваа заштита е имплементирана во:
323
324* UserRepository.java - findByEmail/Username
325* TaskRepository.java - findByDisciplineUser_UserId, resetFinishedForUser (@Query)
326* TrainingSessionRepository.java - findByTrainingUser_UserIdAndDateIn
327* CustomTrackingService.java - ownership checking + validation
328* FinanceService.java - input validation + JPA save
329* DisciplineService.java - ownership checks + validation
330
331== 7. Потребни алатки за QA ==
332
333При тестирање на нови endpoints, следи:
334
3351. SQL logging: вклучи DEBUG на Hibernate SQL во тест environment
3362. Penetration testing: алати како OWASP ZAP или SQLmap (за белите хакери)
3373. Code review: проверка дека нема string concatenation со SQL
3384. Static analysis: SonarQube или Checkmarx може да детектери потенцијални SQL injection ризици
339
340== Закончување ==
341
342Trekr backend е заштитен од SQL Injection напади преку:
343* Spring Data JPA параметризирани пропити
344* Типизирани параметри
345* Приоритетна валидација
346* Ownership & authorization checks
347* Никогаш нема string concatenation со SQL
348
349За сите нови features, следи ги best practices навултени во овој документ.
350
Note: See TracBrowser for help on using the repository browser.