@pilllesss/yorn 1.0.182 → 1.0.183
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @pilllesss/yorn might be problematic. Click here for more details.
- package/README.md +1 -1
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/skills/code-review/LICENSE +21 -0
- package/dist/skills/code-review/SKILL.md +233 -0
- package/dist/skills/code-review/assets/pr-review-template.md +137 -0
- package/dist/skills/code-review/assets/review-checklist.md +123 -0
- package/dist/skills/code-review/reference/angular.md +768 -0
- package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
- package/dist/skills/code-review/reference/c.md +890 -0
- package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
- package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
- package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
- package/dist/skills/code-review/reference/cpp.md +893 -0
- package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
- package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
- package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
- package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
- package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
- package/dist/skills/code-review/reference/csharp.md +525 -0
- package/dist/skills/code-review/reference/css-less-sass.md +661 -0
- package/dist/skills/code-review/reference/dart.md +670 -0
- package/dist/skills/code-review/reference/django.md +985 -0
- package/dist/skills/code-review/reference/fastapi.md +580 -0
- package/dist/skills/code-review/reference/go.md +993 -0
- package/dist/skills/code-review/reference/java.md +409 -0
- package/dist/skills/code-review/reference/java8.md +586 -0
- package/dist/skills/code-review/reference/kotlin.md +1018 -0
- package/dist/skills/code-review/reference/nestjs.md +593 -0
- package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
- package/dist/skills/code-review/reference/php.md +684 -0
- package/dist/skills/code-review/reference/python.md +1073 -0
- package/dist/skills/code-review/reference/qt.md +757 -0
- package/dist/skills/code-review/reference/react.md +871 -0
- package/dist/skills/code-review/reference/ruby.md +964 -0
- package/dist/skills/code-review/reference/rust.md +846 -0
- package/dist/skills/code-review/reference/security-review-guide.md +494 -0
- package/dist/skills/code-review/reference/svelte.md +1064 -0
- package/dist/skills/code-review/reference/swift.md +936 -0
- package/dist/skills/code-review/reference/typescript.md +1016 -0
- package/dist/skills/code-review/reference/vue.md +924 -0
- package/dist/skills/code-review/reference/zig.md +440 -0
- package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
- package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
- package/dist/yorn.cjs +628 -628
- package/package.json +2 -2
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# N+1 查询问题 — 跨语言通用指南
|
|
2
|
+
|
|
3
|
+
> N+1 查询是 ORM 和数据库访问层最常见的性能反模式。本文档覆盖问题定义、检测方法、通用解决方案和跨语言代码示例。
|
|
4
|
+
|
|
5
|
+
## 目录
|
|
6
|
+
|
|
7
|
+
- [问题定义](#问题定义)
|
|
8
|
+
- [性能影响](#性能影响)
|
|
9
|
+
- [检测方法](#检测方法)
|
|
10
|
+
- [通用解决方案](#通用解决方案)
|
|
11
|
+
- [语言特定实现](#语言特定实现)
|
|
12
|
+
- [Review Checklist](#review-checklist)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 问题定义
|
|
17
|
+
|
|
18
|
+
N+1 查询是指:**1 次查询获取 N 条记录,随后在循环中触发 N 次额外查询**来获取关联数据。
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
请求流程:
|
|
22
|
+
1 query → 获取 N 条主记录
|
|
23
|
+
N queries → 每条主记录查一次关联数据
|
|
24
|
+
─────────
|
|
25
|
+
Total: 1 + N queries
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 危害
|
|
29
|
+
|
|
30
|
+
| 问题 | 影响 |
|
|
31
|
+
|------|------|
|
|
32
|
+
| **查询数量线性增长** | 100 条记录 = 101 条 SQL,1000 条 = 1001 条 |
|
|
33
|
+
| **网络延迟叠加** | 每条查询都有往返延迟(RTT),N 次往返 >> 1 次批量查询 |
|
|
34
|
+
| **连接池耗尽** | 大量查询占满数据库连接,拖慢整个应用 |
|
|
35
|
+
| **难以在开发中发现** | 开发环境数据少,N+1 不明显;生产环境数据量大时性能崩塌 |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 性能影响
|
|
40
|
+
|
|
41
|
+
### 场景对比:获取 100 个用户及其订单
|
|
42
|
+
|
|
43
|
+
| 方案 | SQL 数量 | 延迟(假设 RTT=1ms) | 适用场景 |
|
|
44
|
+
|------|----------|---------------------|---------|
|
|
45
|
+
| N+1 懒加载 | 101 条 | ~101ms | 极少数据量 |
|
|
46
|
+
| Eager loading (JOIN) | 1 条 | ~1ms | 一对多,数据量适中 |
|
|
47
|
+
| Eager loading (IN) | 2 条 | ~2ms | 多对多,大数据集 |
|
|
48
|
+
| DataLoader / batch | 2 条 | ~2ms | GraphQL / 复杂图查询 |
|
|
49
|
+
|
|
50
|
+
### SQL 数量对比
|
|
51
|
+
|
|
52
|
+
```sql
|
|
53
|
+
-- ❌ N+1: 1 + 100 = 101 queries
|
|
54
|
+
SELECT * FROM users; -- 1 query
|
|
55
|
+
SELECT * FROM orders WHERE user_id = 1; -- query 2
|
|
56
|
+
SELECT * FROM orders WHERE user_id = 2; -- query 3
|
|
57
|
+
...
|
|
58
|
+
SELECT * FROM orders WHERE user_id = 100; -- query 101
|
|
59
|
+
|
|
60
|
+
-- ✅ Batch: 2 queries
|
|
61
|
+
SELECT * FROM users;
|
|
62
|
+
SELECT * FROM orders WHERE user_id IN (1,2,...,100);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 检测方法
|
|
68
|
+
|
|
69
|
+
### 1. ORM SQL 日志
|
|
70
|
+
|
|
71
|
+
开启 SQL 日志,在测试或开发环境中观察查询数量:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# Django
|
|
75
|
+
import logging
|
|
76
|
+
logging.getLogger('django.db.backends').setLevel(logging.DEBUG)
|
|
77
|
+
|
|
78
|
+
# SQLAlchemy
|
|
79
|
+
import logging
|
|
80
|
+
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```java
|
|
84
|
+
// Spring Boot application.yml
|
|
85
|
+
spring:
|
|
86
|
+
jpa:
|
|
87
|
+
show-sql: true
|
|
88
|
+
properties:
|
|
89
|
+
hibernate.format_sql: true
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```csharp
|
|
93
|
+
// EF Core
|
|
94
|
+
optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 2. 查询计数断言
|
|
98
|
+
|
|
99
|
+
在测试中断言 SQL 查询数量:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
# Django: django-assert-num-queries
|
|
103
|
+
from django.test.utils import CaptureQueriesContext
|
|
104
|
+
from django.db import connection
|
|
105
|
+
|
|
106
|
+
with CaptureQueriesContext(connection) as ctx:
|
|
107
|
+
list(User.objects.select_related("profile").all())
|
|
108
|
+
assert len(ctx) <= 2 # 预期最多 2 条查询
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```java
|
|
112
|
+
// Hibernate: p6spy 或 datasource-proxy
|
|
113
|
+
// 在测试中统计 SQL 执行次数
|
|
114
|
+
assertThat(sqlCount).isLessThanOrEqualTo(2);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 3. APM / 数据库监控工具
|
|
118
|
+
|
|
119
|
+
- **Django Debug Toolbar** — 实时显示 SQL 数量和时间
|
|
120
|
+
- **p6spy** (Java) — JDBC 层拦截,记录所有 SQL
|
|
121
|
+
- **MiniProfiler** (.NET) — 页面内嵌 SQL 统计
|
|
122
|
+
- **DataDog / New Relic** — 生产环境慢查询告警
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 通用解决方案
|
|
127
|
+
|
|
128
|
+
### 方案 1: Eager Loading(JOIN 预加载)
|
|
129
|
+
|
|
130
|
+
一次 JOIN 查询获取主记录和关联记录。适用于一对一、一对多。
|
|
131
|
+
|
|
132
|
+
### 方案 2: Batch Fetching(IN 子句批量查询)
|
|
133
|
+
|
|
134
|
+
两次查询:主记录 + `WHERE id IN (...)` 批量获取关联记录。适用于多对多、大数据集。
|
|
135
|
+
|
|
136
|
+
### 方案 3: DataLoader Pattern
|
|
137
|
+
|
|
138
|
+
在 GraphQL 或复杂图查询场景中,收集所有需要的 ID,合并为一次批量查询。
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
// DataLoader 伪代码
|
|
142
|
+
class DataLoader<K, V> {
|
|
143
|
+
load(K key) → V // 注册需求,不立即查询
|
|
144
|
+
loadAll([K]) → [V] // 合并为一次批量查询
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 方案 4: Projection(投影)
|
|
149
|
+
|
|
150
|
+
只查询需要的字段,减少数据传输量:
|
|
151
|
+
|
|
152
|
+
```sql
|
|
153
|
+
-- ❌ 获取所有列
|
|
154
|
+
SELECT * FROM users JOIN profiles ON ...
|
|
155
|
+
|
|
156
|
+
-- ✅ 只投影需要的字段
|
|
157
|
+
SELECT u.name, p.avatar_url FROM users u JOIN profiles p ON ...
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 语言特定实现
|
|
163
|
+
|
|
164
|
+
### Python / Django
|
|
165
|
+
|
|
166
|
+
> 详见 [Django Guide](../django.md#n1-查询优化)
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
# ForeignKey / OneToOne → select_related (SQL JOIN)
|
|
170
|
+
books = Book.objects.select_related("publisher")
|
|
171
|
+
|
|
172
|
+
# M2M / reverse FK → prefetch_related (2 queries + Python merge)
|
|
173
|
+
authors = Author.objects.prefetch_related("books")
|
|
174
|
+
|
|
175
|
+
# 嵌套预加载
|
|
176
|
+
authors = Author.objects.prefetch_related("books__publisher")
|
|
177
|
+
|
|
178
|
+
# Prefetch 对象精细控制
|
|
179
|
+
from django.db.models import Prefetch
|
|
180
|
+
authors = Author.objects.prefetch_related(
|
|
181
|
+
Prefetch("books", queryset=Book.objects.filter(published=True), to_attr="published_books")
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Python / SQLAlchemy (FastAPI)
|
|
186
|
+
|
|
187
|
+
> 详见 [FastAPI Guide](../fastapi.md#database-sessions--n1)
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from sqlalchemy.orm import selectinload
|
|
191
|
+
|
|
192
|
+
# selectinload: IN 子句批量加载(推荐异步场景)
|
|
193
|
+
stmt = select(Order).options(selectinload(Order.customer))
|
|
194
|
+
|
|
195
|
+
# joinedload: JOIN 加载
|
|
196
|
+
stmt = select(Order).options(joinedload(Order.customer))
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Java / JPA (Spring Boot)
|
|
200
|
+
|
|
201
|
+
> 详见 [Java Guide](../java.md)
|
|
202
|
+
|
|
203
|
+
```java
|
|
204
|
+
// ❌ FetchType.EAGER 或循环中触发懒加载
|
|
205
|
+
@OneToMany(fetch = FetchType.EAGER) // 危险!
|
|
206
|
+
|
|
207
|
+
// ✅ JOIN FETCH
|
|
208
|
+
@Query("SELECT u FROM User u JOIN FETCH u.orders")
|
|
209
|
+
List<User> findAllWithOrders();
|
|
210
|
+
|
|
211
|
+
// ✅ @EntityGraph(声明式)
|
|
212
|
+
@EntityGraph(attributePaths = {"orders", "profile"})
|
|
213
|
+
List<User> findAll();
|
|
214
|
+
|
|
215
|
+
// ✅ @BatchSize(减少 N+1 为 N/batchSize + 1)
|
|
216
|
+
@OneToMany
|
|
217
|
+
@BatchSize(size = 50)
|
|
218
|
+
private List<Order> orders;
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### C# / EF Core
|
|
222
|
+
|
|
223
|
+
> 详见 [C# Guide](../csharp.md)
|
|
224
|
+
|
|
225
|
+
```csharp
|
|
226
|
+
// ❌ N+1: foreach 触发懒加载
|
|
227
|
+
foreach (var blog in await context.Blogs.ToListAsync())
|
|
228
|
+
foreach (var post in blog.Posts) // 每次循环都查询!
|
|
229
|
+
|
|
230
|
+
// ✅ Include + ThenInclude
|
|
231
|
+
var blogs = await context.Blogs
|
|
232
|
+
.Include(b => b.Posts)
|
|
233
|
+
.ToListAsync();
|
|
234
|
+
|
|
235
|
+
// ✅ 投影(最安全,避免过度获取)
|
|
236
|
+
var data = await context.Blogs
|
|
237
|
+
.Select(b => new { b.Url, PostTitles = b.Posts.Select(p => p.Title) })
|
|
238
|
+
.ToListAsync();
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### PHP / Laravel / Doctrine
|
|
242
|
+
|
|
243
|
+
> 详见 [PHP Guide](../php.md)
|
|
244
|
+
|
|
245
|
+
```php
|
|
246
|
+
// ❌ 循环内查询
|
|
247
|
+
foreach ($orders as $order) {
|
|
248
|
+
$customer = $customerRepo->find($order->customerId);
|
|
249
|
+
render($order, $customer);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ✅ 批量预加载
|
|
253
|
+
$customerIds = array_unique(array_map(fn($o) => $o->customerId, $orders));
|
|
254
|
+
$customers = $customerRepo->findByIds($customerIds);
|
|
255
|
+
|
|
256
|
+
foreach ($orders as $order) {
|
|
257
|
+
render($order, $customers[$order->customerId] ?? null);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Laravel Eloquent: with()
|
|
261
|
+
$orders = Order::with('customer')->get();
|
|
262
|
+
|
|
263
|
+
// Doctrine: JOIN FETCH
|
|
264
|
+
$dql = 'SELECT o, c FROM Order o JOIN o.customer c';
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### TypeScript / Prisma
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// ❌ N+1
|
|
271
|
+
const users = await prisma.user.findMany();
|
|
272
|
+
for (const user of users) {
|
|
273
|
+
user.posts = await prisma.post.findMany({ where: { userId: user.id } });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ✅ include(Prisma 自动生成 JOIN 或批量查询)
|
|
277
|
+
const users = await prisma.user.findMany({
|
|
278
|
+
include: { posts: true },
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// ✅ 嵌套 include
|
|
282
|
+
const users = await prisma.user.findMany({
|
|
283
|
+
include: {
|
|
284
|
+
posts: {
|
|
285
|
+
include: { comments: true },
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Review Checklist
|
|
294
|
+
|
|
295
|
+
### 检测
|
|
296
|
+
- [ ] 开启了 SQL 日志或查询计数监控
|
|
297
|
+
- [ ] 测试中有查询数量断言
|
|
298
|
+
- [ ] APM 工具配置了 N+1 告警
|
|
299
|
+
|
|
300
|
+
### 修复
|
|
301
|
+
- [ ] ForeignKey / OneToOne 关系使用 JOIN eager loading
|
|
302
|
+
- [ ] M2M / 反向关系使用 IN 批量预加载
|
|
303
|
+
- [ ] 避免在循环中触发数据库查询
|
|
304
|
+
- [ ] 使用投影只获取需要的字段
|
|
305
|
+
|
|
306
|
+
### 架构
|
|
307
|
+
- [ ] 列表 API 分页,避免一次加载过多记录
|
|
308
|
+
- [ ] GraphQL 场景使用 DataLoader
|
|
309
|
+
- [ ] 缓存策略(Redis)处理高频读取的关联数据
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# SQL Injection Prevention Guide
|
|
2
|
+
|
|
3
|
+
Language-agnostic SQL injection prevention strategies with cross-language code examples.
|
|
4
|
+
|
|
5
|
+
> **Related**: [Security Review Guide](../security-review-guide.md) for comprehensive security checklist and decision framework.
|
|
6
|
+
|
|
7
|
+
## Attack Types
|
|
8
|
+
|
|
9
|
+
SQL injection (SQLi) is ranked #3 in the OWASP Top 10 (2021). Three common variants:
|
|
10
|
+
|
|
11
|
+
| Type | Description | Risk |
|
|
12
|
+
|------|-------------|------|
|
|
13
|
+
| **Classic (In-band)** | Attacker receives results directly in the HTTP response | Data exfiltration, authentication bypass |
|
|
14
|
+
| **Blind (Boolean/Time-based)** | Attacker infers data from response differences or timing | Slower but still viable for data extraction |
|
|
15
|
+
| **Out-of-band** | Attacker uses DNS/HTTP callbacks to exfiltrate data | Less common but harder to detect |
|
|
16
|
+
|
|
17
|
+
## Universal Prevention Strategy
|
|
18
|
+
|
|
19
|
+
1. **Parameterized queries** — always (the #1 defense)
|
|
20
|
+
2. **ORM safe usage** — understand what your ORM escapes
|
|
21
|
+
3. **Input validation** — whitelist over blacklist
|
|
22
|
+
4. **Least privilege** — database user with minimal permissions
|
|
23
|
+
5. **WAF** — web application firewall as defense-in-depth
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Cross-Language Examples
|
|
28
|
+
|
|
29
|
+
### Python
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
# ❌ Vulnerable: string formatting
|
|
33
|
+
query = f"SELECT * FROM users WHERE id = {user_id}"
|
|
34
|
+
cursor.execute(query)
|
|
35
|
+
|
|
36
|
+
# ❌ Vulnerable: % formatting
|
|
37
|
+
cursor.execute("SELECT * FROM users WHERE id = %s" % user_id)
|
|
38
|
+
|
|
39
|
+
# ✅ Parameterized (DB-API)
|
|
40
|
+
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
|
41
|
+
|
|
42
|
+
# ✅ SQLAlchemy ORM
|
|
43
|
+
User.query.filter(User.id == user_id).all()
|
|
44
|
+
|
|
45
|
+
# ❌ SQLAlchemy raw SQL with string interpolation
|
|
46
|
+
session.execute(text(f"SELECT * FROM users WHERE id = {user_id}"))
|
|
47
|
+
|
|
48
|
+
# ✅ SQLAlchemy raw SQL with bound parameters
|
|
49
|
+
session.execute(text("SELECT * FROM users WHERE id = :id"), {"id": user_id})
|
|
50
|
+
|
|
51
|
+
# ✅ Django ORM
|
|
52
|
+
User.objects.filter(id=user_id)
|
|
53
|
+
|
|
54
|
+
# ❌ Django extra() with string interpolation
|
|
55
|
+
User.objects.extra(where=[f"username = '{username}'"])
|
|
56
|
+
|
|
57
|
+
# ✅ Django raw() with parameters
|
|
58
|
+
User.objects.raw("SELECT * FROM users WHERE id = %s", [user_id])
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Java
|
|
62
|
+
|
|
63
|
+
```java
|
|
64
|
+
// ❌ Vulnerable: string concatenation
|
|
65
|
+
String query = "SELECT * FROM users WHERE id = " + userId;
|
|
66
|
+
Statement stmt = connection.createStatement();
|
|
67
|
+
ResultSet rs = stmt.executeQuery(query);
|
|
68
|
+
|
|
69
|
+
// ✅ JDBC PreparedStatement
|
|
70
|
+
String query = "SELECT * FROM users WHERE id = ?";
|
|
71
|
+
PreparedStatement stmt = connection.prepareStatement(query);
|
|
72
|
+
stmt.setLong(1, userId);
|
|
73
|
+
ResultSet rs = stmt.executeQuery();
|
|
74
|
+
|
|
75
|
+
// ✅ JPA parameter binding
|
|
76
|
+
@Query("SELECT u FROM User u WHERE u.id = :id")
|
|
77
|
+
User findById(@Param("id") Long id);
|
|
78
|
+
|
|
79
|
+
// ✅ Spring Data JPA method naming
|
|
80
|
+
User findById(Long id);
|
|
81
|
+
|
|
82
|
+
// ❌ JPA native query with string concatenation
|
|
83
|
+
entityManager.createNativeQuery(
|
|
84
|
+
"SELECT * FROM users WHERE name = '" + name + "'"
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// ✅ JPA native query with parameter binding
|
|
88
|
+
Query query = entityManager.createNativeQuery(
|
|
89
|
+
"SELECT * FROM users WHERE name = :name"
|
|
90
|
+
);
|
|
91
|
+
query.setParameter("name", name);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Go
|
|
95
|
+
|
|
96
|
+
```go
|
|
97
|
+
// ❌ Vulnerable: fmt.Sprintf
|
|
98
|
+
query := fmt.Sprintf("SELECT * FROM users WHERE id = %s", userID)
|
|
99
|
+
rows, err := db.Query(query)
|
|
100
|
+
|
|
101
|
+
// ✅ database/sql parameterized
|
|
102
|
+
rows, err := db.Query("SELECT * FROM users WHERE id = ?", userID)
|
|
103
|
+
|
|
104
|
+
// ✅ Named parameters (sqlx)
|
|
105
|
+
rows, err := db.NamedQuery(
|
|
106
|
+
"SELECT * FROM users WHERE id = :id",
|
|
107
|
+
map[string]interface{}{"id": userID},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
// ⚠️ Dynamic identifiers (table/column names) can't use placeholders
|
|
111
|
+
// Must validate against whitelist
|
|
112
|
+
var allowedColumns = map[string]bool{
|
|
113
|
+
"id": true, "name": true, "email": true, "created_at": true,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func queryWithOrder(db *sql.DB, orderBy string) (*sql.Rows, error) {
|
|
117
|
+
if !allowedColumns[orderBy] {
|
|
118
|
+
return nil, fmt.Errorf("invalid column: %s", orderBy)
|
|
119
|
+
}
|
|
120
|
+
return db.Query(
|
|
121
|
+
fmt.Sprintf("SELECT * FROM users ORDER BY %s", orderBy),
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Node.js
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// ❌ Vulnerable: template literal
|
|
130
|
+
const query = `SELECT * FROM users WHERE id = ${userId}`;
|
|
131
|
+
const result = await client.query(query);
|
|
132
|
+
|
|
133
|
+
// ✅ pg parameterized ($1, $2, ...)
|
|
134
|
+
const result = await client.query(
|
|
135
|
+
"SELECT * FROM users WHERE id = $1",
|
|
136
|
+
[userId]
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// ✅ Prisma ORM (parameterized by default)
|
|
140
|
+
const user = await prisma.user.findUnique({
|
|
141
|
+
where: { id: userId },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ❌ Prisma $queryRawUnsafe with string interpolation
|
|
145
|
+
await prisma.$queryRawUnsafe(
|
|
146
|
+
`SELECT * FROM users WHERE id = ${userId}`
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// ✅ Prisma $queryRaw with tagged template (safe)
|
|
150
|
+
await prisma.$queryRaw`
|
|
151
|
+
SELECT * FROM users WHERE id = ${userId}
|
|
152
|
+
`;
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### PHP
|
|
156
|
+
|
|
157
|
+
```php
|
|
158
|
+
<?php
|
|
159
|
+
|
|
160
|
+
// ❌ Vulnerable: string concatenation
|
|
161
|
+
$sql = "SELECT * FROM users WHERE email = '" . $_GET['email'] . "'";
|
|
162
|
+
$user = $pdo->query($sql)->fetch();
|
|
163
|
+
|
|
164
|
+
// ✅ PDO prepared statements
|
|
165
|
+
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
|
|
166
|
+
$stmt->execute(['email' => $email]);
|
|
167
|
+
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
168
|
+
|
|
169
|
+
// ✅ PDO positional placeholders
|
|
170
|
+
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
|
|
171
|
+
$stmt->execute([$id]);
|
|
172
|
+
|
|
173
|
+
// ❌ mysqli with string interpolation
|
|
174
|
+
$result = mysqli_query($conn,
|
|
175
|
+
"SELECT * FROM users WHERE id = " . $id
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// ✅ mysqli prepared statements
|
|
179
|
+
$stmt = mysqli_prepare($conn, "SELECT * FROM users WHERE id = ?");
|
|
180
|
+
mysqli_stmt_bind_param($stmt, "i", $id);
|
|
181
|
+
mysqli_stmt_execute($stmt);
|
|
182
|
+
|
|
183
|
+
// ✅ Laravel Eloquent ORM
|
|
184
|
+
User::where('id', $id)->first();
|
|
185
|
+
|
|
186
|
+
// ❌ Laravel DB::raw with interpolation
|
|
187
|
+
DB::select(DB::raw("SELECT * FROM users WHERE id = {$id}"));
|
|
188
|
+
|
|
189
|
+
// ✅ Laravel parameterized raw
|
|
190
|
+
DB::select("SELECT * FROM users WHERE id = ?", [$id]);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### C# / .NET
|
|
194
|
+
|
|
195
|
+
```csharp
|
|
196
|
+
// ❌ Vulnerable: string concatenation
|
|
197
|
+
var query = $"SELECT * FROM Users WHERE Id = {userId}";
|
|
198
|
+
using var cmd = new SqlCommand(query, connection);
|
|
199
|
+
var reader = cmd.ExecuteReader();
|
|
200
|
+
|
|
201
|
+
// ✅ ADO.NET parameterized
|
|
202
|
+
var query = "SELECT * FROM Users WHERE Id = @Id";
|
|
203
|
+
using var cmd = new SqlCommand(query, connection);
|
|
204
|
+
cmd.Parameters.AddWithValue("@Id", userId);
|
|
205
|
+
|
|
206
|
+
// ✅ Dapper parameterized
|
|
207
|
+
var users = connection.Query<User>(
|
|
208
|
+
"SELECT * FROM Users WHERE Id = @Id",
|
|
209
|
+
new { Id = userId }
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// ❌ Dapper with string interpolation
|
|
213
|
+
var users = connection.Query<User>(
|
|
214
|
+
$"SELECT * FROM Users WHERE Id = {userId}"
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// ✅ EF Core (parameterized by default)
|
|
218
|
+
var user = await context.Users
|
|
219
|
+
.Where(u => u.Id == userId)
|
|
220
|
+
.FirstOrDefaultAsync();
|
|
221
|
+
|
|
222
|
+
// ❌ EF Core FromSqlRaw with interpolation
|
|
223
|
+
var users = context.Users
|
|
224
|
+
.FromSqlRaw($"SELECT * FROM Users WHERE Id = {userId}")
|
|
225
|
+
.ToList();
|
|
226
|
+
|
|
227
|
+
// ✅ EF Core FromSql with FormattableString (parameterized)
|
|
228
|
+
var users = context.Users
|
|
229
|
+
.FromSql($"SELECT * FROM Users WHERE Id = {userId}")
|
|
230
|
+
.ToList();
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## ORM Unsafe Usage Patterns
|
|
236
|
+
|
|
237
|
+
ORMs do NOT automatically prevent SQL injection in all cases:
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
# ❌ SQLAlchemy: text() with f-string
|
|
241
|
+
session.execute(text(f"SELECT * FROM users WHERE id = {user_id}"))
|
|
242
|
+
|
|
243
|
+
# ❌ Django: extra() / RawSQL() with string interpolation
|
|
244
|
+
User.objects.extra(where=[f"username = '{username}'"])
|
|
245
|
+
User.objects.annotate(
|
|
246
|
+
val=RawSQL(f"SELECT col FROM other WHERE id = {user_id}")
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# ❌ JPA: createNativeQuery with string concatenation
|
|
250
|
+
entityManager.createNativeQuery("SELECT * FROM users WHERE name = '" + name + "'")
|
|
251
|
+
|
|
252
|
+
# ❌ EF Core: FromSqlRaw with string interpolation
|
|
253
|
+
context.Users.FromSqlRaw($"SELECT * FROM Users WHERE Id = {userId}")
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Rule**: Every ORM has a "raw SQL" escape hatch. String interpolation in that escape hatch = SQL injection. Always use the ORM's parameter binding mechanism.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Dynamic Identifiers (Table/Column Names)
|
|
261
|
+
|
|
262
|
+
Placeholders can only bind **values**, not table names, column names, or SQL keywords. For dynamic identifiers:
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
# ✅ Whitelist validation
|
|
266
|
+
ALLOWED_COLUMNS = {"id", "name", "email", "created_at"}
|
|
267
|
+
ALLOWED_DIRECTIONS = {"ASC", "DESC"}
|
|
268
|
+
|
|
269
|
+
def get_users(order_by: str, direction: str) -> list[User]:
|
|
270
|
+
if order_by not in ALLOWED_COLUMNS:
|
|
271
|
+
raise ValueError(f"Invalid column: {order_by}")
|
|
272
|
+
if direction.upper() not in ALLOWED_DIRECTIONS:
|
|
273
|
+
raise ValueError(f"Invalid direction: {direction}")
|
|
274
|
+
|
|
275
|
+
return User.objects.order_by(
|
|
276
|
+
f"{'-' if direction.upper() == 'DESC' else ''}{order_by}"
|
|
277
|
+
)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Detection & Testing
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
# Automated scanning
|
|
286
|
+
sqlmap -u "https://example.com/api/users?id=1" --batch
|
|
287
|
+
|
|
288
|
+
# Static analysis (Python)
|
|
289
|
+
bandit -r src/ -f custom
|
|
290
|
+
|
|
291
|
+
# Static analysis (Java)
|
|
292
|
+
spotbugs -textui build/classes
|
|
293
|
+
|
|
294
|
+
# Code review keywords to search for
|
|
295
|
+
grep -rn "f\".*SELECT\|f'.*SELECT\|fmt.Sprintf.*SELECT\|format.*SELECT" src/
|
|
296
|
+
grep -rn "query.*\+.*\|query.*&\|query.*concat" src/
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Review Checklist
|
|
302
|
+
|
|
303
|
+
- [ ] All SQL queries use parameterized queries (no string interpolation)
|
|
304
|
+
- [ ] ORM raw SQL methods use bound parameters, not string formatting
|
|
305
|
+
- [ ] Dynamic identifiers (table/column names) validated against whitelist
|
|
306
|
+
- [ ] Database user has least privilege (no DROP/ALTER for app user)
|
|
307
|
+
- [ ] No SQL queries constructed from user input without parameterization
|
|
308
|
+
- [ ] Static analysis tools (Bandit, SpotBugs, SonarQube) run in CI
|