ai-developer-skill-os 8.1.7 → 8.1.9
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.
- package/.agents/README.md +3 -3
- package/.agents/skills/qk-code-review/SKILL.md +189 -0
- package/.agents/skills/qk-code-review/references/ai/ai-anti-patterns.md +28 -0
- package/.agents/skills/qk-code-review/references/ai/v8-schema-validation.md +65 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/architecture-review-guide.md +212 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/async-concurrency-patterns.md +515 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/code-quality-universal.md +358 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/code-review-best-practices.md +136 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/common-bugs-checklist.md +124 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/error-handling-principles.md +492 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/n-plus-one-queries.md +309 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/performance-review-guide.md +387 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/security-review-guide.md +318 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/sql-injection-prevention.md +308 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/xss-prevention.md +264 -0
- package/.agents/skills/qk-code-review/references/languages/angular.md +768 -0
- package/.agents/skills/qk-code-review/references/languages/c.md +890 -0
- package/.agents/skills/qk-code-review/references/languages/cpp.md +893 -0
- package/.agents/skills/qk-code-review/references/languages/csharp.md +519 -0
- package/.agents/skills/qk-code-review/references/languages/css-less-sass.md +661 -0
- package/.agents/skills/qk-code-review/references/languages/django.md +985 -0
- package/.agents/skills/qk-code-review/references/languages/fastapi.md +580 -0
- package/.agents/skills/qk-code-review/references/languages/go.md +993 -0
- package/.agents/skills/qk-code-review/references/languages/java.md +409 -0
- package/.agents/skills/qk-code-review/references/languages/java8.md +586 -0
- package/.agents/skills/qk-code-review/references/languages/kotlin.md +1018 -0
- package/.agents/skills/qk-code-review/references/languages/nestjs.md +593 -0
- package/.agents/skills/qk-code-review/references/languages/php.md +684 -0
- package/.agents/skills/qk-code-review/references/languages/python.md +1073 -0
- package/.agents/skills/qk-code-review/references/languages/qt.md +757 -0
- package/.agents/skills/qk-code-review/references/languages/react.md +871 -0
- package/.agents/skills/qk-code-review/references/languages/ruby.md +964 -0
- package/.agents/skills/qk-code-review/references/languages/rust.md +846 -0
- package/.agents/skills/qk-code-review/references/languages/svelte.md +1064 -0
- package/.agents/skills/qk-code-review/references/languages/swift.md +936 -0
- package/.agents/skills/qk-code-review/references/languages/typescript.md +1016 -0
- package/.agents/skills/qk-code-review/references/languages/vue.md +924 -0
- package/.agents/skills/qk-code-review/references/languages/zig.md +440 -0
- package/README.md +3 -3
- package/bin/install.js +5 -2
- package/package.json +1 -1
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# Hướng dẫn Review Bảo mật (Security Review Guide)
|
|
2
|
+
|
|
3
|
+
Checklist code review tập trung vào Bảo mật dựa trên OWASP Top 10 và các best practice của ngành.
|
|
4
|
+
|
|
5
|
+
## Authentication & Authorization (Xác thực & Phân quyền)
|
|
6
|
+
|
|
7
|
+
### Authentication (Xác thực)
|
|
8
|
+
- [ ] Passwords được băm (hash) bằng thuật toán mạnh (bcrypt, argon2)
|
|
9
|
+
- [ ] Bắt buộc độ phức tạp của password (Complexity requirements)
|
|
10
|
+
- [ ] Khóa tài khoản (Account lockout) sau nhiều lần đăng nhập sai
|
|
11
|
+
- [ ] Luồng reset password an toàn
|
|
12
|
+
- [ ] Xác thực đa yếu tố (MFA) cho các thao tác nhạy cảm
|
|
13
|
+
- [ ] Session tokens là chuỗi ngẫu nhiên chuẩn mật mã học (cryptographically random)
|
|
14
|
+
- [ ] Cài đặt timeout cho Session
|
|
15
|
+
|
|
16
|
+
### Authorization (Phân quyền)
|
|
17
|
+
- [ ] Có bước kiểm tra phân quyền (Authorization checks) trên MỌI request
|
|
18
|
+
- [ ] Áp dụng nguyên tắc Đặc quyền tối thiểu (Principle of least privilege)
|
|
19
|
+
- [ ] Phân quyền dựa trên Role (RBAC) được triển khai đúng cách
|
|
20
|
+
- [ ] Không có lỗ hổng leo thang đặc quyền (Privilege escalation paths)
|
|
21
|
+
- [ ] Kiểm tra tham chiếu đối tượng trực tiếp (IDOR prevention)
|
|
22
|
+
- [ ] Các endpoint API được bảo vệ thích hợp
|
|
23
|
+
|
|
24
|
+
### Bảo mật JWT
|
|
25
|
+
```typescript
|
|
26
|
+
// ❌ Cấu hình JWT không an toàn (Secret yếu)
|
|
27
|
+
jwt.sign(payload, 'weak-secret');
|
|
28
|
+
|
|
29
|
+
// ✅ Cấu hình JWT an toàn
|
|
30
|
+
jwt.sign(payload, process.env.JWT_SECRET, {
|
|
31
|
+
algorithm: 'RS256',
|
|
32
|
+
expiresIn: '15m',
|
|
33
|
+
issuer: 'your-app',
|
|
34
|
+
audience: 'your-api'
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ❌ Không xác minh JWT đàng hoàng
|
|
38
|
+
const decoded = jwt.decode(token); // Không verify chữ ký!
|
|
39
|
+
|
|
40
|
+
// ✅ Xác minh chữ ký và các claims
|
|
41
|
+
const decoded = jwt.verify(token, publicKey, {
|
|
42
|
+
algorithms: ['RS256'],
|
|
43
|
+
issuer: 'your-app',
|
|
44
|
+
audience: 'your-api'
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Input Validation (Kiểm duyệt đầu vào)
|
|
49
|
+
|
|
50
|
+
### Phòng chống SQL Injection
|
|
51
|
+
|
|
52
|
+
**Quy tắc số #1**: Luôn luôn sử dụng Parameterized queries (Truy vấn có tham số). Tuyệt đối không nối chuỗi input của user vào lệnh SQL.
|
|
53
|
+
|
|
54
|
+
Mọi ngôn ngữ và framework lớn đều hỗ trợ:
|
|
55
|
+
- Python: `cursor.execute("SELECT ...", params)` / ORM filter methods
|
|
56
|
+
- Java: `PreparedStatement` / JPA `@Query` với `@Param`
|
|
57
|
+
- Go: `db.Query("SELECT ...", args...)`
|
|
58
|
+
- Node.js: `client.query("SELECT ...", [args])` / Prisma ORM
|
|
59
|
+
- PHP: PDO prepared statements / Laravel Eloquent
|
|
60
|
+
- C#: ADO.NET `SqlParameter` / Dapper / EF Core LINQ
|
|
61
|
+
|
|
62
|
+
### Phòng chống XSS (Cross-Site Scripting)
|
|
63
|
+
|
|
64
|
+
**Quy tắc số #1**: Phụ thuộc vào tính năng auto-escaping của Framework. Kiểm toán gắt gao mọi "lỗ hổng thoát" (escape hatch).
|
|
65
|
+
|
|
66
|
+
Các framework hiện đại đều auto-escape theo mặc định:
|
|
67
|
+
- React: JSX tự động escape. Kiểm toán `dangerouslySetInnerHTML`.
|
|
68
|
+
- Vue: `{{ }}` tự động escape. Kiểm toán `v-html`.
|
|
69
|
+
- Angular: Interpolation tự động escape. Kiểm toán `bypassSecurityTrustHtml`.
|
|
70
|
+
- Svelte: `{ }` tự động escape. Kiểm toán `{@html}`.
|
|
71
|
+
- C# (Razor): `@` tự động escape. Kiểm toán `@Html.Raw()`.
|
|
72
|
+
|
|
73
|
+
Để phòng thủ sâu (defense-in-depth), hãy cấu hình Content Security Policy (CSP) với `script-src` dựa trên nonce.
|
|
74
|
+
|
|
75
|
+
### Phòng chống CSRF (Cross-Site Request Forgery)
|
|
76
|
+
|
|
77
|
+
**Triển khai CSRF Token**
|
|
78
|
+
```typescript
|
|
79
|
+
// ✅ Server: Sinh và xác nhận CSRF token
|
|
80
|
+
import crypto from 'node:crypto';
|
|
81
|
+
|
|
82
|
+
function generateCsrfToken(): string {
|
|
83
|
+
return crypto.randomBytes(32).toString('hex');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Middleware: Validate token trên các request thay đổi trạng thái
|
|
87
|
+
app.post('/api/data', (req, res) => {
|
|
88
|
+
const token = req.headers['x-csrf-token'];
|
|
89
|
+
const sessionToken = req.session.csrfToken;
|
|
90
|
+
if (!token || token !== sessionToken) {
|
|
91
|
+
return res.status(403).json({ error: 'Invalid CSRF token' });
|
|
92
|
+
}
|
|
93
|
+
// ...xử lý request
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**C# / ASP.NET Core**
|
|
98
|
+
```csharp
|
|
99
|
+
// ✅ ASP.NET Core: CSRF token (Antiforgery) được tích hợp sẵn
|
|
100
|
+
builder.Services.AddAntiforgery(options =>
|
|
101
|
+
{
|
|
102
|
+
options.HeaderName = "X-CSRF-TOKEN";
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Middleware
|
|
106
|
+
app.UseAntiforgery();
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**SameSite Cookie**
|
|
110
|
+
```typescript
|
|
111
|
+
// ✅ Set SameSite cookie làm hàng rào phòng thủ phụ
|
|
112
|
+
res.cookie('session', sessionId, {
|
|
113
|
+
httpOnly: true,
|
|
114
|
+
secure: true,
|
|
115
|
+
sameSite: 'strict', // Hoặc 'lax' nếu cho phép điều hướng GET
|
|
116
|
+
maxAge: 3600000,
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Phòng chống SSRF (Server-Side Request Forgery)
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// ❌ Lỗ hổng SSRF: URL bị điều khiển bởi người dùng
|
|
124
|
+
const url = req.query.url;
|
|
125
|
+
const response = await fetch(url);
|
|
126
|
+
|
|
127
|
+
// ✅ Validate URL trước khi fetch
|
|
128
|
+
const ALLOWED_DOMAINS = ['api.internal.com'];
|
|
129
|
+
|
|
130
|
+
function isSafeUrl(url: string): boolean {
|
|
131
|
+
try {
|
|
132
|
+
const parsed = new URL(url);
|
|
133
|
+
// Block IP nội bộ (localhost)
|
|
134
|
+
if (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1') {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
// Block dải mạng Private IP
|
|
138
|
+
if (parsed.hostname.match(/^10\.|^172\.(1[6-9]|2\d|3[01])\.|^192\.168\./)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return ALLOWED_DOMAINS.includes(parsed.hostname);
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### IDOR (Insecure Direct Object Reference)
|
|
149
|
+
|
|
150
|
+
```csharp
|
|
151
|
+
// ❌ Lỗ hổng: Không kiểm tra quyền sở hữu
|
|
152
|
+
[HttpGet("api/orders/{id}")]
|
|
153
|
+
public async Task<IActionResult> GetOrder(int id)
|
|
154
|
+
{
|
|
155
|
+
var order = await _db.Orders.FindAsync(id); // Bất kỳ ai cũng lấy được đơn hàng của người khác
|
|
156
|
+
return Ok(order);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ✅ Validate quyền sở hữu trước khi trả dữ liệu
|
|
160
|
+
[HttpGet("api/orders/{id}")]
|
|
161
|
+
public async Task<IActionResult> GetOrder(int id)
|
|
162
|
+
{
|
|
163
|
+
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
164
|
+
var order = await _db.Orders.FirstOrDefaultAsync(o => o.Id == id && o.UserId == userId); // Giới hạn theo Current User
|
|
165
|
+
if (order == null) return NotFound();
|
|
166
|
+
return Ok(order);
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**UUID vs Tự tăng ID (Auto-increment ID)**
|
|
171
|
+
```text
|
|
172
|
+
// ❌ ID tự tăng dễ bị cào dữ liệu (Enumeration)
|
|
173
|
+
// GET /api/users/1, /api/users/2, /api/users/3 ...
|
|
174
|
+
|
|
175
|
+
// ✅ UUID không thể đoán trước
|
|
176
|
+
// GET /api/users/550e8400-e29b-41d4-a716-446655440000
|
|
177
|
+
|
|
178
|
+
// ⚠️ UUID chỉ chống cào dữ liệu, KHÔNG PHẢI là chốt chặn bảo mật.
|
|
179
|
+
// VẪN PHẢI kiểm tra Current User có quyền truy cập UUID đó không!
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Phòng chống Command Injection (Tiêm lệnh)
|
|
183
|
+
|
|
184
|
+
**C#**
|
|
185
|
+
```csharp
|
|
186
|
+
// ❌ Lỗ hổng: Nối chuỗi vào lệnh gọi Shell
|
|
187
|
+
Process.Start("cmd.exe", "/c convert " + filename + " output.png");
|
|
188
|
+
|
|
189
|
+
// ✅ Truyền arguments tách biệt rõ ràng
|
|
190
|
+
var startInfo = new ProcessStartInfo
|
|
191
|
+
{
|
|
192
|
+
FileName = "convert",
|
|
193
|
+
Arguments = $"{filename} output.png" // Vẫn có rủi ro, cần validate input
|
|
194
|
+
};
|
|
195
|
+
Process.Start(startInfo);
|
|
196
|
+
|
|
197
|
+
// ❌ CỰC KỲ NGUY HIỂM: Truyền input của user thẳng vào Shell
|
|
198
|
+
Process.Start("sh", "-c \"echo " + userInput + "\"");
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Data Protection (Bảo vệ Dữ liệu)
|
|
202
|
+
|
|
203
|
+
### Xử lý Dữ liệu Nhạy cảm
|
|
204
|
+
- [ ] Không lưu trữ secret (password, API keys) trong source code.
|
|
205
|
+
- [ ] Secrets phải được lưu ở Environment Variables hoặc Secret Manager (Azure Key Vault, AWS Secrets Manager).
|
|
206
|
+
- [ ] Mã hóa dữ liệu nhạy cảm khi nghỉ (Encrypted at rest).
|
|
207
|
+
- [ ] Mã hóa dữ liệu khi truyền tải (HTTPS / Encrypted in transit).
|
|
208
|
+
- [ ] PII (Dữ liệu cá nhân) được xử lý theo chuẩn (GDPR, v.v.).
|
|
209
|
+
- [ ] KHÔNG log dữ liệu nhạy cảm.
|
|
210
|
+
|
|
211
|
+
### Cấu hình Bảo mật
|
|
212
|
+
```yaml
|
|
213
|
+
# ❌ Lưu lộ liễu trong file cấu hình
|
|
214
|
+
database:
|
|
215
|
+
password: "super-secret-password"
|
|
216
|
+
|
|
217
|
+
# ✅ Trỏ tới biến môi trường
|
|
218
|
+
database:
|
|
219
|
+
password: ${DATABASE_PASSWORD}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Thông báo lỗi (Error Messages)
|
|
223
|
+
```csharp
|
|
224
|
+
// ❌ Làm lộ cấu trúc hệ thống qua Exception
|
|
225
|
+
catch (Exception ex) {
|
|
226
|
+
return StatusCode(500, new {
|
|
227
|
+
error = ex.StackTrace, // Lộ mã nguồn bên trong
|
|
228
|
+
query = sqlQuery // Lộ cấu trúc DB
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ✅ Trả về thông báo lỗi chung chung (Generic)
|
|
233
|
+
catch (Exception ex) {
|
|
234
|
+
_logger.LogError(ex, "Lỗi Database"); // Log lại ở nội bộ
|
|
235
|
+
return StatusCode(500, new {
|
|
236
|
+
error = "Đã xảy ra lỗi không xác định"
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Bảo mật API
|
|
242
|
+
|
|
243
|
+
### Rate Limiting (Giới hạn tốc độ)
|
|
244
|
+
- [ ] Áp dụng Rate limiting cho tất cả public endpoint.
|
|
245
|
+
- [ ] Giới hạn gắt gao hơn cho các endpoint Login / Đăng ký.
|
|
246
|
+
- [ ] Xử lý khéo léo (Graceful handling) khi người dùng vượt quá Limit.
|
|
247
|
+
|
|
248
|
+
### Cấu hình CORS
|
|
249
|
+
```csharp
|
|
250
|
+
// ❌ CORS mở toang cửa cho tất cả (*)
|
|
251
|
+
app.UseCors(builder => builder.AllowAnyOrigin().AllowAnyMethod());
|
|
252
|
+
|
|
253
|
+
// ✅ CORS giới hạn chặt chẽ
|
|
254
|
+
app.UseCors(builder => builder
|
|
255
|
+
.WithOrigins("https://your-app.com")
|
|
256
|
+
.WithMethods("GET", "POST")
|
|
257
|
+
.AllowCredentials());
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Mật mã học (Cryptography)
|
|
261
|
+
|
|
262
|
+
### Best Practice
|
|
263
|
+
- [ ] Sử dụng các thuật toán chuẩn (AES-256, RSA-2048+).
|
|
264
|
+
- [ ] KHÔNG TỰ CHẾ thuật toán mã hóa (Not implementing custom cryptography).
|
|
265
|
+
- [ ] Dùng hàm sinh số ngẫu nhiên an toàn (Cryptographically secure random).
|
|
266
|
+
- [ ] Quản lý và luân chuyển Key (Key rotation) đàng hoàng.
|
|
267
|
+
|
|
268
|
+
### Sai lầm phổ biến
|
|
269
|
+
```csharp
|
|
270
|
+
// ❌ Sinh token ngẫu nhiên yếu (Dùng Random class)
|
|
271
|
+
var token = new Random().Next().ToString();
|
|
272
|
+
|
|
273
|
+
// ✅ Sinh ngẫu nhiên bảo mật (Cryptographically secure)
|
|
274
|
+
using var rng = RandomNumberGenerator.Create();
|
|
275
|
+
var bytes = new byte[32];
|
|
276
|
+
rng.GetBytes(bytes);
|
|
277
|
+
var token = Convert.ToBase64String(bytes);
|
|
278
|
+
|
|
279
|
+
// ❌ Dùng MD5/SHA1 để băm mật khẩu
|
|
280
|
+
var hash = MD5.HashData(Encoding.UTF8.GetBytes(password));
|
|
281
|
+
|
|
282
|
+
// ✅ Dùng BCrypt hoặc Argon2
|
|
283
|
+
var hash = BCrypt.Net.BCrypt.HashPassword(password);
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Dependency Security (Bảo mật Thư viện phụ thuộc)
|
|
287
|
+
|
|
288
|
+
### Checklist
|
|
289
|
+
- [ ] Chỉ dùng thư viện từ nguồn uy tín (Trusted sources).
|
|
290
|
+
- [ ] Quét lỗ hổng định kỳ (`dotnet list package --vulnerable`).
|
|
291
|
+
- [ ] Cập nhật thư viện thường xuyên.
|
|
292
|
+
- [ ] Xác minh giấy phép mã nguồn mở (License compliance).
|
|
293
|
+
|
|
294
|
+
## Logging & Monitoring
|
|
295
|
+
|
|
296
|
+
### Ghi Log An toàn
|
|
297
|
+
- [ ] Không log Password, Token, PII.
|
|
298
|
+
- [ ] Bảo vệ file log khỏi bị giả mạo.
|
|
299
|
+
- [ ] Log lại các sự kiện an ninh mạng (Đăng nhập sai, đổi quyền).
|
|
300
|
+
- [ ] Phòng chống Log Injection.
|
|
301
|
+
|
|
302
|
+
```csharp
|
|
303
|
+
// ❌ Log lộ thông tin nhạy cảm
|
|
304
|
+
_logger.LogInformation($"User login: {email}, password: {password}");
|
|
305
|
+
|
|
306
|
+
// ✅ Log an toàn
|
|
307
|
+
_logger.LogInformation("Thử đăng nhập", new { email, success = true });
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Security Review Severity Levels (Mức độ Nghiêm trọng)
|
|
311
|
+
|
|
312
|
+
| Severity | Mô tả | Hành động |
|
|
313
|
+
|----------|-------------|--------|
|
|
314
|
+
| 🔴 **Critical** | Có thể bị khai thác ngay lập tức, nguy cơ lộ data | Block merge, sửa NGAY LẬP TỨC |
|
|
315
|
+
| 🟡 **High** | Lỗ hổng lớn, nhưng cần điều kiện cụ thể mới khai thác được | Block merge, sửa trước khi release |
|
|
316
|
+
| 🟡 **Medium** | Rủi ro vừa phải | Nên sửa, có thể merge và track lại |
|
|
317
|
+
| 🟢 **Low** | Vấn đề nhỏ, vi phạm best practice | Nên sửa nếu có thể, không block |
|
|
318
|
+
| 💡 **Info** | Đề xuất tối ưu | Không bắt buộc |
|
|
@@ -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
|