@brunosps00/dev-workflow 0.5.0 → 0.6.1

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.
@@ -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`