@brunosps00/dev-workflow 0.4.7 → 0.6.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/README.md +33 -6
- package/lib/constants.js +6 -0
- package/lib/install-deps.js +39 -1
- package/package.json +1 -1
- package/scaffold/en/commands/dw-adr.md +117 -0
- package/scaffold/en/commands/dw-autopilot.md +7 -0
- package/scaffold/en/commands/dw-brainstorm.md +6 -0
- package/scaffold/en/commands/dw-bugfix.md +9 -0
- package/scaffold/en/commands/dw-code-review.md +28 -0
- package/scaffold/en/commands/dw-create-tasks.md +12 -0
- package/scaffold/en/commands/dw-create-techspec.md +8 -0
- package/scaffold/en/commands/dw-fix-qa.md +5 -0
- package/scaffold/en/commands/dw-generate-pr.md +11 -0
- package/scaffold/en/commands/dw-help.md +44 -3
- package/scaffold/en/commands/dw-quick.md +8 -1
- package/scaffold/en/commands/dw-refactoring-analysis.md +1 -0
- package/scaffold/en/commands/dw-resume.md +10 -3
- package/scaffold/en/commands/dw-revert-task.md +114 -0
- package/scaffold/en/commands/dw-review-implementation.md +17 -0
- package/scaffold/en/commands/dw-run-plan.md +19 -1
- package/scaffold/en/commands/dw-run-task.md +14 -1
- package/scaffold/en/commands/dw-security-check.md +271 -0
- package/scaffold/en/commands/dw-update.md +39 -0
- package/scaffold/en/templates/adr-template.md +56 -0
- package/scaffold/en/templates/prd-template.md +12 -0
- package/scaffold/en/templates/task-template.md +12 -0
- package/scaffold/en/templates/tasks-template.md +6 -0
- package/scaffold/en/templates/techspec-template.md +12 -0
- package/scaffold/pt-br/commands/dw-adr.md +117 -0
- package/scaffold/pt-br/commands/dw-autopilot.md +7 -0
- package/scaffold/pt-br/commands/dw-brainstorm.md +6 -0
- package/scaffold/pt-br/commands/dw-bugfix.md +9 -0
- package/scaffold/pt-br/commands/dw-code-review.md +28 -0
- package/scaffold/pt-br/commands/dw-create-tasks.md +12 -0
- package/scaffold/pt-br/commands/dw-create-techspec.md +8 -0
- package/scaffold/pt-br/commands/dw-fix-qa.md +5 -0
- package/scaffold/pt-br/commands/dw-generate-pr.md +11 -0
- package/scaffold/pt-br/commands/dw-help.md +44 -3
- package/scaffold/pt-br/commands/dw-quick.md +8 -1
- package/scaffold/pt-br/commands/dw-refactoring-analysis.md +1 -0
- package/scaffold/pt-br/commands/dw-resume.md +10 -3
- package/scaffold/pt-br/commands/dw-revert-task.md +114 -0
- package/scaffold/pt-br/commands/dw-review-implementation.md +17 -0
- package/scaffold/pt-br/commands/dw-run-plan.md +19 -1
- package/scaffold/pt-br/commands/dw-run-task.md +14 -1
- package/scaffold/pt-br/commands/dw-security-check.md +271 -0
- package/scaffold/pt-br/commands/dw-update.md +40 -0
- package/scaffold/pt-br/templates/adr-template.md +56 -0
- package/scaffold/pt-br/templates/prd-template.md +12 -0
- package/scaffold/pt-br/templates/task-template.md +12 -0
- package/scaffold/pt-br/templates/tasks-template.md +6 -0
- package/scaffold/pt-br/templates/techspec-template.md +12 -0
- package/scaffold/skills/dw-council/SKILL.md +189 -0
- package/scaffold/skills/dw-council/agents/architect-advisor.md +44 -0
- package/scaffold/skills/dw-council/agents/devils-advocate.md +45 -0
- package/scaffold/skills/dw-council/agents/pragmatic-engineer.md +47 -0
- package/scaffold/skills/dw-council/agents/product-mind.md +48 -0
- package/scaffold/skills/dw-council/agents/security-advocate.md +48 -0
- package/scaffold/skills/dw-memory/SKILL.md +178 -0
- package/scaffold/skills/dw-review-rigor/SKILL.md +145 -0
- package/scaffold/skills/dw-verify/SKILL.md +196 -0
- package/scaffold/skills/security-review/languages/csharp.md +507 -0
- package/scaffold/skills/security-review/languages/rust.md +584 -0
- package/scaffold/skills/security-review/languages/typescript.md +373 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
# Rust Security Patterns
|
|
2
|
+
|
|
3
|
+
Covers **Actix Web, Axum, Rocket, Warp, Tonic (gRPC), Tower, Tokio, sqlx, Diesel, SeaORM, serde, reqwest, hyper, std::process, std::fs, unsafe blocks, FFI, and cargo supply chain**. Used by `/dw-security-check` as the primary reference when Rust is detected in scope.
|
|
4
|
+
|
|
5
|
+
> Rust's ownership and borrow checker eliminate **memory-safety** classes of bugs (use-after-free, data races, buffer overflows) — but **logic bugs, misuse of `unsafe`, DoS via panic, injection into string-APIs, and supply-chain compromise are still present**. Do not assume "Rust = secure".
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Framework Detection
|
|
10
|
+
|
|
11
|
+
| Indicator | Framework / Crate |
|
|
12
|
+
|-----------|-------------------|
|
|
13
|
+
| `actix_web::`, `#[actix_web::main]`, `App::new()` | Actix Web |
|
|
14
|
+
| `axum::`, `Router::new()`, `#[tokio::main]` + axum | Axum |
|
|
15
|
+
| `rocket::`, `#[rocket::main]`, `#[get("/...")]` | Rocket |
|
|
16
|
+
| `warp::`, `warp::Filter` | Warp |
|
|
17
|
+
| `tonic::`, `.proto` + `build.rs` with tonic_build | Tonic (gRPC) |
|
|
18
|
+
| `sqlx::`, `sqlx::query!`, `PgPool` | sqlx |
|
|
19
|
+
| `diesel::`, `schema.rs` | Diesel |
|
|
20
|
+
| `sea_orm::` | SeaORM |
|
|
21
|
+
| `serde::{Serialize, Deserialize}` | serde |
|
|
22
|
+
| `reqwest::Client` | reqwest |
|
|
23
|
+
| `tokio::` | tokio runtime |
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## `unsafe` Blocks
|
|
28
|
+
|
|
29
|
+
`unsafe` disables a subset of the borrow checker's guarantees. Every `unsafe` block is a place where memory safety becomes the programmer's responsibility.
|
|
30
|
+
|
|
31
|
+
### Always Flag (for review, not always critical)
|
|
32
|
+
|
|
33
|
+
```rust
|
|
34
|
+
// FLAG HIGH: unsafe with pointer dereference on externally-derived data
|
|
35
|
+
unsafe { *raw_ptr = user_value; }
|
|
36
|
+
|
|
37
|
+
// FLAG HIGH: transmute between types
|
|
38
|
+
let x: u32 = unsafe { std::mem::transmute(user_bytes) };
|
|
39
|
+
|
|
40
|
+
// FLAG CRITICAL: unsafe { std::mem::uninitialized() } or zeroed() on types that require init
|
|
41
|
+
let v: Vec<String> = unsafe { std::mem::zeroed() }; // UB on drop
|
|
42
|
+
|
|
43
|
+
// FLAG HIGH: unsafe fn without safety contract documented
|
|
44
|
+
unsafe fn do_thing(p: *const u8) { /* no # Safety doc */ }
|
|
45
|
+
|
|
46
|
+
// FLAG CRITICAL: `unsafe impl Send/Sync` on a non-thread-safe type
|
|
47
|
+
unsafe impl Send for MyCell {} // MyCell has RefCell internally
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Safe Review Pattern
|
|
51
|
+
|
|
52
|
+
Every `unsafe` block should have an adjacent `// SAFETY:` comment explaining why the invariants hold. Flag unsafe blocks with no SAFETY comment — they indicate lack of review.
|
|
53
|
+
|
|
54
|
+
```rust
|
|
55
|
+
// SAFE: documented safety contract
|
|
56
|
+
// SAFETY: `ptr` is non-null because checked above; length fits allocation checked at line 42.
|
|
57
|
+
unsafe { std::slice::from_raw_parts(ptr, len) }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Panic-Based DoS
|
|
63
|
+
|
|
64
|
+
A panic on the request path can crash a worker or bring down a service. Attacker-controlled input should never be able to trigger a panic.
|
|
65
|
+
|
|
66
|
+
### Always Flag
|
|
67
|
+
|
|
68
|
+
```rust
|
|
69
|
+
// FLAG HIGH: .unwrap() on external input
|
|
70
|
+
let id: i64 = req.query.get("id").unwrap().parse().unwrap();
|
|
71
|
+
|
|
72
|
+
// FLAG HIGH: .expect(...) on external input
|
|
73
|
+
let user: User = serde_json::from_str(&body).expect("valid user");
|
|
74
|
+
|
|
75
|
+
// FLAG HIGH: array indexing with user index
|
|
76
|
+
let item = &items[req.body.index]; // panic on out-of-bounds
|
|
77
|
+
|
|
78
|
+
// FLAG HIGH: integer arithmetic overflow with user input (in release, wraps; in debug, panics — inconsistent)
|
|
79
|
+
let total = user_qty * user_price; // should use checked_mul / saturating_mul
|
|
80
|
+
|
|
81
|
+
// FLAG HIGH: slice range with user bounds
|
|
82
|
+
let window = &data[user_start..user_end]; // panic on invalid range
|
|
83
|
+
|
|
84
|
+
// FLAG MEDIUM: assert!/assert_eq! on external invariants
|
|
85
|
+
assert!(body.len() < 1024); // crashes the worker
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Safe Patterns
|
|
89
|
+
|
|
90
|
+
```rust
|
|
91
|
+
// SAFE: proper error handling
|
|
92
|
+
let id: i64 = req.query.get("id")
|
|
93
|
+
.ok_or(AppError::MissingParam("id"))?
|
|
94
|
+
.parse()
|
|
95
|
+
.map_err(|_| AppError::BadInput("id"))?;
|
|
96
|
+
|
|
97
|
+
// SAFE: checked arithmetic
|
|
98
|
+
let total = user_qty.checked_mul(user_price).ok_or(AppError::Overflow)?;
|
|
99
|
+
|
|
100
|
+
// SAFE: bounds check first
|
|
101
|
+
let item = items.get(req.body.index).ok_or(AppError::NotFound)?;
|
|
102
|
+
|
|
103
|
+
// SAFE: validated range
|
|
104
|
+
if user_start > user_end || user_end > data.len() {
|
|
105
|
+
return Err(AppError::BadRange);
|
|
106
|
+
}
|
|
107
|
+
let window = &data[user_start..user_end];
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## SQL Injection
|
|
113
|
+
|
|
114
|
+
### sqlx
|
|
115
|
+
|
|
116
|
+
```rust
|
|
117
|
+
// FLAG CRITICAL: query_unchecked with interpolation
|
|
118
|
+
sqlx::query_unchecked(&format!("SELECT * FROM users WHERE name = '{}'", user_name))
|
|
119
|
+
.fetch_all(&pool).await?;
|
|
120
|
+
|
|
121
|
+
// FLAG CRITICAL: query() with format!
|
|
122
|
+
sqlx::query(&format!("SELECT * FROM u WHERE id = {}", id))
|
|
123
|
+
.fetch_one(&pool).await?;
|
|
124
|
+
|
|
125
|
+
// SAFE: query! macro (compile-time checked + bound parameters)
|
|
126
|
+
sqlx::query!("SELECT * FROM users WHERE name = $1", user_name)
|
|
127
|
+
.fetch_all(&pool).await?;
|
|
128
|
+
|
|
129
|
+
// SAFE: query() with .bind()
|
|
130
|
+
sqlx::query("SELECT * FROM users WHERE name = $1")
|
|
131
|
+
.bind(user_name)
|
|
132
|
+
.fetch_all(&pool).await?;
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Diesel
|
|
136
|
+
|
|
137
|
+
```rust
|
|
138
|
+
// FLAG CRITICAL: sql_query with format!
|
|
139
|
+
diesel::sql_query(format!("SELECT * FROM users WHERE email = '{}'", user_email))
|
|
140
|
+
.load::<User>(conn)?;
|
|
141
|
+
|
|
142
|
+
// SAFE: sql_query with bind
|
|
143
|
+
diesel::sql_query("SELECT * FROM users WHERE email = $1")
|
|
144
|
+
.bind::<Text, _>(user_email)
|
|
145
|
+
.load::<User>(conn)?;
|
|
146
|
+
|
|
147
|
+
// SAFE: DSL (always parameterized)
|
|
148
|
+
use schema::users::dsl::*;
|
|
149
|
+
users.filter(email.eq(user_email)).first::<User>(conn)?;
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### SeaORM
|
|
153
|
+
|
|
154
|
+
```rust
|
|
155
|
+
// FLAG CRITICAL: Statement::from_string with format!
|
|
156
|
+
Statement::from_string(DbBackend::Postgres,
|
|
157
|
+
format!("SELECT * FROM users WHERE id = {}", id));
|
|
158
|
+
|
|
159
|
+
// SAFE: Statement::from_sql_and_values
|
|
160
|
+
Statement::from_sql_and_values(DbBackend::Postgres,
|
|
161
|
+
"SELECT * FROM users WHERE id = $1", [id.into()]);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## XSS / Template Injection
|
|
167
|
+
|
|
168
|
+
### Actix / Axum / Rocket with template engines
|
|
169
|
+
|
|
170
|
+
```rust
|
|
171
|
+
// FLAG CRITICAL: Tera autoescape off
|
|
172
|
+
let mut tera = Tera::new("templates/**/*")?;
|
|
173
|
+
tera.autoescape_on(vec![]); // disabled globally
|
|
174
|
+
|
|
175
|
+
// FLAG CRITICAL: | safe filter on user input in Tera
|
|
176
|
+
// template: <div>{{ user_input | safe }}</div>
|
|
177
|
+
|
|
178
|
+
// FLAG CRITICAL: Askama escape="none" on user input
|
|
179
|
+
// template: {{ user_input|safe }}
|
|
180
|
+
|
|
181
|
+
// FLAG HIGH: writing unescaped HTML to response body
|
|
182
|
+
HttpResponse::Ok()
|
|
183
|
+
.content_type("text/html")
|
|
184
|
+
.body(format!("<div>{}</div>", user_input)); // raw concat
|
|
185
|
+
|
|
186
|
+
// SAFE: Tera autoescape on (default) + no |safe filter on user input
|
|
187
|
+
// SAFE: Askama with default escape="html"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Response content-type
|
|
191
|
+
|
|
192
|
+
```rust
|
|
193
|
+
// FLAG HIGH: returning user HTML without setting safe content-type
|
|
194
|
+
HttpResponse::Ok().body(user_html); // browsers sniff and render
|
|
195
|
+
|
|
196
|
+
// SAFE
|
|
197
|
+
HttpResponse::Ok()
|
|
198
|
+
.content_type("text/plain; charset=utf-8")
|
|
199
|
+
.body(user_text);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Deserialization
|
|
205
|
+
|
|
206
|
+
### serde + untrusted types
|
|
207
|
+
|
|
208
|
+
```rust
|
|
209
|
+
// FLAG HIGH: untagged enums with recursive types from user JSON
|
|
210
|
+
#[derive(Deserialize)]
|
|
211
|
+
#[serde(untagged)]
|
|
212
|
+
enum Node { Leaf(String), Branch(Vec<Node>) } // DoS via deep nesting
|
|
213
|
+
|
|
214
|
+
// FLAG HIGH: deserializing unbounded containers from untrusted source
|
|
215
|
+
#[derive(Deserialize)]
|
|
216
|
+
struct Req { items: Vec<Big> } // attacker sends N=10M items
|
|
217
|
+
|
|
218
|
+
// FLAG HIGH: serde_yaml::from_str (older versions had billion-laughs / alias-bomb)
|
|
219
|
+
let cfg: Cfg = serde_yaml::from_str(user_yaml)?;
|
|
220
|
+
// Prefer serde_yml (maintained fork) OR bound input size + depth limits
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Safe Patterns
|
|
224
|
+
|
|
225
|
+
```rust
|
|
226
|
+
// SAFE: explicit size limit on request body (Actix)
|
|
227
|
+
App::new().app_data(web::JsonConfig::default().limit(1024 * 1024)); // 1MB
|
|
228
|
+
|
|
229
|
+
// SAFE: Axum body limit
|
|
230
|
+
Router::new()
|
|
231
|
+
.route("/api", post(handler))
|
|
232
|
+
.layer(DefaultBodyLimit::max(1024 * 1024));
|
|
233
|
+
|
|
234
|
+
// SAFE: custom Deserialize with bounds
|
|
235
|
+
#[derive(Deserialize)]
|
|
236
|
+
#[serde(deny_unknown_fields)]
|
|
237
|
+
struct Req { #[serde(default)] #[serde(deserialize_with = "bounded_vec")] items: Vec<Item> }
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Authentication / Authorization
|
|
243
|
+
|
|
244
|
+
### Flag These
|
|
245
|
+
|
|
246
|
+
```rust
|
|
247
|
+
// FLAG CRITICAL: JWT verify with algorithm::any / no algo specified
|
|
248
|
+
let token = jsonwebtoken::decode::<Claims>(token, &key, &Validation::default())?;
|
|
249
|
+
// Validation::default() in jsonwebtoken 9.x is safe (HS256 default), but inspect if overridden
|
|
250
|
+
// FLAG CRITICAL if set_audience=false, set_issuer=false, insecure_disable_signature_validation=true
|
|
251
|
+
|
|
252
|
+
// FLAG CRITICAL: insecure_disable_signature_validation
|
|
253
|
+
let mut val = Validation::default();
|
|
254
|
+
val.insecure_disable_signature_validation(); // FLAG CRITICAL
|
|
255
|
+
|
|
256
|
+
// FLAG HIGH: middleware that only checks presence of header, not value
|
|
257
|
+
async fn auth(req: Request, next: Next) -> Response {
|
|
258
|
+
if req.headers().contains_key("authorization") { // FLAG: doesn't verify
|
|
259
|
+
next.run(req).await
|
|
260
|
+
} else {
|
|
261
|
+
StatusCode::UNAUTHORIZED.into_response()
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// FLAG HIGH: password hashing with non-password-KDF
|
|
266
|
+
use sha2::{Sha256, Digest};
|
|
267
|
+
let hash = Sha256::digest(password.as_bytes()); // FLAG: fast hash for passwords
|
|
268
|
+
|
|
269
|
+
// SAFE: argon2 / bcrypt / scrypt
|
|
270
|
+
use argon2::Argon2;
|
|
271
|
+
let hash = argon2.hash_password(password.as_bytes(), &salt)?;
|
|
272
|
+
|
|
273
|
+
// FLAG HIGH: cookie without secure/httponly/samesite
|
|
274
|
+
cookie::Cookie::build("session", token).finish();
|
|
275
|
+
// SAFE
|
|
276
|
+
cookie::Cookie::build("session", token)
|
|
277
|
+
.http_only(true).secure(true).same_site(SameSite::Strict).finish();
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Actix middleware order / Axum layer order
|
|
281
|
+
|
|
282
|
+
```rust
|
|
283
|
+
// FLAG HIGH: authorization layer applied BEFORE authentication
|
|
284
|
+
Router::new()
|
|
285
|
+
.route("/admin/*", get(admin))
|
|
286
|
+
.layer(RequireRole("admin")) // FLAG: runs even without auth
|
|
287
|
+
.layer(AuthLayer); // too late (layers apply bottom-up, but order matters per use)
|
|
288
|
+
|
|
289
|
+
// Inspect the actual layer order — tower layers execute in reverse declaration order
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Command Injection / Process Execution
|
|
295
|
+
|
|
296
|
+
```rust
|
|
297
|
+
// FLAG CRITICAL: Command::new("sh") with user string
|
|
298
|
+
std::process::Command::new("sh")
|
|
299
|
+
.arg("-c")
|
|
300
|
+
.arg(format!("git log {}", user_branch))
|
|
301
|
+
.output()?;
|
|
302
|
+
|
|
303
|
+
// FLAG CRITICAL: any shell=true equivalent via "/bin/sh -c"
|
|
304
|
+
Command::new("/bin/bash").arg("-c").arg(user_cmd);
|
|
305
|
+
|
|
306
|
+
// SAFE: exec the binary directly with args array (no shell)
|
|
307
|
+
std::process::Command::new("git")
|
|
308
|
+
.arg("log")
|
|
309
|
+
.arg(user_branch)
|
|
310
|
+
.output()?;
|
|
311
|
+
// Still validate user_branch against a ref-name regex
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Path Traversal
|
|
317
|
+
|
|
318
|
+
```rust
|
|
319
|
+
// FLAG CRITICAL: user path concatenated
|
|
320
|
+
let path = format!("/var/data/{}", user_file);
|
|
321
|
+
std::fs::read(&path)?;
|
|
322
|
+
|
|
323
|
+
// FLAG CRITICAL: Path::new().join() is NOT containment
|
|
324
|
+
let p = std::path::Path::new("/var/data").join(user_file); // join("/etc/passwd") → /etc/passwd
|
|
325
|
+
|
|
326
|
+
// SAFE: canonicalize + contained check
|
|
327
|
+
let base = std::path::Path::new("/var/data").canonicalize()?;
|
|
328
|
+
let full = base.join(user_file).canonicalize()?;
|
|
329
|
+
if !full.starts_with(&base) {
|
|
330
|
+
return Err(anyhow!("path traversal"));
|
|
331
|
+
}
|
|
332
|
+
std::fs::read(&full)?;
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## SSRF
|
|
338
|
+
|
|
339
|
+
```rust
|
|
340
|
+
// FLAG HIGH: reqwest with attacker-controlled URL
|
|
341
|
+
let body = reqwest::get(&user_url).await?.text().await?;
|
|
342
|
+
|
|
343
|
+
// FLAG HIGH: hyper::Client with user URI
|
|
344
|
+
// FLAG HIGH: Redirect following enabled on attacker-URL (default on reqwest)
|
|
345
|
+
|
|
346
|
+
// SAFE: allowlist host + block metadata IPs (169.254.169.254, fd00::/8, etc.)
|
|
347
|
+
let url = reqwest::Url::parse(&user_url)?;
|
|
348
|
+
let host = url.host_str().ok_or_else(|| anyhow!("no host"))?;
|
|
349
|
+
if !ALLOWED_HOSTS.contains(host) { return Err(anyhow!("host not allowed")); }
|
|
350
|
+
// Also disable redirects OR re-validate each hop
|
|
351
|
+
let client = reqwest::Client::builder().redirect(reqwest::redirect::Policy::none()).build()?;
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Cryptography
|
|
357
|
+
|
|
358
|
+
### Flag These
|
|
359
|
+
|
|
360
|
+
```rust
|
|
361
|
+
// FLAG CRITICAL: weak hash for passwords
|
|
362
|
+
use md5::Md5; use sha1::Sha1;
|
|
363
|
+
let h = Md5::digest(password);
|
|
364
|
+
let h = Sha1::digest(password);
|
|
365
|
+
|
|
366
|
+
// FLAG HIGH: non-crypto RNG for tokens/session IDs/IVs
|
|
367
|
+
use rand::Rng;
|
|
368
|
+
let token = rand::thread_rng().gen::<u64>(); // OK in modern versions (uses ThreadRng which is CSPRNG)
|
|
369
|
+
// But:
|
|
370
|
+
use rand::rngs::mock::StepRng; // FLAG if used for security
|
|
371
|
+
use oorandom::Rand64; // FLAG: not cryptographic
|
|
372
|
+
|
|
373
|
+
// FLAG HIGH: AES-ECB mode
|
|
374
|
+
use aes::cipher::generic_array::GenericArray;
|
|
375
|
+
// aes::Aes128 in ECB mode directly without a proper mode wrapper
|
|
376
|
+
|
|
377
|
+
// FLAG HIGH: static IV / zeros IV / predictable nonce
|
|
378
|
+
let iv = [0u8; 16];
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Safe Patterns
|
|
382
|
+
|
|
383
|
+
```rust
|
|
384
|
+
// Cryptographic RNG
|
|
385
|
+
use rand::{rngs::OsRng, RngCore};
|
|
386
|
+
let mut bytes = [0u8; 32];
|
|
387
|
+
OsRng.fill_bytes(&mut bytes);
|
|
388
|
+
|
|
389
|
+
// Argon2 for passwords
|
|
390
|
+
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier, password_hash::SaltString};
|
|
391
|
+
let salt = SaltString::generate(&mut OsRng);
|
|
392
|
+
let hash = Argon2::default().hash_password(pwd.as_bytes(), &salt)?.to_string();
|
|
393
|
+
|
|
394
|
+
// AES-GCM (authenticated encryption) with unique nonce
|
|
395
|
+
use aes_gcm::{Aes256Gcm, KeyInit, aead::{Aead, OsRng, rand_core::RngCore}};
|
|
396
|
+
let cipher = Aes256Gcm::new(&key.into());
|
|
397
|
+
let mut nonce = [0u8; 12];
|
|
398
|
+
OsRng.fill_bytes(&mut nonce);
|
|
399
|
+
let ct = cipher.encrypt(&nonce.into(), plaintext)?;
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Async / Tokio Pitfalls
|
|
405
|
+
|
|
406
|
+
```rust
|
|
407
|
+
// FLAG HIGH: blocking I/O inside async (freezes the scheduler thread)
|
|
408
|
+
async fn handler() {
|
|
409
|
+
let data = std::fs::read("file").unwrap(); // FLAG: blocks tokio worker
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// SAFE
|
|
413
|
+
async fn handler() {
|
|
414
|
+
let data = tokio::fs::read("file").await.unwrap_or_default();
|
|
415
|
+
// or tokio::task::spawn_blocking for CPU-bound
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// FLAG HIGH: await point while holding std::sync::Mutex guard (deadlock risk + hangs)
|
|
419
|
+
let guard = std::sync::Mutex::lock(&m).unwrap();
|
|
420
|
+
some_future.await; // guard held across await
|
|
421
|
+
|
|
422
|
+
// SAFE: release before await, or use tokio::sync::Mutex (an async mutex)
|
|
423
|
+
|
|
424
|
+
// FLAG MEDIUM: unbounded channels from untrusted producers (memory DoS)
|
|
425
|
+
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
|
426
|
+
// SAFE: tokio::sync::mpsc::channel(capacity)
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## Unsafe FFI
|
|
432
|
+
|
|
433
|
+
```rust
|
|
434
|
+
// FLAG HIGH: extern "C" fn taking raw pointers without documented invariants
|
|
435
|
+
extern "C" fn callback(data: *const u8, len: usize) {
|
|
436
|
+
let slice = unsafe { std::slice::from_raw_parts(data, len) };
|
|
437
|
+
// no validation that caller respects lifetime/nullness
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// FLAG HIGH: CString::from_raw / CStr::from_ptr on unknown lifetimes
|
|
441
|
+
let s = unsafe { std::ffi::CStr::from_ptr(c_str_ptr) }; // UB if not null-terminated
|
|
442
|
+
|
|
443
|
+
// SAFE: document safety contract; validate before conversion
|
|
444
|
+
// SAFETY: contract documented in FFI header: caller must ensure ptr is null-terminated and static.
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Supply Chain (Cargo)
|
|
450
|
+
|
|
451
|
+
### Flag These
|
|
452
|
+
|
|
453
|
+
- `Cargo.toml` with `*` or open-ended version on security-sensitive crates: `ring = "*"`
|
|
454
|
+
- Missing `Cargo.lock` committed in a binary crate (library crates intentionally don't)
|
|
455
|
+
- `[patch.crates-io]` pointing to a git fork without pinning a commit hash — unpinned fork risk
|
|
456
|
+
- `build.rs` that downloads files at build time — can be compromised by a network attacker
|
|
457
|
+
- Crates without `#![deny(unsafe_code)]` in security-critical positions (auth, crypto) — optional but worth noting
|
|
458
|
+
- Using abandoned or unmaintained crates (check `cargo-audit` output for `RUSTSEC-*-yyyy-mmdd` advisories tagged as "unmaintained")
|
|
459
|
+
|
|
460
|
+
### Safe Patterns
|
|
461
|
+
|
|
462
|
+
```toml
|
|
463
|
+
# Cargo.toml
|
|
464
|
+
[dependencies]
|
|
465
|
+
argon2 = "0.5"
|
|
466
|
+
serde = { version = "1", features = ["derive"] }
|
|
467
|
+
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres"] }
|
|
468
|
+
|
|
469
|
+
# .cargo/config.toml — reproducible builds
|
|
470
|
+
[net]
|
|
471
|
+
offline = false
|
|
472
|
+
retry = 3
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Run `cargo audit` as part of CI. `cargo deny` adds supply-chain policy checks (licenses, advisories, duplicate versions, banned crates).
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## Framework-Specific
|
|
480
|
+
|
|
481
|
+
### Actix Web
|
|
482
|
+
|
|
483
|
+
```rust
|
|
484
|
+
// FLAG HIGH: handler with web::Json<T> without payload limit
|
|
485
|
+
async fn h(body: web::Json<T>) { /* attacker can send huge payloads */ }
|
|
486
|
+
// SAFE: App::new().app_data(web::JsonConfig::default().limit(1024 * 1024))
|
|
487
|
+
|
|
488
|
+
// FLAG HIGH: middleware order — auth middleware after the route
|
|
489
|
+
App::new().service(protected).wrap(Auth); // wrap must wrap before service, but inspect
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Axum
|
|
493
|
+
|
|
494
|
+
```rust
|
|
495
|
+
// FLAG HIGH: missing DefaultBodyLimit override for large uploads without bound
|
|
496
|
+
Router::new().route("/upload", post(upload))
|
|
497
|
+
.layer(DefaultBodyLimit::disable()); // FLAG: removes the 2MB default entirely
|
|
498
|
+
|
|
499
|
+
// FLAG MEDIUM: extractor order — body extractor before auth extractor in handler signature
|
|
500
|
+
async fn h(body: Json<T>, auth: AuthToken) { /* body consumed before auth check */ }
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Rocket
|
|
504
|
+
|
|
505
|
+
```rust
|
|
506
|
+
// FLAG CRITICAL: #[launch] fn with tls::none in production
|
|
507
|
+
rocket::build().configure(Config { tls: None, ..default() })
|
|
508
|
+
|
|
509
|
+
// FLAG HIGH: form guards without size limits
|
|
510
|
+
#[post("/submit", data = "<form>")]
|
|
511
|
+
fn submit(form: Form<BigStruct>) { /* no limits */ }
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Tonic (gRPC)
|
|
515
|
+
|
|
516
|
+
```rust
|
|
517
|
+
// FLAG HIGH: Server without max_decoding_message_size set
|
|
518
|
+
Server::builder().add_service(my_service).serve(addr).await?;
|
|
519
|
+
// SAFE: Server::builder().max_decoding_message_size(4 * 1024 * 1024)
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Research Checklist (before flagging)
|
|
525
|
+
|
|
526
|
+
1. **Is the input attacker-controlled?** — trace from HTTP extractor (`web::Json`, `axum::Json`, `rocket::Form`), from `tokio::net::TcpListener::accept`, from `serde_json::from_str(&body)`. Config / CLI args / env are not attacker-controlled.
|
|
527
|
+
2. **Is there a size limit upstream?** — `JsonConfig::limit`, `DefaultBodyLimit`, `#[serde(deny_unknown_fields)]`, custom `Deserialize` with bounds.
|
|
528
|
+
3. **Is the SQL call actually unsafe?** — `sqlx::query!` and DSL methods are safe; `query_unchecked` / `sql_query(format!(...))` are flags.
|
|
529
|
+
4. **Does `unsafe` have a `// SAFETY:` comment?** — absence is a smell, not always a bug, but worth flagging for review.
|
|
530
|
+
5. **Is `.unwrap()` on a `Result` that came from external input?** — on internal invariants (`.unwrap()` after `is_some()` check), it's fine.
|
|
531
|
+
6. **Does Cargo.lock exist in the committed tree for binary crates?** — reproducibility + supply-chain.
|
|
532
|
+
|
|
533
|
+
Only report findings that pass "attacker-controlled input + missing mitigation + exploitable sink (panic DoS, injection, memory corruption, or information leak)".
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## Grep Patterns
|
|
538
|
+
|
|
539
|
+
```bash
|
|
540
|
+
# unsafe blocks without SAFETY comment (approximate — inspect manually)
|
|
541
|
+
grep -rn "unsafe \({\|fn\)" --include="*.rs"
|
|
542
|
+
|
|
543
|
+
# .unwrap() / .expect() on HTTP/body/serde
|
|
544
|
+
grep -rn "\.(unwrap\|expect)()" --include="*.rs" | grep -E "req\.|body|headers|query|parse|from_str|from_slice"
|
|
545
|
+
|
|
546
|
+
# Raw SQL with interpolation
|
|
547
|
+
grep -rn "query_unchecked\|sql_query(format!\|from_string(format!" --include="*.rs"
|
|
548
|
+
|
|
549
|
+
# Dangerous Command patterns
|
|
550
|
+
grep -rn 'Command::new("\(sh\|bash\|cmd\)")' --include="*.rs"
|
|
551
|
+
|
|
552
|
+
# Weak hash for passwords
|
|
553
|
+
grep -rn "Md5::digest\|Sha1::digest" --include="*.rs"
|
|
554
|
+
|
|
555
|
+
# JWT signature validation disabled
|
|
556
|
+
grep -rn "insecure_disable_signature_validation" --include="*.rs"
|
|
557
|
+
|
|
558
|
+
# Blocking std I/O inside async
|
|
559
|
+
grep -rn "async fn" --include="*.rs" -A 20 | grep -E "std::fs::\|std::process::Command.*output"
|
|
560
|
+
|
|
561
|
+
# Non-CSPRNG for security
|
|
562
|
+
grep -rn "oorandom\|StepRng\|SmallRng" --include="*.rs"
|
|
563
|
+
|
|
564
|
+
# cargo config security
|
|
565
|
+
grep -n '^\s*version\s*=\s*"\*"' Cargo.toml
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
Run `cargo audit` for CVE advisories and `cargo deny check` for policy enforcement.
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## Cross-Reference
|
|
573
|
+
|
|
574
|
+
For concepts not specific to Rust:
|
|
575
|
+
- `../references/xss.md`
|
|
576
|
+
- `../references/injection.md`
|
|
577
|
+
- `../references/deserialization.md`
|
|
578
|
+
- `../references/csrf.md`
|
|
579
|
+
- `../references/authentication.md`
|
|
580
|
+
- `../references/authorization.md`
|
|
581
|
+
- `../references/cryptography.md`
|
|
582
|
+
- `../references/supply-chain.md`
|
|
583
|
+
- `../references/ssrf.md`
|
|
584
|
+
- `../references/file-security.md`
|