ai-developer-skill-os 9.3.1 → 10.2.0
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/AGENTS.md +73 -40
- package/.agents/DEV_PROFILE.md +36 -2
- package/.agents/LICENSE +21 -21
- package/.agents/docs/ARCHITECTURE.md +56 -120
- package/.agents/docs/GOVERNANCE.md +3 -3
- package/.agents/docs/SPEC.md +137 -60
- package/.agents/docs/VERSIONING.md +25 -57
- package/.agents/docs/adr/0005-v10-platform-consolidation.md +50 -0
- package/.agents/docs/schemas/learning.schema.yml +22 -57
- package/.agents/docs/schemas/skill.schema.yml +116 -161
- package/.agents/docs/schemas/workflow.schema.yml +51 -26
- package/.agents/docs/skill-classification.md +1 -1
- package/.agents/registry/graph.json +193 -346
- package/.agents/registry/index.yaml +90 -233
- package/.agents/rules/coding.md +30 -12
- package/.agents/rules/command-safety.md +20 -9
- package/.agents/rules/global.md +234 -28
- package/.agents/rules/prompt-compiler.md +170 -0
- package/.agents/rules/safety.md +1 -1
- package/.agents/rules/security.md +1 -1
- package/.agents/rules/skill-quality.md +18 -3
- package/.agents/skills/_template/SKILL.md +238 -88
- package/.agents/skills/qk-api-data-discovery/SKILL.md +456 -0
- package/.agents/skills/qk-api-data-discovery/references/bronze-record-format.md +47 -0
- package/.agents/skills/qk-api-data-discovery/references/data-contract-yaml.md +72 -0
- package/.agents/skills/qk-api-data-discovery/references/discovery-report-template.md +110 -0
- package/.agents/skills/qk-backend-data/SKILL.md +339 -0
- package/.agents/skills/qk-bug-resolution/SKILL.md +328 -248
- package/.agents/skills/qk-bug-resolution/evals/scorecard.yaml +1 -1
- package/.agents/skills/qk-code-cleaner/SKILL.md +399 -0
- package/.agents/skills/qk-code-review/SKILL.md +320 -247
- package/.agents/skills/qk-code-review/evals/scorecard.yaml +1 -1
- package/.agents/skills/qk-code-review/references/ai/{v8-schema-validation.md → schema-validation.md} +2 -2
- package/.agents/skills/qk-code-review/references/cross-cutting/async-concurrency-patterns.md +515 -515
- package/.agents/skills/qk-code-review/references/cross-cutting/error-handling-principles.md +492 -492
- package/.agents/skills/qk-code-review/references/cross-cutting/n-plus-one-queries.md +309 -309
- package/.agents/skills/qk-code-review/references/cross-cutting/sql-injection-prevention.md +307 -307
- package/.agents/skills/qk-code-review/references/cross-cutting/xss-prevention.md +263 -263
- package/.agents/skills/qk-code-review/references/languages/angular.md +768 -768
- package/.agents/skills/qk-code-review/references/languages/c.md +890 -890
- package/.agents/skills/qk-code-review/references/languages/cpp.md +893 -893
- package/.agents/skills/qk-code-review/references/languages/css-less-sass.md +661 -661
- package/.agents/skills/qk-code-review/references/languages/django.md +985 -985
- package/.agents/skills/qk-code-review/references/languages/fastapi.md +580 -580
- package/.agents/skills/qk-code-review/references/languages/go.md +993 -993
- package/.agents/skills/qk-code-review/references/languages/java.md +409 -409
- package/.agents/skills/qk-code-review/references/languages/java8.md +586 -586
- package/.agents/skills/qk-code-review/references/languages/kotlin.md +1018 -1018
- package/.agents/skills/qk-code-review/references/languages/nestjs.md +593 -593
- package/.agents/skills/qk-code-review/references/languages/php.md +684 -684
- package/.agents/skills/qk-code-review/references/languages/python.md +1073 -1073
- package/.agents/skills/qk-code-review/references/languages/qt.md +757 -757
- package/.agents/skills/qk-code-review/references/languages/react.md +871 -871
- package/.agents/skills/qk-code-review/references/languages/ruby.md +964 -964
- package/.agents/skills/qk-code-review/references/languages/rust.md +846 -846
- package/.agents/skills/qk-code-review/references/languages/svelte.md +1064 -1064
- package/.agents/skills/qk-code-review/references/languages/swift.md +936 -936
- package/.agents/skills/qk-code-review/references/languages/typescript.md +1016 -1016
- package/.agents/skills/qk-code-review/references/languages/vue.md +924 -924
- package/.agents/skills/qk-code-review/references/languages/zig.md +440 -440
- package/.agents/skills/qk-devops-release/SKILL.md +294 -0
- package/.agents/skills/qk-feature-delivery/SKILL.md +331 -247
- package/.agents/skills/qk-feature-delivery/evals/scorecard.yaml +1 -1
- package/.agents/skills/qk-orchestrator/SKILL.md +286 -152
- package/.agents/skills/qk-orchestrator/evals/scorecard.yaml +1 -1
- package/.agents/skills/qk-orchestrator/references/routing-table.md +1 -1
- package/.agents/skills/qk-product-spec/SKILL.md +262 -0
- package/.agents/skills/qk-prompt-compiler/SKILL.md +444 -0
- package/.agents/skills/qk-ui-engineer/SKILL.md +286 -0
- package/.agents/workflows/_schema.yml +146 -146
- package/.agents/workflows/bug-resolution.yml +155 -121
- package/.agents/workflows/code-review.yml +127 -93
- package/.agents/workflows/context-discovery.yml +128 -94
- package/.agents/workflows/documentation.yml +124 -90
- package/.agents/workflows/feature-delivery.yml +158 -124
- package/.agents/workflows/production-release.yml +207 -173
- package/.agents/workflows/prompt-compilation.yml +126 -0
- package/.agents/workflows/refactor.yml +136 -102
- package/.agents/workflows/security-audit.yml +149 -115
- package/.agents/workflows/shared/quality-gate.yml +3 -1
- package/.agents/workflows/skin-governance.yml +149 -115
- package/.agents/workflows/spec-driven-development.yml +116 -87
- package/CHANGELOG.md +77 -0
- package/README.md +125 -205
- package/bin/install.js +324 -329
- package/package.json +68 -74
- package/tooling/build-registry.js +226 -208
- package/tooling/run-aar.js +55 -126
- package/tooling/sync-versions.js +2 -2
- package/tooling/validate-graph.js +100 -87
- package/tooling/validate-skills.js +32 -14
- package/.agents/README.md +0 -90
- package/.agents/docs/CHI_TIET_SKILLS.md +0 -126
- package/.agents/docs/HUONG_DAN_SU_DUNG.md +0 -120
- package/.agents/docs/MIGRATION-CLEANUP-V8.1.3.md +0 -36
- package/.agents/docs/MIGRATION-STATUS.md +0 -35
- package/.agents/docs/MIGRATION-V8.md +0 -10
- package/.agents/docs/ROADMAP-V8.2.md +0 -78
- package/.agents/docs/V8-CERTIFICATION.md +0 -27
- package/.agents/docs/decisions/ADR-001-v8-migration.md +0 -58
- package/.agents/docs/decisions/ADR-002-workflow-separation.md +0 -50
- package/.agents/docs/decisions/ADR-003-registry-generated.md +0 -54
- package/.agents/docs/decisions/ADR-008-skill-boundary-review.md +0 -27
- package/.agents/registry/capability-graph.yml +0 -390
- package/.agents/registry/skills-index.yml +0 -305
- package/.agents/skills/_template/capability.yaml +0 -34
- package/.agents/skills/_template/evals/scorecard.yaml +0 -19
- package/.agents/skills/qk-access-policy/SKILL.md +0 -206
- package/.agents/skills/qk-access-policy/capability.yaml +0 -23
- package/.agents/skills/qk-access-policy/evals/scorecard.yaml +0 -36
- package/.agents/skills/qk-agent-observability/SKILL.md +0 -108
- package/.agents/skills/qk-agent-observability/capability.yaml +0 -29
- package/.agents/skills/qk-agent-observability/evals/scorecard.yaml +0 -29
- package/.agents/skills/qk-agent-observability/references/scorecard.yaml +0 -80
- package/.agents/skills/qk-ai-builder/SKILL.md +0 -254
- package/.agents/skills/qk-ai-builder/capability.yaml +0 -23
- package/.agents/skills/qk-ai-builder/evals/scorecard.yaml +0 -30
- package/.agents/skills/qk-api-consumer/SKILL.md +0 -256
- package/.agents/skills/qk-api-consumer/capability.yaml +0 -21
- package/.agents/skills/qk-api-consumer/evals/scorecard.yaml +0 -29
- package/.agents/skills/qk-api-lifecycle/SKILL.md +0 -251
- package/.agents/skills/qk-api-lifecycle/capability.yaml +0 -23
- package/.agents/skills/qk-api-lifecycle/evals/scorecard.yaml +0 -29
- package/.agents/skills/qk-bug-resolution/capability.yaml +0 -25
- package/.agents/skills/qk-code-review/capability.yaml +0 -23
- package/.agents/skills/qk-context-loader/SKILL.md +0 -198
- package/.agents/skills/qk-context-loader/capability.yaml +0 -23
- package/.agents/skills/qk-context-loader/evals/scorecard.yaml +0 -28
- package/.agents/skills/qk-data-engineer/SKILL.md +0 -253
- package/.agents/skills/qk-data-lifecycle/SKILL.md +0 -197
- package/.agents/skills/qk-data-lifecycle/capability.yaml +0 -23
- package/.agents/skills/qk-data-lifecycle/evals/scorecard.yaml +0 -29
- package/.agents/skills/qk-db-optimizer/SKILL.md +0 -210
- package/.agents/skills/qk-db-optimizer/capability.yaml +0 -22
- package/.agents/skills/qk-db-optimizer/evals/scorecard.yaml +0 -28
- package/.agents/skills/qk-design-system-engineering/SKILL.md +0 -193
- package/.agents/skills/qk-design-system-engineering/capability.yaml +0 -25
- package/.agents/skills/qk-design-system-engineering/evals/scorecard.yaml +0 -27
- package/.agents/skills/qk-devops-platform/SKILL.md +0 -198
- package/.agents/skills/qk-devops-platform/capability.yaml +0 -29
- package/.agents/skills/qk-devops-platform/evals/scorecard.yaml +0 -28
- package/.agents/skills/qk-docs/SKILL.md +0 -193
- package/.agents/skills/qk-docs/capability.yaml +0 -23
- package/.agents/skills/qk-docs/evals/scorecard.yaml +0 -27
- package/.agents/skills/qk-engineering-standard/SKILL.md +0 -89
- package/.agents/skills/qk-engineering-standard/capability.yaml +0 -23
- package/.agents/skills/qk-engineering-standard/evals/scorecard.yaml +0 -28
- package/.agents/skills/qk-engineering-standard/references/anti-patterns.md +0 -121
- package/.agents/skills/qk-engineering-standard/rules/backend.md +0 -122
- package/.agents/skills/qk-engineering-standard/rules/database.md +0 -3
- package/.agents/skills/qk-engineering-standard/rules/frontend.md +0 -152
- package/.agents/skills/qk-engineering-standard/rules/security.md +0 -3
- package/.agents/skills/qk-engineering-standard/rules/testing.md +0 -3
- package/.agents/skills/qk-fe-api-integration/SKILL.md +0 -704
- package/.agents/skills/qk-fe-api-integration/capability.yaml +0 -21
- package/.agents/skills/qk-fe-api-integration/evals/scorecard.yaml +0 -29
- package/.agents/skills/qk-feature-delivery/capability.yaml +0 -24
- package/.agents/skills/qk-frontend-architecture/SKILL.md +0 -127
- package/.agents/skills/qk-frontend-architecture/capability.yaml +0 -28
- package/.agents/skills/qk-frontend-architecture/evals/scorecard.yaml +0 -28
- package/.agents/skills/qk-help/SKILL.md +0 -107
- package/.agents/skills/qk-help/capability.yaml +0 -20
- package/.agents/skills/qk-help/evals/scorecard.yaml +0 -13
- package/.agents/skills/qk-orchestrator/capability.yaml +0 -22
- package/.agents/skills/qk-product-specification/SKILL.md +0 -187
- package/.agents/skills/qk-product-specification/capability.yaml +0 -27
- package/.agents/skills/qk-product-specification/evals/scorecard.yaml +0 -27
- package/.agents/skills/qk-production-release/SKILL.md +0 -188
- package/.agents/skills/qk-production-release/capability.yaml +0 -27
- package/.agents/skills/qk-production-release/evals/scorecard.yaml +0 -28
- package/.agents/skills/qk-project-audit/SKILL.md +0 -174
- package/.agents/skills/qk-project-bootstrap/SKILL.md +0 -372
- package/.agents/skills/qk-project-bootstrap/capability.yaml +0 -23
- package/.agents/skills/qk-project-bootstrap/evals/scorecard.yaml +0 -28
- package/.agents/skills/qk-project-health/SKILL.md +0 -202
- package/.agents/skills/qk-project-health/capability.yaml +0 -23
- package/.agents/skills/qk-project-health/evals/scorecard.yaml +0 -27
- package/.agents/skills/qk-project-memory/SKILL.md +0 -303
- package/.agents/skills/qk-project-memory/capability.yaml +0 -23
- package/.agents/skills/qk-project-memory/evals/scorecard.yaml +0 -27
- package/.agents/skills/qk-refactor/SKILL.md +0 -243
- package/.agents/skills/qk-refactor/capability.yaml +0 -26
- package/.agents/skills/qk-refactor/evals/scorecard.yaml +0 -27
- package/.agents/skills/qk-security-audit/SKILL.md +0 -280
- package/.agents/skills/qk-security-audit/capability.yaml +0 -29
- package/.agents/skills/qk-security-audit/evals/scorecard.yaml +0 -27
- package/.agents/skills/qk-system-evolution/SKILL.md +0 -625
- package/.agents/skills/qk-system-evolution/capability.yaml +0 -24
- package/.agents/skills/qk-system-evolution/evals/scorecard.yaml +0 -26
- package/.agents/skills/qk-test-engineering/SKILL.md +0 -215
- package/.agents/skills/qk-test-engineering/capability.yaml +0 -28
- package/.agents/skills/qk-test-engineering/evals/scorecard.yaml +0 -26
- package/.agents/skills/qk-ui-audit/SKILL.md +0 -175
- package/.agents/skills/qk-ui-audit/capability.yaml +0 -23
- package/.agents/skills/qk-ui-audit/evals/scorecard.yaml +0 -26
- package/.agents/skills/qk-ui-audit/references/anti-slop-checklist.md +0 -136
- package/.agents/skills/qk-ui-builder/SKILL.md +0 -521
- package/.agents/skills/qk-ui-builder/capability.yaml +0 -29
- package/.agents/skills/qk-ui-builder/evals/scorecard.yaml +0 -26
- package/.agents/skills/qk-ui-builder/references/anti-patterns.md +0 -295
- package/.agents/skills/qk-ui-builder/references/color.md +0 -115
- package/.agents/skills/qk-ui-builder/references/component-cookbook.md +0 -458
- package/.agents/skills/qk-ui-builder/references/copy.md +0 -250
- package/.agents/skills/qk-ui-builder/references/interaction-and-states.md +0 -115
- package/.agents/skills/qk-ui-builder/references/layout-and-space.md +0 -111
- package/.agents/skills/qk-ui-builder/references/macrostructures/01-bento-grid.md +0 -48
- package/.agents/skills/qk-ui-builder/references/macrostructures/02-long-document.md +0 -50
- package/.agents/skills/qk-ui-builder/references/macrostructures/03-marquee-hero.md +0 -51
- package/.agents/skills/qk-ui-builder/references/macrostructures/04-stat-led.md +0 -49
- package/.agents/skills/qk-ui-builder/references/macrostructures/05-workbench.md +0 -44
- package/.agents/skills/qk-ui-builder/references/macrostructures/06-conversational-faq.md +0 -50
- package/.agents/skills/qk-ui-builder/references/macrostructures/07-manifesto.md +0 -51
- package/.agents/skills/qk-ui-builder/references/macrostructures/08-photographic.md +0 -50
- package/.agents/skills/qk-ui-builder/references/macrostructures/09-quote-led.md +0 -50
- package/.agents/skills/qk-ui-builder/references/macrostructures/11-catalogue.md +0 -49
- package/.agents/skills/qk-ui-builder/references/macrostructures/12-letter.md +0 -49
- package/.agents/skills/qk-ui-builder/references/macrostructures/13-index-first.md +0 -49
- package/.agents/skills/qk-ui-builder/references/macrostructures/14-narrative-workflow.md +0 -48
- package/.agents/skills/qk-ui-builder/references/macrostructures/15-split-studio.md +0 -48
- package/.agents/skills/qk-ui-builder/references/macrostructures/16-feature-stack.md +0 -51
- package/.agents/skills/qk-ui-builder/references/macrostructures/17-type-specimen.md +0 -48
- package/.agents/skills/qk-ui-builder/references/macrostructures/18-portfolio-grid.md +0 -48
- package/.agents/skills/qk-ui-builder/references/macrostructures/19-map-diagram.md +0 -50
- package/.agents/skills/qk-ui-builder/references/macrostructures/20-ecosystem-index.md +0 -48
- package/.agents/skills/qk-ui-builder/references/macrostructures/21-component-playground.md +0 -45
- package/.agents/skills/qk-ui-builder/references/macrostructures.md +0 -38
- package/.agents/skills/qk-ui-builder/references/motion.md +0 -95
- package/.agents/skills/qk-ui-builder/references/responsive.md +0 -115
- package/.agents/skills/qk-ui-builder/references/slop-test.md +0 -135
- package/.agents/skills/qk-ui-builder/references/structure.md +0 -280
- package/.agents/skills/qk-ui-builder/references/themes/atmospheric.md +0 -53
- package/.agents/skills/qk-ui-builder/references/themes/carnival.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/cobalt.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/editorial.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/garden.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/hum.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/lumen.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/midnight.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/modern-minimal.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/playful.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/specimen.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/terminal.md +0 -52
- package/.agents/skills/qk-ui-builder/references/typography.md +0 -129
- package/.agents/skills/qk-ui-system-builder/SKILL.md +0 -183
- package/.agents/skills/qk-ui-system-builder/capability.yaml +0 -25
- package/.agents/skills/qk-ui-system-builder/evals/scorecard.yaml +0 -26
- package/.agents/skills/qk-upgrade/SKILL.md +0 -301
- package/.agents/skills/qk-upgrade/capability.yaml +0 -24
- package/.agents/skills/qk-upgrade/evals/scorecard.yaml +0 -26
- package/.agents/skills/qk-validation-gate/SKILL.md +0 -88
- package/.agents/skills/qk-validation-gate/capability.yaml +0 -23
- package/.agents/skills/qk-validation-gate/evals/scorecard.yaml +0 -26
- package/.agents/skills/qk-web-quality-gate/SKILL.md +0 -197
- package/.agents/skills/qk-web-quality-gate/capability.yaml +0 -24
- package/.agents/skills/qk-web-quality-gate/evals/scorecard.yaml +0 -26
- package/.agents/workflows/research.yml +0 -75
- package/.agents/workflows/skill-evolution.yml +0 -97
- package/tooling/fix-refactor.js +0 -8
|
@@ -1,308 +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
|
|
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
308
|
- [ ] Static analysis tools (Bandit, SpotBugs, SonarQube) run in CI
|